Deployment
Docker Compose
The repository ships a two-service Compose stack:
db— PostgreSQL 16 with a health check and connection logging.accounts— the Node.js backend on port6767.
git clone https://git.emilyh.de/Emily/UniversalGamingApp-Backend.git
cd UniversalGamingApp-Backend
cp .env.example .env
Edit .env and set at least:
JWT_SECRET=<at-least-32-random-characters>
POSTGRES_DB=universal_gaming
POSTGRES_USER=universal_gaming
POSTGRES_PASSWORD=<strong-password>
CORS_ORIGINS=https://games.example.com
PUBLIC_BASE_URL=https://account.example.com
APP_PUBLIC_URL=https://games.example.com
Start:
docker compose up -d --build
docker compose ps
curl -fsS http://127.0.0.1:6767/healthz
The first start initializes the database and applies migrations.
Reverse proxy
Put the backend behind HTTPS. Example Caddy:
account.example.com {
encode gzip
reverse_proxy 127.0.0.1:6767
}
Example Nginx:
server {
listen 443 ssl http2;
server_name account.example.com;
ssl_certificate /etc/letsencrypt/live/account.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/account.example.com/privkey.pem;
client_max_body_size 64k;
location / {
proxy_pass http://127.0.0.1:6767;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
}
}
Then configure trusted proxies:
TRUSTED_PROXIES=127.0.0.1,::1
If the proxy is on another private network, add its exact IP or CIDR. Do not use TRUST_PROXY=true on a public-facing server.
CORS
The web app runs on a different origin from the account server. Set:
CORS_ORIGINS=https://games.example.com
Multiple origins are comma-separated. * allows every origin and should only be used for local development.
Native mobile and desktop builds do not enforce browser CORS, but the same server settings apply.
Enable verification and password reset by configuring SMTP:
EMAIL_CONFIRMATION_ENABLED=true
PUBLIC_BASE_URL=https://account.example.com
APP_PUBLIC_URL=https://games.example.com
SMTP_HOST=mail.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_REQUIRE_TLS=true
SMTP_USER=noreply@example.com
SMTP_PASSWORD=<secret>
SMTP_FROM=noreply@example.com
PASSWORD_RESET_TTL_MINUTES=30
Port guidance:
| Port | Mode | Settings |
|---|---|---|
465 |
Implicit TLS | SMTP_SECURE=true |
587, 25, 2525 |
STARTTLS | SMTP_SECURE=false, SMTP_REQUIRE_TLS=true |
The backend logs the effective mode at startup:
[mail] SMTP host=mail.example.com port=587 secure=false requireTLS=true
Verify from inside the container:
docker compose exec accounts node -e "
import('./src/config.js').then(async ({loadConfig}) => {
const {createMailer} = await import('./src/mailer.js');
const m = createMailer(loadConfig());
console.log(m.enabled ? 'mailer enabled' : 'mailer disabled');
await m.verify();
console.log('SMTP verify ok');
});
"
E-mail failure never blocks account creation or login.
Upgrades
Use the included script from the repository root:
./update.sh
It performs:
- Checks
.envexists and the Git tree is clean. - Takes a
pg_dumpbackup tobackups/pre-update-<UTC>.dumpif the database is running. git pull --ff-only.- Pulls the PostgreSQL base image.
- Rebuilds the accounts image.
- Runs
node src/migrate.jsin a one-off container. docker compose up -d --remove-orphans.
If the working tree is dirty, the script refuses to run. Commit or stash changes first.
Manual upgrade
git pull --ff-only
docker compose pull db
docker compose build --pull accounts
docker compose run --rm accounts node src/migrate.js
docker compose up -d --remove-orphans
Always take a backup before applying migrations to a production database.
Images
The Compose default image is:
git.emilyh.de/emily/universal-gaming-backend:1.4.0
Override it with ACCOUNT_IMAGE. To publish:
docker login git.emilyh.de
docker build -t git.emilyh.de/emily/universal-gaming-backend:1.4.0 .
docker tag git.emilyh.de/emily/universal-gaming-backend:1.4.0 \
git.emilyh.de/emily/universal-gaming-backend:latest
docker push git.emilyh.de/emily/universal-gaming-backend:1.4.0
docker push git.emilyh.de/emily/universal-gaming-backend:latest
The image runs as the unprivileged node user and exposes 6767.
PostgreSQL password changes
POSTGRES_PASSWORD only initializes a new data directory. Changing it in .env does not change the existing role password.
docker compose exec db \
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
-c "ALTER ROLE \"$POSTGRES_USER\" WITH PASSWORD '<new-password>';"
Then update .env and restart the backend. Never delete the volume to apply a password change.
Production checklist
JWT_SECRETis random and at least 32 characters.- Database password is strong and stored only in
.env. CORS_ORIGINSlists the exact web origin.- HTTPS terminates at the reverse proxy.
TRUSTED_PROXIESlists only the actual proxy.- SMTP mode matches the port.
PUBLIC_BASE_URLandAPP_PUBLIC_URLare correct.MANAGEMENT_PASSWORDis set only if the panel is needed.- Backups run before upgrades and are copied off-host.
GET /healthzreturnsstatus: ok.- A test registration, login, score and chess challenge succeed.
- Request logs show the real client IP, not the proxy IP.
- Old images are pruned periodically.
Health and rollback
curl -fsS https://account.example.com/healthz
docker compose ps
docker compose logs --tail=100 accounts
If a release is broken:
- Stop the accounts service.
- Check out the previous tag.
- Restore the pre-update dump if the schema changed.
- Rebuild and start.
Database migrations are forward-only. Restoring a backup is the supported way to move backwards across a schema change.
Universal Gaming Accounts
- Home
- Deployment
- Configuration
- API Reference
- Database and Migrations
- Security
- Operations
- Development and Testing
- Troubleshooting
Repositories
The accounts backend is optional. Universal Gaming remains fully playable offline without it.