Host on AWS Lightsail as boards.watersmedia.org
Deploy Waters Boards to AWS / deploy (push) Canceled after 0s
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:
+38
-53
@@ -1,23 +1,35 @@
|
|||||||
# Push to main -> the NAS rebuilds Waters Boards.
|
# Push to main -> Waters Boards is rebuilt on the AWS Lightsail box.
|
||||||
#
|
#
|
||||||
# Same shape as the checklist app's deploy: runs on the "nas" act_runner (in Docker on the
|
# Runs on the "nas" act_runner, which lives inside the church network and can reach the
|
||||||
# NAS itself), copies server/ over the live directory, then recreates the container in place.
|
# internet. (The Gitea server isn't reachable from outside, so AWS can't pull from it; the
|
||||||
|
# runner pushes to AWS instead.) It syntax-checks the code, rsyncs server/ and deploy/aws/ to
|
||||||
|
# /opt/waters-boards on the box over SSH, rebuilds in place and waits for the healthcheck.
|
||||||
|
# All of that is deploy/aws/deploy.sh, the same script you run from a laptop.
|
||||||
#
|
#
|
||||||
# ONE-TIME NAS SETUP before the first run (see README.md › Deploy):
|
# ONE-TIME SETUP before the first run (see README.md › Deploy to AWS):
|
||||||
# 1. mkdir /volume2/docker/waters-boards and put a filled-in .env there
|
# 1. Make a deploy-only SSH key (e.g. ssh-keygen -t ed25519 -N '' -f deploy_key) and add the
|
||||||
# 2. add "/volume2/docker/waters-boards" to container.valid_volumes in the act_runner
|
# public half to /home/ubuntu/.ssh/authorized_keys on the box.
|
||||||
# config.yaml on the NAS, and restart the runner
|
# 2. Add repository secrets (Settings ▸ Actions ▸ Secrets):
|
||||||
# 3. point a reverse-proxy hostname (https) at port 3100 — Google sign-in needs https
|
# DEPLOY_SSH_KEY the private key from step 1
|
||||||
# 4. set the repository variable DEPLOY_ENABLED=true (Settings ▸ Actions ▸ Variables).
|
# DEPLOY_KNOWN_HOSTS the output of: ssh-keyscan -t ed25519 <static IP>
|
||||||
# Until then this job is skipped, so pushes don't fail before the NAS is ready.
|
# 3. Add repository variables (Settings ▸ Actions ▸ Variables):
|
||||||
|
# DEPLOY_HOST the box's static IP
|
||||||
|
# DEPLOY_ENABLED true
|
||||||
|
# Until DEPLOY_ENABLED is true this job is skipped, so pushes made before the box is
|
||||||
|
# ready don't show as failures.
|
||||||
|
# 4. The "nas" runner must be registered where it can see this repo (user- or instance-level,
|
||||||
|
# not only on the checklist repo).
|
||||||
|
# /opt/waters-boards/.env must already exist on the box. The deploy never creates or
|
||||||
|
# overwrites it, and never touches data/.
|
||||||
|
|
||||||
name: Deploy Waters Boards to NAS
|
name: Deploy Waters Boards to AWS
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- 'server/**'
|
- 'server/**'
|
||||||
|
- 'deploy/aws/**'
|
||||||
- '.gitea/workflows/deploy.yml'
|
- '.gitea/workflows/deploy.yml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -27,55 +39,28 @@ jobs:
|
|||||||
runs-on: nas
|
runs-on: nas
|
||||||
container:
|
container:
|
||||||
image: node:22-alpine
|
image: node:22-alpine
|
||||||
volumes:
|
|
||||||
# Mounted at the SAME path as on the host: docker-compose.yml binds ./data relative to
|
|
||||||
# the compose file and the daemon resolves that as a host path. (The docker socket is
|
|
||||||
# already mounted by act_runner — declaring it again breaks the job.)
|
|
||||||
- /volume2/docker/waters-boards:/volume2/docker/waters-boards
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install tools
|
- name: Install tools
|
||||||
run: apk add --no-cache docker-cli docker-cli-compose rsync
|
# deploy.sh is a bash script and needs node (already in the image) for its syntax check.
|
||||||
|
run: apk add --no-cache bash openssh-client rsync
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Check the deploy directory is set up
|
- name: Set up the SSH key
|
||||||
|
# Secrets go in through env, not pasted into the script text.
|
||||||
|
env:
|
||||||
|
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
DEST=/volume2/docker/waters-boards
|
mkdir -p ~/.ssh
|
||||||
test -f "$DEST/.env" || { echo "REFUSING: $DEST/.env is missing — see README.md › Deploy"; exit 1; }
|
chmod 700 ~/.ssh
|
||||||
|
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||||
|
chmod 644 ~/.ssh/known_hosts
|
||||||
|
|
||||||
- name: Copy server files over the live directory
|
- name: Deploy
|
||||||
run: |
|
run: BOARDS_HOST="ubuntu@${{ vars.DEPLOY_HOST }}" bash deploy/aws/deploy.sh
|
||||||
set -eu
|
|
||||||
# data/ holds the live database, uploads and backups; .env holds the secrets.
|
|
||||||
# Neither is in git and neither may ever be written here. No --delete.
|
|
||||||
rsync -a \
|
|
||||||
--exclude 'data/' \
|
|
||||||
--exclude '.env' \
|
|
||||||
--exclude 'node_modules/' \
|
|
||||||
server/ /volume2/docker/waters-boards/
|
|
||||||
|
|
||||||
- name: Verify the live data and credentials survived
|
|
||||||
run: |
|
|
||||||
set -eu
|
|
||||||
test -f /volume2/docker/waters-boards/.env || { echo "ABORT: .env is gone"; exit 1; }
|
|
||||||
echo ".env intact"; ls -la /volume2/docker/waters-boards/data 2>/dev/null || echo "(no data/ yet — first deploy)"
|
|
||||||
|
|
||||||
- name: Rebuild the container in place
|
|
||||||
# No 'down' first: an interrupted deploy then leaves the old container serving.
|
|
||||||
run: docker compose -f /volume2/docker/waters-boards/docker-compose.yml up -d --build --remove-orphans
|
|
||||||
|
|
||||||
- name: Healthcheck
|
|
||||||
run: |
|
|
||||||
set -eu
|
|
||||||
for i in $(seq 1 20); do
|
|
||||||
S=$(docker inspect --format '{{.State.Health.Status}}' waters-boards 2>/dev/null || echo starting)
|
|
||||||
[ "$S" = "healthy" ] && { echo "container healthy"; exit 0; }
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
echo "container did not report healthy in time:"
|
|
||||||
docker ps --filter name=waters-boards --format '{{.Names}} {{.Status}}'
|
|
||||||
docker logs --tail 30 waters-boards
|
|
||||||
exit 1
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
A self-hosted work-management app for the Waters Church team: boards, groups, items, statuses,
|
A self-hosted work-management app for the Waters Church team: boards, groups, items, statuses,
|
||||||
people, dates, updates and automations. It covers the parts of monday.com the team uses and
|
people, dates, updates and automations. It covers the parts of monday.com the team uses and
|
||||||
runs on the NAS beside the checklist app, so there's no monthly subscription.
|
runs on a small AWS Lightsail server for a few dollars a month, covered by the church's AWS
|
||||||
|
credits for now.
|
||||||
|
|
||||||
It's one Node server (Express + SQLite) and a front end with no build step. It runs on desktop
|
It's one Node server (Express + SQLite) and a front end with no build step. It runs on desktop
|
||||||
browsers and on phones, and a phone can install it to the home screen like an app.
|
browsers and on phones, and a phone can install it to the home screen like an app.
|
||||||
@@ -48,14 +49,13 @@ Data goes to `server/data/` (`boards.db`, `uploads/`, `backups/`). The folder is
|
|||||||
|
|
||||||
## Set up Google sign-in (once)
|
## Set up Google sign-in (once)
|
||||||
|
|
||||||
Google only allows sign-in from **https** addresses (or `localhost`), so the app needs a
|
Google only allows sign-in from **https** addresses (or `localhost`), so the app lives at
|
||||||
hostname behind the NAS reverse proxy, for example `https://boards.watersmedia.org`, set up the
|
**`https://boards.watersmedia.org`**, behind Caddy on the AWS box (see *Deploy to AWS* below).
|
||||||
same way as `check.watersmedia.org`.
|
|
||||||
|
|
||||||
1. Go to <https://console.cloud.google.com/> and sign in with the church Google Workspace admin account.
|
1. Go to <https://console.cloud.google.com/> and sign in with the church Google Workspace admin account.
|
||||||
2. Create a project (e.g. "Waters Boards").
|
2. Create a project (e.g. "Waters Boards").
|
||||||
3. **APIs & Services → OAuth consent screen**: choose **Internal**, so only
|
3. **APIs & Services → OAuth consent screen**: choose **Internal**, so only accounts in the
|
||||||
`@waterschurch.org` accounts can ever use it. Fill in the app name and support email, then save.
|
church's Google Workspace can ever use it. Fill in the app name and support email, then save.
|
||||||
4. **APIs & Services → Credentials → Create credentials → OAuth client ID**
|
4. **APIs & Services → Credentials → Create credentials → OAuth client ID**
|
||||||
- Application type: **Web application**
|
- Application type: **Web application**
|
||||||
- Authorized JavaScript origins: `https://boards.watersmedia.org` (and `http://localhost:3100` for local testing)
|
- Authorized JavaScript origins: `https://boards.watersmedia.org` (and `http://localhost:3100` for local testing)
|
||||||
@@ -70,32 +70,95 @@ Google sign-in is ever misconfigured.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deploy to the NAS
|
## Deploy to AWS (Lightsail)
|
||||||
|
|
||||||
This follows the same pattern as the checklist app: Docker on the NAS, and Gitea Actions
|
Waters Boards runs on one AWS Lightsail server. Docker runs two containers there: the app, and
|
||||||
redeploys on every push to `main` that touches `server/`.
|
Caddy in front of it. Caddy gets and renews the Let's Encrypt certificate itself, so TLS ends at
|
||||||
|
Caddy and the app speaks plain HTTP on the private Docker network. Only ports 80 and 443 are
|
||||||
|
public.
|
||||||
|
|
||||||
**One-time setup on the NAS** (`10.1.1.5`):
|
**The box:** Ubuntu 24.04, 2 GB RAM, `us-east-1`, a static IPv4 address, Docker Engine with the
|
||||||
|
compose plugin. Lightsail snapshots the disk every day at 07:00 UTC.
|
||||||
|
|
||||||
```bash
|
**Folder on the box** (owned by `ubuntu`):
|
||||||
mkdir -p /volume2/docker/waters-boards
|
|
||||||
cp server/.env.example /volume2/docker/waters-boards/.env # then fill it in
|
```
|
||||||
|
/opt/waters-boards/
|
||||||
|
.env settings and secrets. Made by hand once, never in git
|
||||||
|
data/ database, uploads, nightly backups
|
||||||
|
Caddyfile copied from deploy/aws/
|
||||||
|
docker-compose.yml copied from deploy/aws/
|
||||||
|
server/ the app source, copied from this repo's server/
|
||||||
```
|
```
|
||||||
|
|
||||||
1. In the act_runner `config.yaml` on the NAS, add `/volume2/docker/waters-boards` to
|
**One-time setup**
|
||||||
`container.valid_volumes`, then restart the runner. The runner refuses host mounts that
|
|
||||||
aren't listed there.
|
|
||||||
2. Add a reverse-proxy rule: `https://boards.watersmedia.org` → `http://localhost:3100`.
|
|
||||||
Allow WebSocket/streaming headers, because the live updates use a long-lived HTTP response.
|
|
||||||
3. In the Gitea repo, set **Settings → Actions → Variables → `DEPLOY_ENABLED` = `true`**. Until it's
|
|
||||||
set, the deploy job is skipped, so pushes made before the NAS was ready don't show as failures.
|
|
||||||
4. Push to `main`, or run the workflow by hand from the Gitea Actions tab.
|
|
||||||
|
|
||||||
The workflow (`.gitea/workflows/deploy.yml`) never writes `data/` or `.env`. It rebuilds the
|
1. **DNS.** `watersmedia.org` is served by the church's own nameservers (`ns1` and
|
||||||
container in place without taking it down first, and fails loudly if `.env` is missing.
|
`ns2.waterschurch.org`), so there's no registrar panel to use. The zone is *not* in the
|
||||||
|
`watersch` cPanel account; it belongs to whichever cPanel/WHM account owns `watersmedia.org`
|
||||||
|
(its SOA contact is `nick@waterschurch.org`). In that account's **Zone Editor**, add an `A`
|
||||||
|
record `boards` → the box's static IP, TTL 300. Caddy can't get a certificate until that
|
||||||
|
record resolves, and ports 80 and 443 must be open in the Lightsail firewall.
|
||||||
|
2. **`.env`.** Copy `server/.env.example` to `/opt/waters-boards/.env` and fill it in. The deploy
|
||||||
|
script refuses to run without it, so do this from your laptop before the first deploy:
|
||||||
|
|
||||||
**Manual deploy**, without Gitea: copy `server/` (minus `data/`, `.env` and `node_modules/`) to the
|
```bash
|
||||||
NAS folder, then run `docker compose up -d --build` there.
|
scp server/.env.example boards-aws:/opt/waters-boards/.env
|
||||||
|
ssh boards-aws nano /opt/waters-boards/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `ADMIN_EMAILS` and `ADMIN_PASSWORD`. `ADMIN_PASSWORD` is the break-glass login until
|
||||||
|
Google sign-in is configured. Add `GOOGLE_CLIENT_ID` once you have it (see above).
|
||||||
|
3. **First deploy**, as below. When it finishes, <https://boards.watersmedia.org> should load.
|
||||||
|
|
||||||
|
**Deploy from a laptop.** Needs the `boards-aws` SSH alias (user `ubuntu`) in `~/.ssh/config`.
|
||||||
|
From the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deploy/aws/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
It checks the server code for syntax errors, confirms `.env` exists on the box, copies `server/`,
|
||||||
|
`docker-compose.yml` and `Caddyfile` over, rebuilds the containers in place, waits for the
|
||||||
|
healthcheck, and prints what `https://boards.watersmedia.org/healthz` returns. Set `BOARDS_HOST`
|
||||||
|
to deploy to a host other than `boards-aws`.
|
||||||
|
|
||||||
|
**Deploy from Gitea Actions.** The Gitea server is only reachable from inside the church network,
|
||||||
|
so AWS can't pull from it. Instead the `nas` runner, which is inside the network and can reach
|
||||||
|
the internet, runs the same `deploy.sh` and pushes to the box over SSH. The workflow
|
||||||
|
(`.gitea/workflows/deploy.yml`) runs on every push to `main` that touches `server/` or
|
||||||
|
`deploy/aws/`, or by hand from the Gitea Actions tab. One-time setup:
|
||||||
|
|
||||||
|
1. Make a deploy-only SSH key. Add its public half to `/home/ubuntu/.ssh/authorized_keys` on
|
||||||
|
the box.
|
||||||
|
2. In the Gitea repo, **Settings → Actions → Secrets**, add:
|
||||||
|
- `DEPLOY_SSH_KEY`: the private key.
|
||||||
|
- `DEPLOY_KNOWN_HOSTS`: the output of `ssh-keyscan -t ed25519 <static IP>`.
|
||||||
|
3. In **Settings → Actions → Variables**, add `DEPLOY_HOST` (the static IP) and
|
||||||
|
`DEPLOY_ENABLED` = `true`. Until `DEPLOY_ENABLED` is `true`, the deploy job is skipped, so
|
||||||
|
pushes made before the box was ready don't show as failures.
|
||||||
|
4. The `nas` runner must be registered where it can see this repo (user- or instance-level, not
|
||||||
|
only on the checklist repo).
|
||||||
|
|
||||||
|
**What a deploy never touches:** `data/` and `.env`. It deletes nothing on the box. It rebuilds
|
||||||
|
without taking the running container down first, so a failed build leaves the old version
|
||||||
|
serving.
|
||||||
|
|
||||||
|
**Restore**
|
||||||
|
|
||||||
|
- *From a Lightsail snapshot* (the whole box): in the Lightsail console, create a new instance
|
||||||
|
from the latest snapshot, then move the static IP to it. The folder, `.env`, data and SSH host
|
||||||
|
key all come back as they were, so nothing else changes.
|
||||||
|
- *From `data/backups/`* (the database only): `data/` is owned by root, hence `sudo`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh boards-aws
|
||||||
|
cd /opt/waters-boards
|
||||||
|
docker compose stop boards
|
||||||
|
sudo rm -f data/boards.db-wal data/boards.db-shm
|
||||||
|
sudo cp data/backups/boards-YYYY-MM-DD.db data/boards.db
|
||||||
|
docker compose start boards
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -128,9 +191,11 @@ Inbox, Search). Pickers open as bottom sheets.
|
|||||||
## Backups
|
## Backups
|
||||||
|
|
||||||
- Every night after 2am the app writes `data/backups/boards-YYYY-MM-DD.db` and keeps 14.
|
- Every night after 2am the app writes `data/backups/boards-YYYY-MM-DD.db` and keeps 14.
|
||||||
- For off-site copies, back up the whole `data/` folder (database + `uploads/`) the same way
|
- Lightsail takes an automatic snapshot of the whole disk every day and keeps 7. That covers
|
||||||
the checklist data is backed up.
|
`data/` (database + `uploads/`), `.env` and everything else on the box.
|
||||||
- To restore, stop the container, copy a snapshot over `data/boards.db`, and start it again.
|
- To restore a database snapshot, follow *Restore* under *Deploy to AWS* above. Don't just copy
|
||||||
|
the file over `boards.db`: the database runs in WAL mode, and a stale `boards.db-wal` beside a
|
||||||
|
restored file corrupts it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -155,4 +220,8 @@ server/
|
|||||||
js/item.js Item panel: updates, info, files, activity
|
js/item.js Item panel: updates, info, files, activity
|
||||||
js/kanban.js · calendar.js · timeline.js · chart.js
|
js/kanban.js · calendar.js · timeline.js · chart.js
|
||||||
js/pages.js Home, My work, Inbox, Search, Team, Admin
|
js/pages.js Home, My work, Inbox, Search, Team, Admin
|
||||||
|
deploy/aws/
|
||||||
|
docker-compose.yml The app and Caddy containers, as run on the Lightsail box
|
||||||
|
Caddyfile Caddy config: HTTPS for boards.watersmedia.org, proxy to the app
|
||||||
|
deploy.sh Deploys from a laptop or Gitea: check, copy, rebuild, wait for healthy
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+78
@@ -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."
|
||||||
@@ -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:
|
||||||
+3
-2
@@ -6,8 +6,9 @@ PORT=3100
|
|||||||
# See README.md › "Set up Google sign-in". Leave empty to disable Google sign-in.
|
# See README.md › "Set up Google sign-in". Leave empty to disable Google sign-in.
|
||||||
GOOGLE_CLIENT_ID=
|
GOOGLE_CLIENT_ID=
|
||||||
|
|
||||||
# Only Google accounts on these domains can sign in (comma-separated).
|
# Only Google accounts on these domains can sign in (comma-separated). Both church
|
||||||
ALLOWED_DOMAINS=waterschurch.org
|
# domains are Google Workspace domains, so both are allowed by default.
|
||||||
|
ALLOWED_DOMAINS=waterschurch.org,waters.church
|
||||||
|
|
||||||
# These people are always admins (comma-separated). The very first person to sign in
|
# These people are always admins (comma-separated). The very first person to sign in
|
||||||
# also becomes an admin.
|
# also becomes an admin.
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
services:
|
|
||||||
boards:
|
|
||||||
build: .
|
|
||||||
image: waters-boards:latest
|
|
||||||
container_name: waters-boards
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
# Host port : container port. 3000 is the checklist app, so this uses 3100.
|
|
||||||
- "3100:3100"
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
volumes:
|
|
||||||
# Database (boards.db), uploaded files and nightly backups.
|
|
||||||
- ./data:/app/data
|
|
||||||
Reference in New Issue
Block a user