This guide walks through deploying the Backgammon application to an AWS Lightsail Instance running docker-compose. Images are pushed to GitHub Container Registry (GHCR) by the Deploy to Lightsail GitHub Actions workflow; the deploy step then SSHes into the Lightsail Instance and pulls the new images.
GitHub push → Actions builds 3 images (server, webclient, gnubg)
→ pushes to ghcr.io/garrettbeatty/backgammon-{name}
→ SSHes to Lightsail Instance
→ docker compose pull && up -d (6 containers)
The instance runs everything in containers: postgres, redis, gnubg-service, server, webclient, caddy. Persistent state (Postgres data, Redis AOF, Caddy certificates) lives on the instance's root disk.
- AWS account with Lightsail access
- A registered domain (for HTTPS via Caddy + Let's Encrypt)
- An SSH key pair for the deploy bot (separate from any personal key)
- Open the Lightsail console.
- Create instance:
- Region: pick one close to your users (e.g.,
us-east-1). - Platform: Linux/Unix.
- Blueprint: Ubuntu 24.04 LTS (OS Only).
- Instance plan: $10/mo at minimum (2 GB RAM, 2 vCPU, 60 GB SSD). The $5 plan (512 MB) cannot run the full 6-container stack reliably.
- Name:
backgammon-prod(or whatever you like).
- Region: pick one close to your users (e.g.,
- Create.
- In Lightsail: Networking → Create static IP.
- Attach it to the instance you just created.
- Note the IP — you'll need it for DNS and GitHub secrets.
In the instance's Networking tab, add these inbound rules (in addition to the default SSH on 22):
| Application | Protocol | Port |
|---|---|---|
| HTTP | TCP | 80 |
| HTTPS | TCP | 443 |
Caddy handles TLS termination on 443 and ACME challenges on 80.
SSH into the instance using the default Lightsail key (downloadable from the console's Account → SSH keys page) or the browser-based SSH session:
ssh -i ~/.ssh/LightsailDefaultKey.pem ubuntu@<STATIC_IP># Docker
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker ubuntu
# Re-login for the group change to take effect
exit
# (SSH back in)
# Verify
docker --version
docker compose version # Compose v2 ships with the Docker packagemkdir -p ~/backgammon
cd ~/backgammoncat > ~/backgammon/.env <<'EOF'
POSTGRES_USER=postgres
POSTGRES_PASSWORD=<generate a strong password>
JWT_SECRET=<generate a 64-char random string>
DOMAIN=yourdomain.com
TLS_EMAIL=you@yourdomain.com
EOF
chmod 600 ~/backgammon/.envGenerate secrets locally with openssl rand -base64 48 and paste them in.
Generate a fresh keypair locally (no passphrase — required for non-interactive GitHub Actions deploys):
ssh-keygen -t ed25519 -f ~/.ssh/backgammon-deploy -C "backgammon-ci" -N ""Append the public key to the instance's authorized_keys:
# On your laptop
cat ~/.ssh/backgammon-deploy.pub
# On the instance
echo "<paste public key>" >> ~/.ssh/authorized_keysIf your GHCR images are public, you can skip this. If they are private (the default), the instance needs a GitHub Personal Access Token with read:packages scope:
- On GitHub: Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token with the
read:packagesscope. Save the token. - On the instance:
The credential is cached at
echo "<TOKEN>" | docker login ghcr.io -u garrettbeatty --password-stdin
~/.docker/config.jsonand reused by subsequentdocker compose pullcalls.
Point your domain at the Lightsail static IP:
| Record | Name | Value |
|---|---|---|
| A | @ (or subdomain) |
<STATIC_IP> |
| A | www (optional) |
<STATIC_IP> |
DNS propagation can take a few minutes to a few hours. Caddy will automatically obtain a Let's Encrypt certificate on first request once the domain resolves.
In Settings → Secrets and variables → Actions, add:
| Secret | Value |
|---|---|
LIGHTSAIL_HOST |
Lightsail static IP (e.g., 54.83.12.45) |
LIGHTSAIL_USER |
ubuntu (or whichever user owns ~/backgammon) |
LIGHTSAIL_SSH_PRIVATE_KEY |
Contents of ~/.ssh/backgammon-deploy (the private key) |
The workflow does not need any AWS credentials — GHCR auth uses GITHUB_TOKEN, which is injected automatically.
Push to main (or run the workflow manually from the Actions tab). The workflow will:
- Build three ARM64 images and push them to
ghcr.io/garrettbeatty/backgammon-{server,webclient,gnubg}:<sha>(and:latest). - SCP
docker-compose.prod.ymlandCaddyfileto the instance. - SSH in and run
docker compose pull && up -d. - Wait for every container's health check to report
healthy. - Reload Caddy.
You can also deploy manually from the instance once images exist:
cd ~/backgammon
export IMAGE_TAG=latest
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -dFrom your laptop:
curl -I https://yourdomain.com/health
# HTTP/2 200From the instance:
docker ps --format 'table {{.Names}}\t{{.Status}}'
docker compose -f docker-compose.prod.yml logs --tail 50 serverLightsail Instances support automatic snapshots (in Snapshots tab) — schedule a daily snapshot at minimum. The Postgres data lives in the postgres_data Docker volume on the instance's disk, so a Lightsail snapshot captures it. For point-in-time DB backup, run pg_dump from a periodic cron job and write the dump elsewhere (e.g., S3 or Backblaze B2).
docker compose pullfails withdenied: denied— GHCR credentials missing or expired. Re-rundocker login ghcr.ioon the instance with a fresh PAT.- Caddy can't get a certificate — DNS hasn't propagated yet, or port 80 is blocked.
dig yourdomain.comfrom your laptop to confirm the A record resolves to the static IP;docker logs backgammon-caddyfor ACME errors. - Server can't reach Postgres —
docker compose logs postgresand confirmPOSTGRES_PASSWORDin.envis set. The server readsConnectionStrings__Postgresfrom compose, which references the same variable. - Out of disk — the gnubg image is large and old images accumulate. The workflow runs
docker image prune -afafter each deploy; you can also run it manually.
To dismantle:
- Lightsail console → Instances → Stop, then Delete instance.
- Lightsail console → Networking → Release the static IP.
- Remove the deploy-bot PAT from GitHub.
- Optionally delete the GHCR packages under Profile → Packages.