feat(deploy): Docker Compose stack for the instance
Caddy, the Serverpod server and PostgreSQL 16 in deploy/. Only Caddy publishes ports; the server and Postgres are reachable only inside the Compose network (Docker bypasses ufw, and the server listens on every interface). Secrets come from deploy/.env as environment variables; there is no production passwords.yaml, and .dockerignore keeps local secrets out of the build context. The server image builds on Serverpod's template with an Ubuntu 24.04 runtime: complete glibc incl. libgcc_s for the SQLite libraries Serverpod 4 bundles, CA certificates, a non-root user, and exec so SIGTERM reaches the server. The scaffold's dev-only docker-compose.yaml is removed. Verified locally: image builds, all native libraries resolve, no passwords.yaml in the image, migrations apply at start, Caddy serves 200, port 8080 unreachable from the host, Postgres in UTC. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
2bb8439293
commit
af3a01907e
3 changed files with 172 additions and 82 deletions
17
deploy/.env.example
Normal file
17
deploy/.env.example
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Copy to deploy/.env, restrict it (chmod 600), and fill in. deploy/.env is git-ignored
|
||||
# and never goes into an image. How it reaches sahaquiel is decided when the server is
|
||||
# set up (e.g. rendered from Proton Pass on the deploying machine and copied over SSH,
|
||||
# so the server itself never holds Proton credentials).
|
||||
|
||||
# `http://localhost` for the local rehearsal; `learn.halkyon.eu` on sahaquiel.
|
||||
DOMAIN=http://localhost
|
||||
|
||||
# Postgres password, shared by the postgres and server services.
|
||||
# Generate: openssl rand -base64 32
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
# Pepper for hashing session keys (server-side sessions). NEVER change it once
|
||||
# sessions exist: everyone would be signed out, and anonymous accounts can't sign
|
||||
# back in. Rotation only via fallbackSessionKeyHashPeppers.
|
||||
# Generate: openssl rand -base64 32
|
||||
SESSION_KEY_HASH_PEPPER=
|
||||
12
deploy/Caddyfile
Normal file
12
deploy/Caddyfile
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Caddy forwards to the API server only. Insights (8081) and a web server (8082) don't
|
||||
# run in production; if Insights is ever re-enabled, reach it through an SSH tunnel,
|
||||
# never through this file.
|
||||
#
|
||||
# {$DOMAIN} is `learn.halkyon.eu` on sahaquiel, where Caddy obtains and renews a
|
||||
# Let's Encrypt certificate by itself, and `http://localhost` for the local rehearsal.
|
||||
{$DOMAIN} {
|
||||
encode zstd gzip
|
||||
|
||||
# Serverpod's streaming methods use WebSockets; reverse_proxy upgrades them as-is.
|
||||
reverse_proxy server:8080
|
||||
}
|
||||
77
deploy/compose.yaml
Normal file
77
deploy/compose.yaml
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# The Halkyon Learn instance: Caddy in front, the Serverpod server, PostgreSQL.
|
||||
#
|
||||
# cd deploy
|
||||
# cp .env.example .env && chmod 600 .env # then fill in the secrets
|
||||
# docker compose up -d --build
|
||||
#
|
||||
# Only Caddy publishes ports. Docker opens published ports directly in the kernel's
|
||||
# packet filter, past ufw, and the Serverpod server listens on every interface, so the
|
||||
# server and Postgres publish nothing: they're reachable only inside this project's
|
||||
# network, where Caddy finds the server by its service name.
|
||||
|
||||
name: halkyon-learn
|
||||
|
||||
# Logs go to the host's journal, whose size and age limits apply (journald.conf).
|
||||
x-logging: &logging
|
||||
driver: journald
|
||||
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp" # HTTP/3
|
||||
environment:
|
||||
# `learn.halkyon.eu` on sahaquiel (automatic Let's Encrypt certificate);
|
||||
# `http://localhost` for the local rehearsal (plain HTTP, no certificate).
|
||||
DOMAIN: ${DOMAIN:?set DOMAIN in deploy/.env}
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data # certificates: must survive restarts (rate limits)
|
||||
- caddy_config:/config
|
||||
depends_on:
|
||||
- server
|
||||
logging: *logging
|
||||
|
||||
server:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: halkyon_learn_server/Dockerfile
|
||||
image: halkyon-learn-server
|
||||
restart: unless-stopped
|
||||
# Applies pending migrations at every start, then serves. One instance only, so
|
||||
# no two servers ever migrate at once.
|
||||
command: ["--apply-migrations"]
|
||||
environment:
|
||||
SERVERPOD_DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in deploy/.env}
|
||||
SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper: ${SESSION_KEY_HASH_PEPPER:?set SESSION_KEY_HASH_PEPPER in deploy/.env}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
logging: *logging
|
||||
|
||||
postgres:
|
||||
image: postgres:16
|
||||
restart: unless-stopped
|
||||
# The server's session is UTC anyway (the driver sets it); this makes the
|
||||
# database's own default UTC too, for psql, COPY loads and future sidecars.
|
||||
command: ["postgres", "-c", "timezone=UTC", "-c", "log_timezone=UTC"]
|
||||
environment:
|
||||
POSTGRES_USER: halkyon_learn
|
||||
POSTGRES_DB: halkyon_learn
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in deploy/.env}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U halkyon_learn -d halkyon_learn"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
logging: *logging
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
postgres_data:
|
||||
Loading…
Reference in a new issue