Docker
Building, configuring, and running ACMEEH in Docker
ACMEEH ships with a production-ready Dockerfile (multi-stage build), a
docker-compose.yaml that brings up ACMEEH and PostgreSQL together, and a
fully parameterized config at docker/config.yaml.
Quick Start
# 1. Create your .env (only POSTGRES_PASSWORD is required)
cp docker/.env.example .env
vi .env # set POSTGRES_PASSWORD
# 2. Place your CA root cert + key
mkdir -p certs
cp /path/to/root.pem certs/root.pem
cp /path/to/root-key.pem certs/root-key.pem
# 3. Build and start
docker compose up -d
# 4. Verify
curl http://localhost:8443/livez
curl http://localhost:8443/directory
Tip
No CA cert yet? Generate a self-signed root CA for testing:
mkdir -p certs
openssl ecparam -genkey -name prime256v1 -out certs/root-key.pem
openssl req -new -x509 -key certs/root-key.pem -out certs/root.pem \
-days 3650 -subj "/CN=ACMEEH Development CA"
Image Overview
Base image |
|
Build strategy |
Multi-stage (builder + runtime) for small final image |
Init system |
|
User |
Non-root |
Port |
|
Healthcheck |
|
Entry point |
|
Directory Layout Inside the Container
/app/
├── config.yaml Default config (override via bind mount)
├── certs/ CA certificates and keys (bind mount)
└── data/ Persistent application data (named volume)
/var/log/acmeeh/ Audit and application logs (named volume)
Build ARGs
Customise the image at build time by setting these ARGs in your .env or
passing them to docker compose build --build-arg:
ARG |
Default |
Description |
|---|---|---|
|
|
Set to |
|
|
Set to |
|
|
Set to |
|
(empty) |
Space-separated list of additional pip packages to install (e.g. custom hook or CA backend plugins) |
|
|
UID for the |
|
|
GID for the |
Examples — build with optional backends:
# HSM support
docker compose build --build-arg INSTALL_HSM=1
# ACME proxy support
docker compose build --build-arg INSTALL_ACME_PROXY=1
Or set them in your .env:
INSTALL_HSM=1
INSTALL_ACME_PROXY=1
Docker Compose Services
The docker-compose.yaml at the project root defines two services:
acmeeh
The application container.
Builds from the project-root
DockerfileReads environment variables from
.envMounts
docker/config.yamlread-only at/app/config.yamlMounts the local
certs/directory read-only at/app/certsUses named volumes for logs and data
Waits for PostgreSQL to be healthy before starting
Health check:
GET /healthzevery 30 sstop_grace_period: 35sallows gunicorn’s graceful shutdown to complete
postgres
PostgreSQL 16 (Alpine).
Data persisted in the
acmeeh-pgdatanamed volumeHost port bound to
127.0.0.1only (not exposed to network)Health check via
pg_isready
Named volumes:
Volume |
Purpose |
|---|---|
|
PostgreSQL data directory |
|
ACMEEH audit and application logs |
|
Application data (ACME proxy storage, etc.) |
Configuration
The Docker config at docker/config.yaml covers every settings section.
It uses two mechanisms:
String fields —
${VAR:-default}environment variable substitution. These resolve at startup through ACMEEH’s built-in env var processor.Non-string fields (integers, booleans, floats) — native YAML values. ACMEEH’s env var resolver produces strings, which would fail JSON Schema validation for typed fields. Adjust these directly in the YAML or override the entire file via a bind mount.
To customise the config:
Simple changes: Set environment variables in
.env(see table below).Structural changes: Edit
docker/config.yamldirectly, or bind-mount your own config file.
Environment Variables
All variables are documented in docker/.env.example. Copy it to .env
and uncomment the ones you need. Only POSTGRES_PASSWORD is required.
PostgreSQL
Variable |
Default |
Description |
|---|---|---|
|
(required) |
Database password (used by both ACMEEH and PostgreSQL container) |
|
|
Database hostname (container name in Compose) |
|
|
Database name |
|
|
Database user |
|
|
PostgreSQL SSL mode ( |
|
|
Host port for PostgreSQL (Compose only — internal port is always 5432) |
Server
Variable |
Default |
Description |
|---|---|---|
|
|
The public URL clients use to reach ACMEEH |
|
|
Bind address inside the container |
|
|
Host port mapping (Compose only) |
|
(empty) |
URL path prefix for all endpoints (e.g. |
|
|
Host path to CA certificates directory (Compose only) |
Reverse Proxy
Variable |
Default |
Description |
|---|---|---|
|
|
Header name for the client IP (set by reverse proxy) |
|
|
Header name for the original protocol ( |
CA Backend
Variable |
Default |
Description |
|---|---|---|
|
|
CA backend type ( |
|
|
Path to root CA certificate (internal backend) |
|
|
Path to root CA private key (internal backend) |
|
|
Serial number source ( |
|
|
Hash algorithm for signing ( |
External CA
Variable |
Default |
Description |
|---|---|---|
|
(empty) |
External CA signing endpoint URL |
|
(empty) |
External CA revocation endpoint URL |
|
|
HTTP header name for authentication |
|
(empty) |
Authentication header value (e.g. |
HSM / PKCS#11
Requires INSTALL_HSM=1 at build time.
Variable |
Default |
Description |
|---|---|---|
|
|
Path to the PKCS#11 shared library |
|
|
PKCS#11 token label |
|
(empty) |
Token PIN |
|
|
Signing key label on the token |
|
|
Key type ( |
|
|
Hash algorithm for HSM signing |
|
|
Path to the issuer certificate |
|
|
Serial number source |
ACME Proxy
Requires INSTALL_ACME_PROXY=1 at build time.
Variable |
Default |
Description |
|---|---|---|
|
(empty) |
Upstream ACME directory URL |
|
(empty) |
Email for upstream ACME account |
|
|
Path for ACME proxy state (inside |
|
|
Challenge type to use with upstream CA |
|
(empty) |
Challenge handler class path |
|
(empty) |
EAB Key Identifier for upstream CAs that require External Account Binding (e.g. ZeroSSL) |
|
(empty) |
EAB HMAC key (Base64-encoded) for External Account Binding |
|
(empty) |
HTTP proxy URL for outbound connections to the upstream ACME server |
Logging
Variable |
Default |
Description |
|---|---|---|
|
|
Log level ( |
|
|
Log format ( |
|
|
Audit log file path (inside |
SMTP
Variable |
Default |
Description |
|---|---|---|
|
(empty) |
SMTP server hostname (enable |
|
(empty) |
SMTP authentication username |
|
(empty) |
SMTP authentication password |
|
(empty) |
Sender email address |
Admin API
Variable |
Default |
Description |
|---|---|---|
|
(empty) |
JWT signing secret (enable |
|
(empty) |
Email for the initial admin user |
|
|
Admin API URL prefix |
Other
Variable |
Default |
Description |
|---|---|---|
|
|
Rate limit storage ( |
|
(empty) |
Terms of Service URL shown in the ACME directory |
|
|
CRL endpoint path |
|
|
ARI endpoint path |
|
|
Metrics endpoint path |
|
(empty) |
Webhook URL for audit event export |
|
(empty) |
Syslog host for audit event export |
Common Operations
Validate Config Without Starting
docker compose run --rm --no-deps acmeeh \
acmeeh -c /app/config.yaml --validate-only
The --no-deps flag skips starting PostgreSQL, which is not needed for
config validation.
View Logs
# All services
docker compose logs -f
# ACMEEH only
docker compose logs -f acmeeh
# Audit log (inside the container volume)
docker compose exec acmeeh cat /var/log/acmeeh/audit.log
Check Health
# Liveness (is the process alive?)
curl http://localhost:8443/livez
# Comprehensive health (database, CA, workers)
curl http://localhost:8443/healthz
# Readiness (ready for traffic?)
curl http://localhost:8443/readyz
Test CA Signing
docker compose exec acmeeh acmeeh -c /app/config.yaml ca test-sign
Database Operations
# Check database connectivity
docker compose exec acmeeh acmeeh -c /app/config.yaml db status
# Connect to PostgreSQL directly
docker compose exec postgres psql -U acmeeh -d acmeeh
Rebuild After Code Changes
docker compose build
docker compose up -d
Restart ACMEEH Only
docker compose restart acmeeh
Stop Everything
# Stop containers (preserves volumes)
docker compose down
# Stop and delete volumes (destroys database!)
docker compose down -v
Enabling Optional Subsystems
Several subsystems are disabled by default. To enable them, edit
docker/config.yaml and set the enabled flag to true:
Subsystem |
Config key |
Notes |
|---|---|---|
Admin API |
|
Set |
CRL |
|
Requires the internal or HSM CA backend |
ARI |
|
ACME Renewal Information (draft-ietf-acme-ari) |
Metrics |
|
Prometheus-compatible |
SMTP |
|
Set |
Background worker |
|
Retries stale challenges automatically |
Reverse Proxy with Docker
In production, put ACMEEH behind a TLS-terminating reverse proxy. Enable
proxy header handling in docker/config.yaml:
proxy:
enabled: true
trusted_proxies:
- 172.16.0.0/12 # Docker default bridge network range
- 10.0.0.0/8
Example Nginx service added to docker-compose.yaml:
services:
nginx:
image: nginx:alpine
ports:
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./tls:/etc/nginx/tls:ro
depends_on:
- acmeeh
networks:
- acmeeh-net
Production Hardening
Set
ACMEEH_EXTERNAL_URLto the real public HTTPS URLUse a strong, random
POSTGRES_PASSWORD(32+ characters)Set
POSTGRES_SSLMODE=requireif PostgreSQL is not on the same hostSet
LOG_FORMAT=jsonfor structured log ingestionSet
ADMIN_TOKEN_SECRETto 32+ random bytes if the admin API is enabledRestrict the
certs/directory permissions — the CA private key should be readable only by the container userBump
server.workersindocker/config.yamlto match your CPU count (2–4x cores)Set
server.max_requests: 1000to recycle workers periodicallyUse
databaserate limit backend for multi-instance deploymentsEnable CRL for revocation checking
Back up the
acmeeh-pgdatavolume regularly
Scaling
ACMEEH is stateless — all state lives in PostgreSQL. To run multiple instances:
docker compose up -d --scale acmeeh=3
All instances share the same database. Background workers use PostgreSQL
advisory locks for leader election, so only one instance runs each worker at
a time. Point a load balancer at the scaled instances and set the same
ACMEEH_EXTERNAL_URL on all of them.
Troubleshooting
Container exits immediately
Check logs for configuration or CA errors:
docker compose logs acmeeh
Common causes:
Missing
POSTGRES_PASSWORDin.envCA certificate files not mounted (
certs/directory empty or missing)Config validation error (run
--validate-onlyto diagnose)
Port already in use
If PostgreSQL port 5432 is already taken on your host:
# In .env
POSTGRES_PORT=5433
This only changes the host-side port mapping. The containers still communicate on port 5432 internally.
Database connection refused
The ACMEEH container waits for PostgreSQL to be healthy before starting.
If it still fails, verify the POSTGRES_HOST matches the service name in
docker-compose.yaml (default: postgres).
Permission denied on certs
The container runs as acmeeh (UID 1000 by default). Ensure your
certificate files are readable by this UID, or change ACMEEH_UID /
ACMEEH_GID to match your host file ownership.