Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,17 @@ PROBUS_DATA=./data
PROBUS_MEM_LIMIT=2g

# ---------------------------------------------------------------------------
# TLS (only used by the `tls` profile)
# Facing the internet: pick one
# ---------------------------------------------------------------------------

# A reverse proxy already on the server (Nginx Proxy Manager, Traefik, Caddy)
# that reaches services over a shared Docker network: name that network here
# and point the proxy at `busfinder:3000`. Leave empty to run our own Caddy,
# which needs the two TLS settings below instead.
PROBUS_PROXY_NETWORK=

# TLS with our own Caddy (the `tls` profile), used when the line above is empty

# Domain whose A/AAAA record already points at this server. Caddy obtains and
# renews the certificate itself; nothing else has to be configured.
PROBUS_DOMAIN=busfinder.example.com
Expand Down
20 changes: 15 additions & 5 deletions DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ source: it pulls a ready-made image, the same one you tried here.
|---|---|
| `docker-compose.yml` | the stack. Same file here and on the server; only `.env` differs |
| `docker-compose.build.yml` | overlay for building locally instead of pulling |
| `docker-compose.proxy.yml` | overlay for a server with its own reverse proxy, see step 4 |
| `Caddyfile` | HTTPS with an automatic certificate. Only needed on the server |
| `.env` | this machine |
| `.env.server` | the server. `deploy.sh` copies it over as `.env` |
Expand Down Expand Up @@ -54,9 +55,18 @@ point at the server, and ports 80 and 443 must be reachable, before the first
deploy: Caddy fetches the certificate at startup and Let's Encrypt allows five
attempts per week per domain.

**4. `.env.server`.** Fill in `PROBUS_IMAGE`, `PROBUS_DOMAIN`,
`PROBUS_ACME_EMAIL` and `PROBUS_CONTACT`. `deploy.sh` refuses to start if the
`CHANGEME` placeholders are still there.
**4. `.env.server`.** Fill in `PROBUS_IMAGE` and `PROBUS_CONTACT`, then pick
how the app faces the internet:

- *Our own Caddy*: fill in `PROBUS_DOMAIN` and `PROBUS_ACME_EMAIL`. Caddy takes
ports 80 and 443 and fetches the certificate itself.
- *A reverse proxy already on the server* (Nginx Proxy Manager, Traefik,
another Caddy): set `PROBUS_PROXY_NETWORK` to the Docker network the proxy
uses to reach its services. The app joins it and the proxy is pointed at
`busfinder` on port `3000`; the certificate is the proxy's business, and our
Caddy is not started at all.

`deploy.sh` refuses to start if a required value is still a `CHANGEME`.

**5. The server.** All it needs is Docker and the compose plugin. `deploy.sh`
creates `PROBUS_REMOTE_DIR` itself and copies the configuration and `.env` into
Expand All @@ -71,11 +81,11 @@ prompt for a password.

