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
8 changed files with 172 additions and 82 deletions
20
.dockerignore
Normal file
20
.dockerignore
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# The Docker build context is the repository root. Nothing secret, local or bulky
|
||||||
|
# goes into it: the server's Dockerfile copies the whole halkyon_learn_server folder.
|
||||||
|
|
||||||
|
# Secrets and local state
|
||||||
|
halkyon_learn_server/config/passwords.yaml
|
||||||
|
halkyon_learn_server/.serverpod/
|
||||||
|
deploy/.env
|
||||||
|
**/*.log
|
||||||
|
|
||||||
|
# Tooling and build output
|
||||||
|
.git/
|
||||||
|
**/.dart_tool/
|
||||||
|
**/build/
|
||||||
|
.vscode/
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Not needed to build the server
|
||||||
|
halkyon_learn_flutter/
|
||||||
|
docs/
|
||||||
|
NEXT.md
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -17,3 +17,5 @@ pubspec_overrides.yaml
|
||||||
|
|
||||||
# Serverpod Cloud CLI
|
# Serverpod Cloud CLI
|
||||||
.scloud/
|
.scloud/
|
||||||
|
# Deployment secrets (see deploy/.env.example)
|
||||||
|
deploy/.env
|
||||||
|
|
|
||||||
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:
|
||||||
|
|
@ -1,58 +1,64 @@
|
||||||
# Build from the project root with:
|
# The server image. Built from the repository root (the build context), because the
|
||||||
|
# workspace lockfile lives there:
|
||||||
|
#
|
||||||
# docker build -f halkyon_learn_server/Dockerfile .
|
# docker build -f halkyon_learn_server/Dockerfile .
|
||||||
|
#
|
||||||
|
# Based on Serverpod's template, with three changes: Dart 3.13 (our SDK floor), an
|
||||||
|
# Ubuntu 24.04 runtime instead of alpine + Dart's minimal glibc runtime (same
|
||||||
|
# distribution as our servers; complete glibc, which Serverpod 4's bundled SQLite
|
||||||
|
# libraries need), and a start command that passes Docker's stop signal on.
|
||||||
|
|
||||||
# Build stage
|
# ── Build stage: compile the server to a native executable ────────────────────────
|
||||||
FROM dart:3.12.2 AS build
|
FROM dart:3.13.4 AS build
|
||||||
|
|
||||||
# Create a minimal workspace that preserves locked server dependency versions.
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY pubspec.lock .
|
COPY pubspec.lock .
|
||||||
COPY halkyon_learn_server halkyon_learn_server
|
COPY halkyon_learn_server halkyon_learn_server
|
||||||
|
|
||||||
|
# A workspace root containing only the server package: the client package and the
|
||||||
|
# Flutter app aren't needed to build the server.
|
||||||
RUN printf '%s\n' \
|
RUN printf '%s\n' \
|
||||||
'name: _' \
|
'name: _' \
|
||||||
'environment:' \
|
'environment:' \
|
||||||
' sdk: ^3.12.2' \
|
' sdk: ^3.13.0' \
|
||||||
'workspace:' \
|
'workspace:' \
|
||||||
' - halkyon_learn_server' \
|
' - halkyon_learn_server' \
|
||||||
> pubspec.yaml
|
> pubspec.yaml
|
||||||
RUN dart pub get
|
RUN dart pub get
|
||||||
|
|
||||||
# Compile the server executable.
|
|
||||||
WORKDIR /app/halkyon_learn_server
|
WORKDIR /app/halkyon_learn_server
|
||||||
RUN dart build cli --target bin/main.dart --output build
|
RUN dart build cli --target bin/main.dart --output build
|
||||||
RUN mv build/bundle/bin/main build/bundle/bin/server
|
RUN mv build/bundle/bin/main build/bundle/bin/server
|
||||||
|
RUN mkdir -p config migrations
|
||||||
|
|
||||||
# Add a fallback for the copy of possibly missing directories.
|
# ── Runtime stage ─────────────────────────────────────────────────────────────────
|
||||||
RUN mkdir -p config web migrations
|
# Ubuntu 24.04: glibc 2.39 (the bundled binaries need at most 2.34, measured) and
|
||||||
|
# libgcc_s (needed by libsqlite3_connection_pool, a Rust library; a server running
|
||||||
# Final stage
|
# against Postgres never loads it, but a future code path might). CA certificates
|
||||||
FROM alpine:latest
|
# for outgoing TLS, e.g. GitHub sign-in.
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
ENV runmode=production
|
ENV runmode=production
|
||||||
ENV serverid=default
|
ENV serverid=default
|
||||||
ENV logging=normal
|
ENV logging=normal
|
||||||
ENV role=monolith
|
ENV role=monolith
|
||||||
|
|
||||||
# Copy runtime dependencies
|
|
||||||
COPY --from=build /runtime/ /
|
|
||||||
|
|
||||||
# Copy compiled server executable and native libraries
|
|
||||||
COPY --from=build /app/halkyon_learn_server/build/bundle/ ./
|
COPY --from=build /app/halkyon_learn_server/build/bundle/ ./
|
||||||
|
|
||||||
# Copy configuration files and resources
|
|
||||||
COPY --from=build /app/halkyon_learn_server/config/ config/
|
COPY --from=build /app/halkyon_learn_server/config/ config/
|
||||||
COPY --from=build /app/halkyon_learn_server/web/ web/
|
|
||||||
COPY --from=build /app/halkyon_learn_server/migrations/ migrations/
|
COPY --from=build /app/halkyon_learn_server/migrations/ migrations/
|
||||||
|
|
||||||
# This file is required to enable the endpoint log filter in Insights.
|
|
||||||
COPY --from=build /app/halkyon_learn_server/lib/src/generated/protocol.yaml lib/src/generated/protocol.yaml
|
COPY --from=build /app/halkyon_learn_server/lib/src/generated/protocol.yaml lib/src/generated/protocol.yaml
|
||||||
|
|
||||||
# Expose ports
|
# The server needs no root rights (it listens on 8080 and writes nothing to the
|
||||||
EXPOSE 8080
|
# image), so it runs as an unprivileged system user.
|
||||||
EXPOSE 8081
|
RUN useradd --system --no-create-home --shell /usr/sbin/nologin app
|
||||||
EXPOSE 8082
|
USER app
|
||||||
|
|
||||||
# Define the entrypoint command
|
# Only the API server runs in production (no Insights, no web server).
|
||||||
ENTRYPOINT ./bin/server --mode=$runmode --server-id=$serverid --logging=$logging --role=$role
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# `exec` makes the server PID 1, so it receives Docker's SIGTERM and shuts down
|
||||||
|
# cleanly; extra arguments (e.g. --apply-migrations) come from the container command.
|
||||||
|
ENTRYPOINT ["/bin/sh", "-c", "exec ./bin/server --mode=$runmode --server-id=$serverid --logging=$logging --role=$role \"$@\"", "--"]
|
||||||
|
CMD []
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,16 @@ apiServer:
|
||||||
#
|
#
|
||||||
# No webServer: there is no web app.
|
# No webServer: there is no web app.
|
||||||
|
|
||||||
# This is the database setup for your servers. The default for the Google Cloud
|
# The database: the `postgres` service of deploy/compose.yaml, reachable only inside
|
||||||
# Engine Terraform configuration is to connect on a private IP address.
|
# the Compose network (it publishes no port), so no TLS between server and database.
|
||||||
# If you are connecting on a public IP (e.g. on AWS or Google Cloud Run), you
|
# The password comes from the environment (SERVERPOD_DATABASE_PASSWORD, set in
|
||||||
# connect on the public IP of the database e.g. database.examplepod.com.
|
# deploy/.env); there is no production passwords.yaml.
|
||||||
database:
|
database:
|
||||||
host: database.private-production.examplepod.com
|
host: postgres
|
||||||
port: 5432
|
port: 5432
|
||||||
name: serverpod
|
name: halkyon_learn
|
||||||
user: postgres
|
user: halkyon_learn
|
||||||
requireSsl: true
|
requireSsl: false
|
||||||
#isUnixSocket: true # defaults to false
|
#isUnixSocket: true # defaults to false
|
||||||
#maxConnectionCount: 10 # Defaults to 10, a negative or null value removes the limit
|
#maxConnectionCount: 10 # Defaults to 10, a negative or null value removes the limit
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
services:
|
|
||||||
# Development services
|
|
||||||
postgres:
|
|
||||||
image: ghcr.io/serverpod/postgres:16
|
|
||||||
ports:
|
|
||||||
- "8090:5432"
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_DB: halkyon_learn
|
|
||||||
POSTGRES_PASSWORD: "zLZiS0hYW86MNDWF3fTB9iYudoEQ4rZG"
|
|
||||||
volumes:
|
|
||||||
- halkyon_learn_data:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:6.2.6
|
|
||||||
ports:
|
|
||||||
- "8091:6379"
|
|
||||||
command: redis-server --requirepass "CaFsHWMa_hwVFcDEVDMRc5nneKiIC1In"
|
|
||||||
environment:
|
|
||||||
- REDIS_REPLICATION_MODE=master
|
|
||||||
|
|
||||||
# Test services
|
|
||||||
postgres_test:
|
|
||||||
image: ghcr.io/serverpod/postgres:16
|
|
||||||
ports:
|
|
||||||
- "9090:5432"
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_DB: halkyon_learn_test
|
|
||||||
POSTGRES_PASSWORD: "h8hg2NM2Ho1N7Zv-WxwJyhfJC76hxy6h"
|
|
||||||
volumes:
|
|
||||||
- halkyon_learn_test_data:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
redis_test:
|
|
||||||
image: redis:6.2.6
|
|
||||||
ports:
|
|
||||||
- "9091:6379"
|
|
||||||
command: redis-server --requirepass "JRcMj86Jrhg4s6soZ_DShJqQilJGzEJo"
|
|
||||||
environment:
|
|
||||||
- REDIS_REPLICATION_MODE=master
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
halkyon_learn_data:
|
|
||||||
halkyon_learn_test_data:
|
|
||||||
Loading…
Reference in a new issue