feat(deploy): Docker Compose stack for the instance
Some checks failed
Analyze / analyze (push) Has been cancelled
Format / format (push) Has been cancelled
Tests / test (push) Has been cancelled

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:
Tizian 「ティツィアーン」 2026-09-27 17:18:31 +02:00
commit af3a01907e
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 172 additions and 82 deletions

View file

@ -1,58 +1,64 @@
# Build from the project root with:
# docker build -f halkyon_learn_server/Dockerfile .
# The server image. Built from the repository root (the build context), because the
# workspace lockfile lives there:
#
# 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
FROM dart:3.12.2 AS build
# Create a minimal workspace that preserves locked server dependency versions.
# ── Build stage: compile the server to a native executable ────────────────────────
FROM dart:3.13.4 AS build
WORKDIR /app
COPY pubspec.lock .
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' \
'name: _' \
'environment:' \
' sdk: ^3.12.2' \
' sdk: ^3.13.0' \
'workspace:' \
' - halkyon_learn_server' \
> pubspec.yaml
RUN dart pub get
# Compile the server executable.
WORKDIR /app/halkyon_learn_server
RUN dart build cli --target bin/main.dart --output build
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.
RUN mkdir -p config web migrations
# Final stage
FROM alpine:latest
# ── Runtime stage ─────────────────────────────────────────────────────────────────
# 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
# against Postgres never loads it, but a future code path might). CA certificates
# 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
# Environment variables
ENV runmode=production
ENV serverid=default
ENV logging=normal
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 configuration files and resources
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/
# 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
# Expose ports
EXPOSE 8080
EXPOSE 8081
EXPOSE 8082
# The server needs no root rights (it listens on 8080 and writes nothing to the
# image), so it runs as an unprivileged system user.
RUN useradd --system --no-create-home --shell /usr/sbin/nologin app
USER app
# Define the entrypoint command
ENTRYPOINT ./bin/server --mode=$runmode --server-id=$serverid --logging=$logging --role=$role
# Only the API server runs in production (no Insights, no web server).
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 []

View file

@ -16,16 +16,16 @@ apiServer:
#
# No webServer: there is no web app.
# This is the database setup for your servers. The default for the Google Cloud
# Engine Terraform configuration is to connect on a private IP address.
# If you are connecting on a public IP (e.g. on AWS or Google Cloud Run), you
# connect on the public IP of the database e.g. database.examplepod.com.
# The database: the `postgres` service of deploy/compose.yaml, reachable only inside
# the Compose network (it publishes no port), so no TLS between server and database.
# The password comes from the environment (SERVERPOD_DATABASE_PASSWORD, set in
# deploy/.env); there is no production passwords.yaml.
database:
host: database.private-production.examplepod.com
host: postgres
port: 5432
name: serverpod
user: postgres
requireSsl: true
name: halkyon_learn
user: halkyon_learn
requireSsl: false
#isUnixSocket: true # defaults to false
#maxConnectionCount: 10 # Defaults to 10, a negative or null value removes the limit

View file

@ -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: