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

244 lines
6.7 KiB
Markdown

# 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
```bash
git clone git@github.com:aimpress/shumiland-site-dev.git /opt/shumiland
cd /opt/shumiland
```
### 2. Create `.env.production`
```bash
cp .env.example .env.production
nano .env.production
```
Fill in all values (see table below). Generate secrets with:
```bash
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:
```bash
# 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
```bash
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
```
### 5. Run database migrations
```bash
docker compose -f docker-compose.prod.yml exec app npx payload migrate
```
### 6. Seed initial data and sync tariffs
```bash
# 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
```bash
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
```bash
cd /opt/shumiland
docker compose -f docker-compose.prod.yml restart app
```
### View logs
```bash
# 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
```bash
docker compose -f docker-compose.prod.yml ps
```
### Update to latest image manually
```bash
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
```bash
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.
```bash
# List existing backups
ls -lh /opt/shumiland/backups/
```
### Restore from backup
```bash
# 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:
```bash
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
```bash
# 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
```bash
# 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
```bash
# 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
```bash
# 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
```