1. refuses to start if `.env.server` still contains a `CHANGEME`;
2. notes which image is running right now, so it can go back to it;
3. copies `docker-compose.yml`, `Caddyfile` and `.env.server` to the server —
3. copies the compose files, `Caddyfile` and `.env.server` to the server —
configuration only, never source;
4. pulls the new image and restarts;
5. waits up to five minutes for the container to become `healthy`;
6. runs `scripts/smoke.ts` against the public URL: eighteen checks across every
6. runs `scripts/smoke.ts` against the public URL: 47 checks across every
route, including a full write-and-delete round trip through sync;
7. if the container never becomes healthy or the smoke test fails, it **puts
the previous image back** and exits non-zero.
Expand Down
17 changes: 17 additions & 0 deletions docker-compose.proxy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Overlay for a server where a reverse proxy (Nginx Proxy Manager, Traefik,
# another Caddy) already owns ports 80 and 443 and reaches services over a
# shared Docker network. The app joins that network and the proxy talks to it
# as `busfinder:3000`; the `tls` profile stays off.
#
# docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d

services:
app:
networks:
- default
- proxy

networks:
proxy:
external: true
name: ${PROBUS_PROXY_NETWORK:?set PROBUS_PROXY_NETWORK to the proxy's Docker network}
25 changes: 21 additions & 4 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ set +a
: "${PROBUS_URL:?PROBUS_URL is not set in .env}"
REMOTE_DIR="${PROBUS_REMOTE_DIR:-/opt/busfinder}"

# Two ways to face the internet: our own Caddy (tls profile), or a reverse
# proxy already on the host that reaches the app over a shared Docker network.
if [ -n "${PROBUS_PROXY_NETWORK:-}" ]; then
COMPOSE=(docker compose -f docker-compose.yml -f docker-compose.proxy.yml)
MODE="proxy network ${PROBUS_PROXY_NETWORK}"
else
COMPOSE=(docker compose --profile tls)
MODE="caddy on ${PROBUS_DOMAIN:-?}"
fi

TARGET="${PROBUS_IMAGE}:${PROBUS_TAG}"
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=10)

Expand All @@ -44,7 +54,8 @@ remote() { ssh "${SSH_OPTS[@]}" "$PROBUS_SSH" "$@"; }

# Caddy needs these and compose cannot demand them without breaking the
# app-only stack, so this is where a missing domain has to be caught.
for key in PROBUS_DOMAIN PROBUS_ACME_EMAIL PROBUS_IMAGE; do
if [ -n "${PROBUS_PROXY_NETWORK:-}" ]; then REQUIRED="PROBUS_IMAGE"; else REQUIRED="PROBUS_DOMAIN PROBUS_ACME_EMAIL PROBUS_IMAGE"; fi
for key in $REQUIRED; do
value="$(grep -E "^${key}=" .env.server | head -1 | cut -d= -f2-)"
if [ -z "$value" ] || case "$value" in *CHANGEME*) true ;; *) false ;; esac; then
echo "deploy: ${key} is not filled in in .env.server (value: '${value}')." >&2
Expand All @@ -54,6 +65,7 @@ done

say "server ${PROBUS_SSH}:${REMOTE_DIR}"
say "image ${TARGET}"
say "front ${MODE}"
say "smoke ${PROBUS_URL}"
echo

Expand All @@ -62,6 +74,11 @@ if ! remote true; then
exit 1
fi

if [ -n "${PROBUS_PROXY_NETWORK:-}" ] && ! remote "docker network inspect '${PROBUS_PROXY_NETWORK}' >/dev/null 2>&1"; then
echo "deploy: Docker network '${PROBUS_PROXY_NETWORK}' does not exist on ${PROBUS_SSH}." >&2
exit 1
fi

# What is running now, so a failed rollout can be undone. Empty on first deploy.
PREVIOUS="$(remote "docker inspect -f '{{.Config.Image}}' busfinder 2>/dev/null || true" | tr -d '\r')"
if [ -n "$PREVIOUS" ]; then
Expand All @@ -72,15 +89,15 @@ fi

say "sending the configuration"
remote "mkdir -p '${REMOTE_DIR}'"
scp "${SSH_OPTS[@]}" -q docker-compose.yml Caddyfile "${PROBUS_SSH}:${REMOTE_DIR}/"
scp "${SSH_OPTS[@]}" -q docker-compose.yml docker-compose.proxy.yml Caddyfile "${PROBUS_SSH}:${REMOTE_DIR}/"
scp "${SSH_OPTS[@]}" -q .env.server "${PROBUS_SSH}:${REMOTE_DIR}/.env"
# The tag lives in the local .env; the server's copy must agree with it.
remote "cd '${REMOTE_DIR}' && (grep -q '^PROBUS_TAG=' .env \
&& sed -i 's|^PROBUS_TAG=.*|PROBUS_TAG=${PROBUS_TAG}|' .env \
|| echo 'PROBUS_TAG=${PROBUS_TAG}' >> .env)"

bring_up() {
remote "cd '${REMOTE_DIR}' && docker compose --profile tls pull --quiet && docker compose --profile tls up -d"
remote "cd '${REMOTE_DIR}' && ${COMPOSE[*]} pull --quiet && ${COMPOSE[*]} up -d"
}

say "pulling and restarting"
Expand All @@ -102,7 +119,7 @@ rollback() {
fi
echo "deploy: rolling back to ${PREVIOUS}" >&2
old_tag="${PREVIOUS##*:}"
remote "cd '${REMOTE_DIR}' && sed -i 's|^PROBUS_TAG=.*|PROBUS_TAG=${old_tag}|' .env && docker compose --profile tls up -d" || true
remote "cd '${REMOTE_DIR}' && sed -i 's|^PROBUS_TAG=.*|PROBUS_TAG=${old_tag}|' .env && ${COMPOSE[*]} up -d" || true
}

if [ -z "$healthy" ]; then
Expand Down