Shumiland/docs/deploy.md
Vadym Samoilenko 9b41fa447a
Some checks are pending
CI / Type Check (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Unit Tests (push) Waiting to run
Deploy / Build & Push Image (push) Waiting to run
Deploy / Deploy to VPS (push) Blocked by required conditions
feat: complete backend B1-B7 — Payload CMS, ezy payments, leads, deploy
- 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>
2026-05-09 19:14:54 +01:00

6.7 KiB

Deployment Guide — Shumiland VPS

Prerequisites

  • VPS with Ubuntu 22.04+, Docker + Docker Compose v2 installed
  • Domain shumiland.com.ua DNS 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:

  1. CI runs lint + typecheck + tests + build
  2. Docker image is built and pushed to ghcr.io/aimpress/shumiland:<sha>
  3. 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