- 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>
244 lines
6.7 KiB
Markdown
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
|
|
```
|