Deployment — Operations¶
Day-2 tasks for a running Genesis stack. All commands run from the
genesis-deploy checkout on the host; dc below is shorthand for:
alias dc='docker compose -f docker/docker-compose.yml --env-file .env'
Status & logs¶
dc ps # container status + health
dc logs -f backend # follow backend logs (also on disk in ./logs/)
dc logs -f frontend
dc logs --tail 100 postgres
Log files & retention¶
There are two independent log stores, both size-bounded and self-pruning — no manual cleanup is needed:
| Store | What it is | Cap / retention | Configured in |
|---|---|---|---|
| Container logs | stdout/stderr of every service (what dc logs reads) |
json-file driver, ~50 MB per service (5 × 10 MB, rotated) | docker/docker-compose.yml |
| Backend file log | ./logs/genesis.log on the host (prod profile only) |
rolls daily and at 50 MB, keeps 30 days, hard cap 2 GB total | backend logback-spring.xml |
The backend file log is bind-mounted from the container (./logs:/app/logs), so
it survives container rebuilds and is handy for grep/tail after a container
has been removed:
tail -f logs/genesis.log
ls logs/ # genesis.log + dated, size-indexed archives
Both stores rotate and delete their own oldest files, so disk usage stays
bounded without a cron job. If you ever need to reclaim space from old image
layers (not logs), docker system prune is safe.
Update to a new release¶
# 1. In the app repo(s): fast-forward uni-prod from main, push.
# 2. On the host:
git pull
./scripts/deploy.sh
Only changed layers rebuild. The database is untouched; Flyway applies any new migrations on backend startup.
Rollback¶
# In the affected app repo (example: one release back):
git checkout uni-prod
git reset --hard <last-good-commit-or-tag>
git push --force-with-lease origin uni-prod
# On the host:
./scripts/deploy.sh
Warning
Rolling back code does not roll back database migrations. If the bad release introduced a migration, restore the database from backup (below) before starting the rolled-back backend.
Database backup & restore¶
# backup (run nightly via cron)
dc exec postgres pg_dump -U postgres genesis | gzip > "backup-$(date +%F).sql.gz"
# restore into a fresh volume
dc down
docker volume rm genesis_pgdata
dc up -d postgres
gunzip -c backup-YYYY-MM-DD.sql.gz | dc exec -T postgres psql -U postgres genesis
dc up -d
Example crontab entry (02:30 nightly, keep 14 days):
30 2 * * * cd /path/to/genesis-deploy && docker compose -f docker/docker-compose.yml --env-file .env exec -T postgres pg_dump -U postgres genesis | gzip > backups/backup-$(date +\%F).sql.gz && find backups -name 'backup-*.sql.gz' -mtime +14 -delete
Restart / stop¶
dc restart backend # one service
dc down # stop everything (data persists in the volume)
dc up -d # start again
All services use restart: unless-stopped, so the stack survives host
reboots once Docker starts.
Hardening checklist (recommended)¶
- Put nginx or Caddy in front of both apps for TLS and a single public port; then close 3000/8080 on the firewall and only expose 443.
- Keep PostgreSQL unexposed (the compose file already does this).
- Store
.envreadable only by the deploy user (chmod 600 .env). - Set up the backup cron above and periodically test a restore.
- Keep the host patched;
docker system pruneoccasionally to reclaim space from old image layers.
Troubleshooting¶
| Symptom | Likely cause / fix |
|---|---|
deploy.sh fails at fetch |
Host can't reach the git host or lacks access to the repos — check deploy keys / token in the repo URLs |
| Backend unhealthy, logs show JWT error | JWT_SECRET missing or shorter than 32 chars |
| Backend unhealthy, logs show CORS error at boot | CORS_ALLOWED_ORIGINS unset (required in prod) |
| Browser shows network errors calling the API | NEXT_PUBLIC_API_URL wrong or backend port blocked by firewall — remember it's evaluated in the browser |
| Login works but uploads fail | With STORAGE_PROVIDER=cloudinary: Cloudinary credentials wrong. With local: the data/uploads volume isn't writable. Or the file exceeds the 25 MB action limit |
Uploads succeed but tokenization fails (local provider) |
The data/uploads volume isn't persistent or is shared across multiple backend instances — local needs a single instance on durable storage |
| Frontend rebuilt but old API URL persists | NEXT_PUBLIC_API_URL is baked at build time — rerun deploy.sh after changing it (it rebuilds the image) |