Bridge Training
Contributing

Deployment

How to build and deploy apps to production servers.

Overview

Apps are deployed to remote servers over SSH. Docker-based services (API, Docs) are transferred as images and run via docker compose. Static apps (App, Web, Player) are built locally and copied directly to the server's filesystem.

Each Docker-based app has a release script in scripts/ and a compose.yml in its app directory. Static apps have a release script only.

Deploy configuration

All deploy settings live in .env.deploy at the monorepo root (committed):

# API
API_HOST=btapi
API_REMOTE_DIR=/home/www/api
API_PORT=3001
API_IMAGE=bridge-training-api:latest

# Docs
DOCS_HOST=btdocs
DOCS_REMOTE_DIR=/home/www/docs
DOCS_PORT=3002
DOCS_IMAGE=bridge-training-docs:latest

To override locally (e.g. different SSH host), create .env.deploy.local — it's gitignored via the *.local rule.

Environment variables can also be passed inline:

API_HOST=my-server pnpm release:api

Priority: inline env > .env.deploy.local > .env.deploy

Domains and services

ServiceDomainTypeSSH HostRemote Directory
Webbridge-training.comStaticbtweb~/www/web/
Appapp.bridge-training.comStaticbtcom~/www/app/
Playerplayer.bridge-training.comStaticbtcom~/www/player/
APIapi.bridge-training.comDockerbtapi/home/www/api/
Docsdocs.bridge-training.comDockerbtdocs/home/www/docs/

SSH hosts (btapi, btdocs, btweb, btcom) are aliases defined in ~/.ssh/config. They may point to the same server.

Release commands

From the monorepo root:

pnpm release:api      # Build + deploy API (Docker)
pnpm release:app      # Build + deploy main app (scp)
pnpm release:docs     # Build + deploy docs (Docker)
pnpm release:player   # Build + deploy deal player (scp)
pnpm release:web      # Build + deploy marketing site (tar + ssh)

How Docker releases work

The release:api and release:docs scripts follow the same pipeline:

  1. Build the Docker image locally
  2. Create the remote directory if needed (mkdir -p)
  3. Upload the compose.yml to the server
  4. Transfer the Docker image via SSH pipe (docker save | gzip | ssh docker load)
  5. Restart the service with docker compose up -d --force-recreate

Image transfer

docker save "$IMAGE" | gzip | ssh "$HOST" "gzip -d | docker load"

The image is streamed, compressed, and loaded on the remote server — nothing is written to disk intermediately.

Server prerequisites

Software

  • Docker with the Compose plugin (v2) — required for API and Docs
  • A web server (Nginx or Caddy) — serves static files and reverse-proxies Docker services
  • SSH access configured via ~/.ssh/config on the deploying machine

Remote directory structure

# Docker services
/home/www/api/
├── compose.yml    # Uploaded by the release script
└── .env           # Manually created with production credentials

/home/www/docs/
├── compose.yml
└── .env

# Static apps (served by web server)
~/www/web/        # Web (bridge-training.com)
~/www/app/        # App (app.bridge-training.com)
~/www/player/     # Player (player.bridge-training.com)

Reverse proxy configuration

The web server must route each domain to the correct backend:

DomainTarget
bridge-training.comStatic files from ~/www/web/
app.bridge-training.comStatic files from ~/www/app/
player.bridge-training.comStatic files from ~/www/player/
api.bridge-training.comhttp://localhost:3001 (Docker)
docs.bridge-training.comhttp://localhost:3002 (Docker)

Static apps are SPAs — the web server should serve index.html for all non-file routes (try_files or equivalent).

Docs must be protected with HTTP basic authentication at the web server level (e.g. Nginx auth_basic or Caddy basicauth).

Docker build (local)

To build Docker images locally without deploying:

pnpm docker:build:api     # Build API image
pnpm docker:build:docs    # Build docs image

App-specific notes

API

The API is bundled with tsup into a single JS file (dist/index.js). The Docker runner stage contains only Node.js and the bundle — no node_modules, no pnpm. Listens on port 3001.

Docs

Next.js app running in a Docker container on port 3002.

App

Vite SPA deployed as static files via scp to ~/www/app/.

Player

Vite SPA deployed as static files via scp to ~/www/player/.

Web

Static marketing site deployed via tar over SSH to ~/www/web/.

On this page