Host on AWS Lightsail as boards.watersmedia.org
Deploy Waters Boards to AWS / deploy (push) Canceled after 0s

Replace the planned Synology deploy with one Lightsail box: Caddy in front of the
app for automatic HTTPS (deploy/aws/docker-compose.yml, Caddyfile), a deploy
script that rsyncs server/ and rebuilds in place without touching data/ or .env,
and a Gitea workflow that runs the same script from the nas runner over SSH.
README documents the box, DNS, .env, deploys and restore. Allow both church
Google Workspace domains to sign in. Drop the NAS-only compose file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 14:20:25 -04:00
co-authored by Claude Fable 5.1
parent fcf3805626
commit 3aa7fb7b83
7 changed files with 281 additions and 97 deletions
+20
View File
@@ -0,0 +1,20 @@
# Caddy in front of Waters Boards.
#
# Caddy gets and renews the Let's Encrypt certificate by itself. That only works while
# the DNS A record for boards.watersmedia.org points at this box and ports 80 and 443
# are open in the Lightsail firewall.
{
# Let's Encrypt sends certificate expiry notices here.
email michael@waterschurch.org
}
boards.watersmedia.org {
encode zstd gzip
reverse_proxy boards:3100 {
# Required: send every write straight through. Without it Caddy can buffer the
# server-sent events stream at /api/events and live updates stop arriving.
flush_interval -1
}
}
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# Deploy Waters Boards to the AWS Lightsail box.
#
# Usage, from the repo root:
# deploy/aws/deploy.sh
#
# Target host comes from BOARDS_HOST (default: the "boards-aws" SSH alias).
# Never writes or deletes data/ or .env on the box. Uses ssh BatchMode, so it never prompts.
set -euo pipefail
HOST="${BOARDS_HOST:-boards-aws}"
REMOTE_DIR=/opt/waters-boards
SSH_OPTS=(-o BatchMode=yes)
cd "$(dirname "$0")/../.."
step() { printf '\n==> %s\n' "$*"; }
remote() { ssh "${SSH_OPTS[@]}" "$HOST" "$@"; }
step "1/7 Syntax-check the server code"
node --check server/server.js
for f in server/src/*.js; do
node --check "$f"
done
echo "syntax OK"
step "2/7 Check $HOST:$REMOTE_DIR/.env exists"
if ! remote "test -f $REMOTE_DIR/.env"; then
echo "ABORT: $REMOTE_DIR/.env is missing on $HOST (or $HOST is unreachable)." >&2
echo "Create it once by hand from server/.env.example. This script never creates or overwrites it." >&2
echo "See README.md > Deploy to AWS (Lightsail)." >&2
exit 1
fi
echo ".env found"
step "3/7 Copy server/ to $HOST:$REMOTE_DIR/server/ (no data/, .env or node_modules/, no --delete)"
rsync -a -e "ssh ${SSH_OPTS[*]}" \
--exclude 'data/' --exclude '.env' --exclude 'node_modules/' \
server/ "$HOST:$REMOTE_DIR/server/"
step "4/7 Copy docker-compose.yml and Caddyfile to $HOST:$REMOTE_DIR/"
# --inplace: the Caddyfile is bind-mounted as a single file. A normal rsync replaces it with a
# new file, and the running container would keep reading the old one.
rsync -a --inplace -e "ssh ${SSH_OPTS[*]}" \
deploy/aws/docker-compose.yml deploy/aws/Caddyfile "$HOST:$REMOTE_DIR/"
step "5/7 Rebuild and restart in place (no 'down' first: a failed build leaves the old container serving)"
remote "cd $REMOTE_DIR && docker compose up -d --build --remove-orphans"
# Caddy doesn't watch its config. Reload so a changed Caddyfile takes effect without a restart.
remote "cd $REMOTE_DIR && docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile" \
|| echo "WARNING: caddy reload failed; check: docker logs waters-caddy" >&2
step "6/7 Wait for the waters-boards container to report healthy (up to 20 x 3 s)"
healthy=0
for i in $(seq 1 20); do
status=$(remote "docker inspect --format '{{.State.Health.Status}}' waters-boards" 2>/dev/null || echo starting)
echo " try $i/20: $status"
if [ "$status" = "healthy" ]; then
healthy=1
break
fi
sleep 3
done
if [ "$healthy" -ne 1 ]; then
echo "ABORT: waters-boards did not report healthy in time." >&2
remote "docker ps" || true
remote "docker logs --tail 30 waters-boards" || true
exit 1
fi
echo "container healthy"
step "7/7 Check the public URL (informational only; DNS or the certificate may not be ready yet)"
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 https://boards.watersmedia.org/healthz || true)
echo "https://boards.watersmedia.org/healthz -> HTTP ${code:-000}"
echo
echo "Deploy finished."
+45
View File
@@ -0,0 +1,45 @@
# Waters Boards on the AWS Lightsail box.
#
# This file is copied to /opt/waters-boards/ on the box (deploy/aws/deploy.sh does it), and
# sits there next to:
# .env secrets, created by hand once, never overwritten by a deploy
# data/ database, uploads and nightly backups, never touched by a deploy
# Caddyfile the reverse-proxy config from this folder
# server/ the app source, rsynced from ../../server
# "build: ./server" and "./data" below are relative to that folder, not to this repo.
services:
boards:
build: ./server
image: waters-boards:latest
container_name: waters-boards
restart: unless-stopped
env_file:
- ./.env
volumes:
# Database (boards.db), uploaded files and nightly backups.
- ./data:/app/data
# Reachable only from Caddy on the compose network. No "ports": the app is never public.
expose:
- "3100"
caddy:
image: caddy:2.10-alpine
container_name: waters-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
# Certificates and Caddy's state. Keep these volumes, or Caddy has to re-issue the
# certificate on every restart and Let's Encrypt will rate-limit it.
- caddy_data:/data
- caddy_config:/config
depends_on:
- boards
volumes:
caddy_data:
caddy_config: