This guide covers the Docker Compose production stack on a single VPS. Run state-changing commands only when the deployment or maintenance action has been explicitly authorized.
The production stack combines compose.yml and compose.prod.yml:
| Service | Role |
|---|---|
api |
Express API and static client on port 8080 inside Compose |
socket |
Socket.IO on port 9000 inside Compose |
mongo |
Primary application database and session store |
postgres |
Optional dual-write and analytics database |
redis |
Socket.IO adapter and cross-instance coordination |
nginx |
TLS termination and public reverse proxy |
API and Socket.IO run the same letscube-app image with separate commands.
Production images are tagged with the full deployed commit SHA.
The host needs Docker Engine, Docker Compose, Git, the repository checkout, and
TLS certificates. Copy .env.example to an untracked .env.prod, replace every
placeholder, and restrict its permissions.
Required secrets include:
AUTH_SECRET;WCA_CLIENT_IDandWCA_CLIENT_SECRET;REACT_APP_WCA_CLIENT_IDfor the browser build; andPOSTGRES_PASSWORDwhen PostgreSQL is enabled.
Production TLS mounts default to /etc/letsencrypt, with nginx expecting the
certificate for www.letscube.net. MongoDB, PostgreSQL, and Redis should remain
internal to the Compose network.
Do not assume a checkout path from an example or an old server. Determine the
active Compose project directory and pass it as APP_DIR.
Before deploying:
- Confirm the intended Docker context and clear stale
DOCKER_HOSTvalues. - Confirm the checkout is owned by the deployment account and has no local changes.
- Fetch the target branch and record the current and target commit SHAs.
- Check free disk space and Docker usage.
- Record current container status, image IDs, restart counts, and public health.
Typical read-only checks:
git status --short
git rev-parse HEAD
git rev-parse origin/master
df -h /
docker system df
docker compose -f compose.yml -f compose.prod.yml --env-file .env.prod ps
curl -fsS https://letscube.net/health/api
curl -fsS https://letscube.net/health/socketUse scripts/deploy.sh for the Docker production stack:
APP_DIR=/path/to/active/checkout \
ENV_FILE=.env.prod \
scripts/deploy.shThe default DEPLOY_TARGET=auto classifies the files changed since the previous
commit:
| Changed paths | Target |
|---|---|
| Documentation, CI, or agent metadata only | none |
| Browser client only | api |
| Server, shared protocol, dependencies, containers, deployment, or unknown | all |
Override only when the runtime impact is known:
DEPLOY_TARGET=api APP_DIR=/path/to/checkout scripts/deploy.sh
DEPLOY_TARGET=socket APP_DIR=/path/to/checkout scripts/deploy.sh
DEPLOY_TARGET=all APP_DIR=/path/to/checkout scripts/deploy.sh
DEPLOY_TARGET=none APP_DIR=/path/to/checkout scripts/deploy.shDEPLOY_FORCE=true rebuilds both application services at the current commit.
DEPLOY_WAIT_TIMEOUT_SECONDS changes the default 60-second readiness deadline.
The deploy script:
- fetches and fast-forwards the configured branch;
- builds one immutable application image;
- starts backing services without recreating them;
- applies committed PostgreSQL migrations;
- snapshots the current image for every selected application service;
- replaces and health-checks Socket.IO before API when both are selected; and
- reloads nginx after each replacement.
Socket.IO is deployed first so a backward-compatible server is available before the new browser bundle is served. Shared protocol changes must support this brief mixed-version window.
scripts/update-production.sh is the older PM2-oriented updater. It is not the
canonical path for the Docker Compose stack.
A build, backing-service, or migration failure occurs before application
containers are replaced. If replacement readiness or nginx reload fails,
scripts/deploy.sh restores every attempted application service in reverse
order from its exact pre-deploy image and Compose definition, then reloads
nginx.
The Git checkout remains at the fetched commit after failure. Correct the cause and rerun with the explicit target printed by the script.
Database migrations are not rolled back automatically. Every production migration must remain compatible with the previous application image.
The deploy script can be tested without production access:
scripts/test-deploy-classifier.sh
scripts/test-deploy.sh
scripts/test-mongo-backup-restore.sh
scripts/test-mongo-backup-restore-live.sh
scripts/classify-deploy-target.sh client/src/components/App.jsxThe live MongoDB rehearsal starts two disposable mongo:7 containers on an
isolated Docker network, writes only a synthetic attempt, restores it with
--drop, verifies it, and removes the containers and network on exit. It never
connects to the configured production or development database.
After deployment, verify:
- the checkout exactly matches the intended
origin/mastercommit; - selected containers use that commit's image and have zero unexpected restarts;
- all Compose services are healthy;
/health/apiand/health/socketsucceed publicly;- a real Socket.IO connection completes; and
- recent API, socket, nginx, and migration logs contain no new errors.
Useful commands:
docker compose -f compose.yml -f compose.prod.yml --env-file .env.prod ps
docker compose -f compose.yml -f compose.prod.yml --env-file .env.prod logs --tail=200 api
docker compose -f compose.yml -f compose.prod.yml --env-file .env.prod logs --tail=200 socket
docker compose -f compose.yml -f compose.prod.yml --env-file .env.prod logs --tail=200 nginx/health/api requires MongoDB and treats PostgreSQL as optional.
/health/socket requires MongoDB and Redis and treats PostgreSQL as optional. An
optional PostgreSQL failure returns HTTP 200 with status degraded; a required
dependency failure returns HTTP 503 with status error.
The default Socket.IO namespace also supports health_check, returning a health
report through its acknowledgement or a health_status event.
Create a MongoDB backup with an explicit checkout and destination:
APP_DIR=/path/to/checkout \
BACKUP_DIR=/path/to/backups \
scripts/backup-mongo.shThe script creates gzip archives and retains 7 daily, 4 weekly, and 3 monthly
local backups. Set BACKUP_S3_URI and the documented AWS-compatible variables
to copy new archives off-server.
Backups are only useful if restore is tested periodically against non-production data. Monitor backup age, size, and off-server upload failures.
Restore is destructive: mongorestore --drop replaces matching collections in
the target database. Verify the archive, target URI, and current backup before
continuing.
APP_DIR=/path/to/checkout \
scripts/restore-mongo.sh /path/to/letscube-mongo-backup.archive.gzThe script requires typing RESTORE. Never test restore against the production
database.
Keep enough free space for a cold application image build as well as current and rollback images. On the current small VPS class, 8–10 GB free is a practical operating target; investigate at roughly 75% filesystem use and treat 85% as critical.
Inspect before deleting:
df -h /
docker system df -v
journalctl --disk-usage
du -sh /path/to/backupsSafe retention principles:
- keep the current image and at least two known-good rollback revisions;
- prune old unused build cache and dangling images, not in-use images;
- configure bounded Docker and systemd-journal retention;
- keep backup retention explicit; and
- never prune Docker volumes as routine cleanup.
See the backup scripts before changing retention paths or schedules. Cron and other host automation are external state; inspect the deployed account's actual crontab rather than assuming the repository documents its current schedule.
- Snapshot the old VPS and take a fresh off-server MongoDB backup.
- Provision Docker Engine and Compose on the new host.
- Clone the repository and create a fresh
.env.prod. - Install or copy TLS certificates securely.
- Start MongoDB, PostgreSQL, and Redis.
- Restore MongoDB and apply PostgreSQL migrations.
- Start API, Socket.IO, and nginx.
- Test login, room creation/join, timing, reconnect, health, backups, and restore against non-production data.
- Confirm data services are not publicly reachable.
- Move DNS and retain the old VPS until the new stack is stable.