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:latestTo 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:apiPriority: inline env > .env.deploy.local > .env.deploy
Domains and services
| Service | Domain | Type | SSH Host | Remote Directory |
|---|---|---|---|---|
| Web | bridge-training.com | Static | btweb | ~/www/web/ |
| App | app.bridge-training.com | Static | btcom | ~/www/app/ |
| Player | player.bridge-training.com | Static | btcom | ~/www/player/ |
| API | api.bridge-training.com | Docker | btapi | /home/www/api/ |
| Docs | docs.bridge-training.com | Docker | btdocs | /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:
- Build the Docker image locally
- Create the remote directory if needed (
mkdir -p) - Upload the
compose.ymlto the server - Transfer the Docker image via SSH pipe (
docker save | gzip | ssh docker load) - 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/configon 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:
| Domain | Target |
|---|---|
bridge-training.com | Static files from ~/www/web/ |
app.bridge-training.com | Static files from ~/www/app/ |
player.bridge-training.com | Static files from ~/www/player/ |
api.bridge-training.com | http://localhost:3001 (Docker) |
docs.bridge-training.com | http://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 imageApp-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/.