PostgreSQL and MinIO for the platform, behind one TLS entry point
A Compose stack that publishes PostgreSQL, the MinIO S3 API and console, and pgAdmin through Caddy, which is the only container with public ports. The data services sit on an internal network. Caddy is built with the pinned caddy-l4 module so it can terminate TLS on the PostgreSQL wire protocol itself (SSLRequest), not only HTTP. MinIO is built from its pinned upstream security release, which has no published image. Every secret comes from .env, which is ignored; .env.example documents it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
09d1871a4b
|
|
@ -0,0 +1,3 @@
|
|||
.env
|
||||
.git
|
||||
README.md
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# All four names must resolve publicly to this server before the first start.
|
||||
POSTGRES_DOMAIN=postgres.example.com
|
||||
MINIO_API_DOMAIN=s3.example.com
|
||||
MINIO_CONSOLE_DOMAIN=minio.example.com
|
||||
PGADMIN_DOMAIN=pgadmin.example.com
|
||||
TLS_CONTACT=admin@example.com
|
||||
|
||||
# Caddy is the only public entry point. Change the port only if clients also use it.
|
||||
PUBLIC_BIND_ADDRESS=0.0.0.0
|
||||
HTTP_PUBLIC_PORT=80
|
||||
HTTPS_PUBLIC_PORT=443
|
||||
POSTGRES_PUBLIC_PORT=5432
|
||||
|
||||
POSTGRES_DB=humainflow
|
||||
POSTGRES_USER=humainflow
|
||||
POSTGRES_PASSWORD=replace-with-a-long-random-password
|
||||
|
||||
# MinIO requires at least 3 characters for the user and 8 for the password;
|
||||
# use substantially longer random values in production.
|
||||
MINIO_ROOT_USER=humainflow-admin
|
||||
MINIO_ROOT_PASSWORD=replace-with-an-independent-long-random-password
|
||||
|
||||
# Initial pgAdmin administrator. These values are used only when the
|
||||
# pgadmin-data volume is created for the first time.
|
||||
PGADMIN_DEFAULT_EMAIL=admin@example.com
|
||||
PGADMIN_DEFAULT_PASSWORD=replace-with-a-third-independent-random-password
|
||||
|
||||
# Optional image override. Keep the same major version when upgrading an existing volume.
|
||||
POSTGRES_IMAGE=postgres:17.11-alpine
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Holds the real passwords and domains of one deployment; .env.example is the documented one.
|
||||
.env
|
||||
.DS_Store
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
admin off
|
||||
email {$TLS_CONTACT}
|
||||
|
||||
# Stock Caddy only proxies HTTP. The pinned caddy-l4 module also handles the
|
||||
# PostgreSQL SSLRequest handshake, terminates TLS, then proxies cleartext only
|
||||
# over the private Docker network.
|
||||
layer4 {
|
||||
:5432 {
|
||||
@postgres postgres
|
||||
route @postgres {
|
||||
postgres_tls
|
||||
tls
|
||||
proxy postgres:5432
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Besides providing a useful status response, this site block tells Caddy to
|
||||
# obtain and renew the certificate used by the Layer 4 PostgreSQL listener.
|
||||
{$POSTGRES_DOMAIN} {
|
||||
respond "PostgreSQL is available on port 5432 with TLS required.\n" 200
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format json
|
||||
}
|
||||
}
|
||||
|
||||
# S3 API. Keep this on its own hostname: S3 request signatures include the host.
|
||||
{$MINIO_API_DOMAIN} {
|
||||
reverse_proxy minio:9000
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format json
|
||||
}
|
||||
}
|
||||
|
||||
# Browser-based MinIO console, separated from the S3 API hostname.
|
||||
{$MINIO_CONSOLE_DOMAIN} {
|
||||
reverse_proxy minio:9001
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format json
|
||||
}
|
||||
}
|
||||
|
||||
# Multi-user PostgreSQL administration UI. Authentication is handled by
|
||||
# pgAdmin; database permissions remain the responsibility of PostgreSQL roles.
|
||||
{$PGADMIN_DOMAIN} {
|
||||
reverse_proxy pgadmin:5050
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format json
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
# PostgreSQL and MinIO public data stack
|
||||
|
||||
This Compose stack publishes four encrypted endpoints through Caddy:
|
||||
|
||||
- PostgreSQL on `postgres.example.com:5432` using the native PostgreSQL TLS negotiation;
|
||||
- the MinIO S3 API on `https://s3.example.com`;
|
||||
- the MinIO console on `https://minio.example.com`;
|
||||
- the multi-user pgAdmin interface on `https://pgadmin.example.com`.
|
||||
|
||||
PostgreSQL and MinIO have no directly published container ports. Caddy is the only public entry
|
||||
point; traffic from Caddy to the services stays on an internal Docker network. Caddy persists its
|
||||
ACME account, certificates and private keys in `caddy-data`, so restarts do not trigger unnecessary
|
||||
certificate reissuance.
|
||||
|
||||
The Caddy image is built with the pinned `caddy-l4` module because stock Caddy only proxies HTTP.
|
||||
The module understands PostgreSQL's initial `SSLRequest`, terminates TLS with Caddy's automatically
|
||||
managed certificate, and sends the decrypted connection to PostgreSQL only on the private network.
|
||||
|
||||
The MinIO server is built from its latest upstream security release. Upstream did not publish a
|
||||
container for that release, so the included multi-stage Dockerfile builds the pinned source tag.
|
||||
|
||||
pgAdmin is pinned to version 9.18 and runs in server mode. Its users, sessions, preferences and
|
||||
saved server definitions live in the `pgadmin-data` volume.
|
||||
|
||||
## Start
|
||||
|
||||
1. Create public `A`/`AAAA` records for the four names and point them to the server.
|
||||
2. Allow inbound TCP ports `80`, `443`, and `5432` in the host/cloud firewall. Restrict `5432` to
|
||||
known client address ranges whenever possible.
|
||||
3. Copy the environment template and replace every placeholder:
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
openssl rand -base64 36
|
||||
docker compose config --quiet
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
4. Follow certificate issuance and startup:
|
||||
|
||||
```sh
|
||||
docker compose logs -f caddy postgres minio pgadmin
|
||||
```
|
||||
|
||||
Ports 80 and 443 must reach Caddy from the public Internet for the usual ACME HTTP/TLS challenges.
|
||||
The names must be eligible for Let's Encrypt issuance; a restrictive DNS CAA record can refuse it.
|
||||
|
||||
## Connect
|
||||
|
||||
PostgreSQL requires TLS at the public listener. `verify-full` both encrypts the connection and checks
|
||||
that the certificate matches the hostname:
|
||||
|
||||
```sh
|
||||
psql "host=postgres.example.com port=5432 dbname=humainflow user=humainflow sslmode=verify-full"
|
||||
```
|
||||
|
||||
MinIO/S3 clients use `https://s3.example.com`; administrators open
|
||||
`https://minio.example.com`. `MINIO_SERVER_URL` is set to the public S3 hostname so presigned URLs
|
||||
remain valid behind the reverse proxy.
|
||||
|
||||
Open `https://pgadmin.example.com` and sign in with `PGADMIN_DEFAULT_EMAIL` and
|
||||
`PGADMIN_DEFAULT_PASSWORD`. On the first login, register the database with these values:
|
||||
|
||||
- host: `postgres` (the Compose service name, not the public hostname);
|
||||
- port: `5432`;
|
||||
- maintenance database: the value of `POSTGRES_DB`;
|
||||
- username/password: a PostgreSQL role and its password.
|
||||
|
||||
The initial pgAdmin administrator can create additional pgAdmin accounts from User Management.
|
||||
pgAdmin accounts only control access to the web interface: create separate least-privilege
|
||||
PostgreSQL roles for database authorization. Each person should use their own database role rather
|
||||
than sharing `POSTGRES_USER`. Changing the default pgAdmin password in `.env` after the first start
|
||||
does not update the account already stored in `pgadmin-data`; change it from pgAdmin instead.
|
||||
|
||||
## Operations
|
||||
|
||||
The persistent volumes are `postgres-data`, `minio-data`, `pgadmin-data`, and `caddy-data`. Back up
|
||||
the first three; do not treat Docker volumes as backups. Never run `docker compose down -v` unless
|
||||
permanent deletion of both data stores, pgAdmin's configuration and Caddy's certificate state is
|
||||
intended.
|
||||
|
||||
Upgrade PostgreSQL one major version at a time using the PostgreSQL upgrade procedure. Updating an
|
||||
image tag alone does not migrate an existing database volume.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
FROM caddy:2.11.4-builder-alpine AS builder
|
||||
|
||||
# Layer 4 adds PostgreSQL protocol matching and SSLRequest/TLS termination.
|
||||
RUN xcaddy build \
|
||||
--with github.com/mholt/caddy-l4@v0.1.2
|
||||
|
||||
FROM caddy:2.11.4-alpine
|
||||
|
||||
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
name: humainflow-data-stack
|
||||
|
||||
x-logging: &default-logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: ${POSTGRES_IMAGE:-postgres:17.11-alpine}
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-humainflow}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-humainflow}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
|
||||
POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256
|
||||
POSTGRES_HOST_AUTH_METHOD: scram-sha-256
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- password_encryption=scram-sha-256
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
expose:
|
||||
- "5432"
|
||||
networks:
|
||||
- data-backend
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
shm_size: 256mb
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 60s
|
||||
logging: *default-logging
|
||||
|
||||
minio:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: minio.Dockerfile
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?set MINIO_ROOT_USER in .env}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?set MINIO_ROOT_PASSWORD in .env}
|
||||
# Required for presigned URLs generated while MinIO is behind Caddy.
|
||||
MINIO_SERVER_URL: https://${MINIO_API_DOMAIN:?set MINIO_API_DOMAIN in .env}
|
||||
MINIO_BROWSER_REDIRECT_URL: https://${MINIO_CONSOLE_DOMAIN:?set MINIO_CONSOLE_DOMAIN in .env}
|
||||
command: ["server", "/data", "--console-address", ":9001"]
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
expose:
|
||||
- "9000"
|
||||
- "9001"
|
||||
networks:
|
||||
- data-backend
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "--quiet", "http://127.0.0.1:9000/minio/health/live"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 15s
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 60s
|
||||
logging: *default-logging
|
||||
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4:9.18
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:?set PGADMIN_DEFAULT_EMAIL in .env}
|
||||
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:?set PGADMIN_DEFAULT_PASSWORD in .env}
|
||||
# Caddy terminates TLS. pgAdmin listens only on the private Docker network.
|
||||
PGADMIN_LISTEN_ADDRESS: 0.0.0.0
|
||||
PGADMIN_LISTEN_PORT: 5050
|
||||
PGADMIN_DISABLE_POSTFIX: "true"
|
||||
PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: "True"
|
||||
PGADMIN_CONFIG_SESSION_COOKIE_SECURE: "True"
|
||||
PGADMIN_CONFIG_UPGRADE_CHECK_ENABLED: "False"
|
||||
volumes:
|
||||
- pgadmin-data:/var/lib/pgadmin
|
||||
expose:
|
||||
- "5050"
|
||||
networks:
|
||||
- data-backend
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:5050/misc/ping"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
cap_drop:
|
||||
- ALL
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
pids_limit: 256
|
||||
mem_limit: 512m
|
||||
cpus: 1
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
|
||||
caddy:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: caddy.Dockerfile
|
||||
environment:
|
||||
TLS_CONTACT: ${TLS_CONTACT:?set TLS_CONTACT in .env}
|
||||
POSTGRES_DOMAIN: ${POSTGRES_DOMAIN:?set POSTGRES_DOMAIN in .env}
|
||||
MINIO_API_DOMAIN: ${MINIO_API_DOMAIN:?set MINIO_API_DOMAIN in .env}
|
||||
MINIO_CONSOLE_DOMAIN: ${MINIO_CONSOLE_DOMAIN:?set MINIO_CONSOLE_DOMAIN in .env}
|
||||
PGADMIN_DOMAIN: ${PGADMIN_DOMAIN:?set PGADMIN_DOMAIN in .env}
|
||||
ports:
|
||||
# 80/443 are used for ACME challenges, HTTPS and redirects.
|
||||
- "${PUBLIC_BIND_ADDRESS:-0.0.0.0}:${HTTP_PUBLIC_PORT:-80}:80"
|
||||
- "${PUBLIC_BIND_ADDRESS:-0.0.0.0}:${HTTPS_PUBLIC_PORT:-443}:443"
|
||||
# PostgreSQL-over-TLS is terminated by Caddy's Layer 4 module.
|
||||
- "${PUBLIC_BIND_ADDRESS:-0.0.0.0}:${POSTGRES_PUBLIC_PORT:-5432}:5432"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
# Certificates, private keys and the ACME account must survive restarts.
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
networks:
|
||||
- edge
|
||||
- data-backend
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
pgadmin:
|
||||
condition: service_healthy
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
read_only: true
|
||||
pids_limit: 128
|
||||
mem_limit: 256m
|
||||
cpus: 1
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
|
||||
networks:
|
||||
edge:
|
||||
data-backend:
|
||||
internal: true
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
minio-data:
|
||||
pgadmin-data:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
FROM golang:1.25.1-alpine3.22 AS builder
|
||||
|
||||
ARG MINIO_VERSION=RELEASE.2025-10-15T17-29-55Z
|
||||
|
||||
RUN apk add --no-cache bash git
|
||||
RUN git clone --branch "${MINIO_VERSION}" --depth 1 https://github.com/minio/minio.git /src
|
||||
|
||||
WORKDIR /src
|
||||
RUN LDFLAGS="$(MINIO_RELEASE=RELEASE go run buildscripts/gen-ldflags.go)" && \
|
||||
CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "${LDFLAGS}" -o /out/minio .
|
||||
|
||||
FROM alpine:3.22
|
||||
|
||||
RUN apk add --no-cache ca-certificates && \
|
||||
addgroup -S -g 1000 minio && \
|
||||
adduser -S -D -H -u 1000 -G minio minio && \
|
||||
install -d -o minio -g minio /data
|
||||
|
||||
COPY --from=builder /out/minio /usr/local/bin/minio
|
||||
|
||||
USER minio:minio
|
||||
EXPOSE 9000 9001
|
||||
VOLUME ["/data"]
|
||||
ENTRYPOINT ["/usr/local/bin/minio"]
|
||||
Loading…
Reference in New Issue