1 Deployment
Emily edited this page 2026-11-04 23:26:39 +01:00

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 port 6767.
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.

E-mail

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:

  1. Checks .env exists and the Git tree is clean.
  2. Takes a pg_dump backup to backups/pre-update-<UTC>.dump if the database is running.
  3. git pull --ff-only.
  4. Pulls the PostgreSQL base image.
  5. Rebuilds the accounts image.
  6. Runs node src/migrate.js in a one-off container.
  7. 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_SECRET is random and at least 32 characters.
  • Database password is strong and stored only in .env.
  • CORS_ORIGINS lists the exact web origin.
  • HTTPS terminates at the reverse proxy.
  • TRUSTED_PROXIES lists only the actual proxy.
  • SMTP mode matches the port.
  • PUBLIC_BASE_URL and APP_PUBLIC_URL are correct.
  • MANAGEMENT_PASSWORD is set only if the panel is needed.
  • Backups run before upgrades and are copied off-host.
  • GET /healthz returns status: 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:

  1. Stop the accounts service.
  2. Check out the previous tag.
  3. Restore the pre-update dump if the schema changed.
  4. Rebuild and start.

Database migrations are forward-only. Restoring a backup is the supported way to move backwards across a schema change.