- B1: Next.js 15 + Payload CMS 3.0 + Postgres 16, ESLint, Prettier, Husky, Vitest - B2: 9 collections, 6 globals, 12 Page Builder blocks, access control, slugify/revalidate hooks - B3: ezy.com.ua payments, Binotel HMAC webhook, leads API, Telegram bot, Resend email, rate limiting - B4: Tariffs collection with ezy API sync (cron + manual), dynamic pricing source-of-truth - B5: 13 test files covering unit libs and all API routes - B6: Dockerfile multi-stage, docker-compose.prod.yml, nginx.conf SSL, GitHub Actions CI/CD, health endpoint - B7: docs/admin-guide-ua.md (marketer guide), docs/deploy.md (VPS instructions), README quickstart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.7 KiB
6.7 KiB
Deployment Guide — Shumiland VPS
Prerequisites
- VPS with Ubuntu 22.04+, Docker + Docker Compose v2 installed
- Domain
shumiland.com.uaDNS A-record pointing to VPS IP - SSH access as non-root user with sudo
- GitHub Actions secrets configured (see CI/CD section)
First Deploy
1. Clone the repository
git clone git@github.com:aimpress/shumiland-site-dev.git /opt/shumiland
cd /opt/shumiland
2. Create .env.production
cp .env.example .env.production
nano .env.production
Fill in all values (see table below). Generate secrets with:
openssl rand -base64 32
| Variable | Value |
|---|---|
DATABASE_URL |
postgresql://shumiland:<POSTGRES_PASSWORD>@postgres:5432/shumiland |
POSTGRES_PASSWORD |
Generate with openssl |
PAYLOAD_SECRET |
Generate with openssl |
REVALIDATE_SECRET |
Generate with openssl |
SYNC_SECRET |
Generate with openssl |
CRON_SECRET |
Generate with openssl |
EZY_PARTNER_KEY |
From ezy.com.ua dashboard |
EZY_ACTIVITY |
From ezy.com.ua dashboard |
TELEGRAM_BOT_TOKEN |
From BotFather |
TELEGRAM_CHAT_ID |
Manager group chat ID |
RESEND_API_KEY |
From resend.com |
MANAGER_EMAILS |
Comma-separated manager emails |
BINOTEL_HMAC_SECRET |
From Binotel dashboard → Webhooks |
CERTBOT_DOMAIN |
shumiland.com.ua |
CERTBOT_EMAIL |
admin@ai-impress.com |
NEXT_PUBLIC_SITE_URL |
https://shumiland.com.ua |
3. Obtain SSL certificate
Run certbot once to get the certificate before starting nginx:
# Start only postgres and certbot (nginx needs cert to start)
docker compose -f docker-compose.prod.yml run --rm certbot
# Verify cert created
ls certbot/conf/live/shumiland.com.ua/
4. Pull image and start all services
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
5. Run database migrations
docker compose -f docker-compose.prod.yml exec app npx payload migrate
6. Seed initial data and sync tariffs
# Create admin user + seed globals
docker compose -f docker-compose.prod.yml exec app pnpm seed
# Sync tariffs from ezy API
curl -X POST https://shumiland.com.ua/api/tariffs/sync \
-H "Authorization: Bearer <SYNC_SECRET>"
7. Verify health
curl https://shumiland.com.ua/api/health
# Expected: {"status":"healthy","db":"ok","ezy":"ok","ts":...}
GitHub Actions CI/CD
Required Secrets (Settings → Secrets and variables → Actions)
| Secret | Description |
|---|---|
GHCR_TOKEN |
GitHub Personal Access Token with write:packages |
VPS_HOST |
VPS IP address |
VPS_USER |
SSH username |
VPS_SSH_KEY |
Private SSH key (cat ~/.ssh/id_ed25519) |
VPS_PORT |
SSH port (default: 22) |
Deploy Flow
Every push to main:
- CI runs lint + typecheck + tests + build
- Docker image is built and pushed to
ghcr.io/aimpress/shumiland:<sha> - SSH into VPS →
docker compose pull && docker compose up -d
Routine Operations
Restart services
cd /opt/shumiland
docker compose -f docker-compose.prod.yml restart app
View logs
# App logs (last 100 lines, follow)
docker compose -f docker-compose.prod.yml logs -f --tail=100 app
# Nginx access logs
docker compose -f docker-compose.prod.yml logs -f nginx
Check service status
docker compose -f docker-compose.prod.yml ps
Update to latest image manually
cd /opt/shumiland
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
Database Backup & Restore
Manual backup
docker compose -f docker-compose.prod.yml exec postgres \
pg_dump -U shumiland shumiland | gzip > /tmp/manual-backup-$(date +%Y%m%d).sql.gz
Automated backups
The pg_backup service runs daily at 03:00 UTC. Backups are stored in ./backups/ and older than 14 days are deleted automatically.
# List existing backups
ls -lh /opt/shumiland/backups/
Restore from backup
# Stop the app (keep postgres running)
docker compose -f docker-compose.prod.yml stop app
# Restore
gunzip -c /opt/shumiland/backups/<filename>.sql.gz | \
docker compose -f docker-compose.prod.yml exec -T postgres \
psql -U shumiland shumiland
# Start app
docker compose -f docker-compose.prod.yml start app
SSL Certificate Renewal
Certbot renews automatically via the certbot container. To renew manually:
docker compose -f docker-compose.prod.yml run --rm certbot renew
docker compose -f docker-compose.prod.yml restart nginx
Troubleshooting
App fails to start
# Check logs
docker compose -f docker-compose.prod.yml logs app
# Common causes:
# - Missing .env.production variable → check all required vars are set
# - DB not ready → check postgres health: docker compose ps
# - Port 3000 conflict → check nothing else runs on port 3000
nginx returns 502 Bad Gateway
# Check if app container is running
docker compose -f docker-compose.prod.yml ps
# Check app logs for startup errors
docker compose -f docker-compose.prod.yml logs --tail=50 app
Database connection error
# Test DB connectivity
docker compose -f docker-compose.prod.yml exec postgres \
psql -U shumiland -c "SELECT 1"
# Check DATABASE_URL in .env.production matches POSTGRES_PASSWORD
Tariffs not syncing
# Manual sync
curl -X POST https://shumiland.com.ua/api/tariffs/sync \
-H "Authorization: Bearer <SYNC_SECRET>" -v
# Check EZY_ACTIVITY and EZY_PARTNER_KEY are set
docker compose -f docker-compose.prod.yml exec app env | grep EZY