Docker Build & Deployment
September 8, 2026 · View on GitHub
This guide covers building, deploying, and running CertMate in Docker — including multi-platform support for ARM and AMD64.
Quick Start
Pull and Run
# Docker automatically selects the right architecture
docker run -d --name certmate \
--env-file .env \
-p 8000:8000 \
-v certmate_data:/app/data \
-v certmate_certificates:/app/certificates \
fabriziosalmi/certmate:latest
Build and Run Locally
docker build -t certmate:latest .
docker run -d --name certmate \
--env-file .env \
-p 8000:8000 \
-v certmate_certificates:/app/certificates \
-v certmate_data:/app/data \
-v certmate_logs:/app/logs \
certmate:latest
Security
The build process ensures no secrets are included in the image:
.dockerignoreexcludes all.envfiles and sensitive data- Environment variables are provided at runtime, not build time
- Only essential application files are included
- Images can be safely pushed to public registries
Verify No Secrets in Image
docker history certmate:latest
docker inspect certmate:latest | grep -i env
docker run --rm certmate:latest find / -name "*.env" 2>/dev/null
Runtime Configuration
Option 1: Environment File
Create a .env file on your host (not in the Docker image):
SECRET_KEY=your-super-secret-key-here
# SECRET_KEY_FILE=/run/secrets/secret_key # Alternative: takes precedence over SECRET_KEY
# Set API_BEARER_TOKEN before exposing a not-yet-onboarded instance to a
# network: until the first admin exists, an instance with no token serves the
# setup bypass to anyone who can reach it. When set, paste it once on the
# first-run screen to create the admin.
API_BEARER_TOKEN=your-api-bearer-token-here
# API_BEARER_TOKEN_FILE=/run/secrets/api_bearer_token # Alternative: takes precedence over API_BEARER_TOKEN
CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
LOG_LEVEL=INFO
docker run -d --name certmate \
--env-file .env \
-p 8000:8000 \
-v certmate_certificates:/app/certificates \
-v certmate_data:/app/data \
-v certmate_logs:/app/logs \
certmate:latest
Option 2: Direct Environment Variables
docker run -d --name certmate \
-e SECRET_KEY="your-secret-key" \
# -e SECRET_KEY_FILE="/run/secrets/secret_key" \ # Alternative: takes precedence over SECRET_KEY
-e API_BEARER_TOKEN="your-api-bearer-token" \
# -e API_BEARER_TOKEN_FILE="/run/secrets/api_bearer_token" \ # Alternative: takes precedence over API_BEARER_TOKEN
-e CLOUDFLARE_API_TOKEN="your-api-token" \
-p 8000:8000 \
-v certmate_certificates:/app/certificates \
-v certmate_data:/app/data \
certmate:latest
Environment Variables Reference
| Variable | Required | Description |
|---|---|---|
SECRET_KEY | No | Flask secret key for sessions (auto-generated if unset) |
SECRET_KEY_FILE | No | Path to a file containing the Flask secret key (takes precedence over SECRET_KEY). If set and the file cannot be read or is empty, CertMate refuses to start: a secret that failed to mount is a configuration error, and generating a key instead would sign out every user, on every restart, with sessions signed by a key you did not choose. |
API_BEARER_TOKEN | No (auto-generated) | API auth token. Auto-generated if unset, but set it before exposing a not-yet-onboarded instance to a network; when set, paste it once on the first-run screen to create the admin. This value is authoritative: if it differs from the token already stored, the stored one is replaced at startup and stops working. That is what makes adding or rotating the variable on an existing install take effect — previously enforcement used this value while authentication still checked the old one, and the first-run screen asked for a token it then rejected |
API_BEARER_TOKEN_FILE | No | Path to a file containing the API bearer token (takes precedence over API_BEARER_TOKEN) |
LOG_LEVEL | No | INFO (default), DEBUG, WARNING, ERROR |
CERTMATE_BACKUP_PASSPHRASE | Set it if you want automatic backups you can restore from | Encrypts unified backups at rest (.zip.enc, PBKDF2-SHA256 + Fernet), and is what makes automatic backups complete — with it they can restore this instance, without it they keep their credentials masked and cannot. The same passphrase is required to restore them, and CertMate never stores it for you. Keep it somewhere other than the machine holding the backups |
CLOUDFLARE_API_TOKEN | No | Cloudflare DNS provider token |
AWS_ACCESS_KEY_ID | No | AWS Route53 access key |
AWS_SECRET_ACCESS_KEY | No | AWS Route53 secret key |
See the Installation Guide for the complete list.
Keeping DNS tokens out of settings.json
DNS provider tokens configured through the UI are stored in settings.json,
which is the file that gets backed up, copied and mounted. Any credential field
can instead name where its value lives — api_token_file with a path, or
api_token_env with a variable name — the same way API_BEARER_TOKEN_FILE
works. The value is read when certbot runs and is never written back. The OIDC
client_secret accepts the same pair. See
SECURITY.md.
Docker Compose
Basic Setup
version: '3.8'
services:
certmate:
image: fabriziosalmi/certmate:latest
container_name: certmate
ports:
- "8000:8000"
environment:
- SECRET_KEY=${SECRET_KEY:-}
# - SECRET_KEY_FILE=${SECRET_KEY_FILE:-} # Alternative: path to a file containing the secret key
- API_BEARER_TOKEN=${API_BEARER_TOKEN:-}
# - API_BEARER_TOKEN_FILE=${API_BEARER_TOKEN_FILE:-} # Alternative: path to a file containing the bearer token
- LOG_LEVEL=${LOG_LEVEL:-INFO}
volumes:
- certmate_certificates:/app/certificates
- certmate_data:/app/data
- certmate_logs:/app/logs
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
certmate_certificates:
certmate_data:
certmate_logs:
# Start with .env file in the same directory
docker-compose up -d
# Or specify a different env file
docker-compose --env-file /path/to/.env up -d
Rootless podman / OpenShift (arbitrary UID)
The image runs as the non-root user 1000 by default, but it also follows the
OpenShift arbitrary-UID pattern: the runtime-writable directories
(/app/data, /app/certificates, /app/logs, /app/backups) are owned by
group 0 (root group) and are group-writable + setgid. A container process
running as any UID that belongs to group 0 — which is how rootless podman and
OpenShift launch containers — can therefore create and rename files without a
manual chown -R 1000:1000 of the volumes. Secret files the app writes (the CA
private key, the audit-signing key, DNS credential files, .secret_key) are
always created 0600 owner-only, so the group-writable directories never expose
a key. (Issue #380.)
Named volumes — works out of the box
A fresh named volume inherits the image's group-0 permissions, so no host preparation is needed regardless of the UID podman assigns:
podman run -d --name certmate \
-p 8000:8000 \
-v certmate_data:/app/data \
-v certmate_certificates:/app/certificates \
-v certmate_logs:/app/logs \
-v certmate_backups:/app/backups \
docker.io/fabriziosalmi/certmate:latest
Bind mounts (host directories)
A bind mount keeps the host directory's ownership, which shadows the image's permissions. Make the host directories writable by group 0 once, then run:
# Create the host dirs group-0 writable (any UID in group 0 can write)
mkdir -p ./{data,certificates,logs,backups}
chgrp -R 0 ./{data,certificates,logs,backups}
chmod -R g+rwX ./{data,certificates,logs,backups}
podman run -d --name certmate \
-p 8000:8000 \
-v ./data:/app/data \
-v ./certificates:/app/certificates \
-v ./logs:/app/logs \
-v ./backups:/app/backups \
docker.io/fabriziosalmi/certmate:latest
Alternatively let podman fix the ownership for you with the :U mount option
(recursively chowns the source to match the container UID/GID):
podman run -d --name certmate \
-p 8000:8000 \
-v ./data:/app/data:U \
-v ./certificates:/app/certificates:U \
-v ./logs:/app/logs:U \
-v ./backups:/app/backups:U \
docker.io/fabriziosalmi/certmate:latest
rootless podman-compose
services:
certmate:
image: docker.io/fabriziosalmi/certmate:latest
ports:
- "8000:8000"
volumes:
- certmate_data:/app/data
- certmate_certificates:/app/certificates
- certmate_logs:/app/logs
- certmate_backups:/app/backups
restart: unless-stopped
volumes:
certmate_data:
certmate_certificates:
certmate_logs:
certmate_backups:
If startup aborts with "Required directories are not writable by the CertMate
process", the mount is not group-0 writable — apply the chgrp 0 … && chmod g+rwX … above, switch to a named volume, or add :U to the bind mounts.
Kubernetes / OpenShift: no changes needed. Set
spec.securityContext.fsGroup: 0(or rely on the default restricted SCC, which already assigns an arbitrary UID in group 0) and the mounted volumes become group-0 writable automatically.
Upgrading
CertMate keeps all persistent state in the mounted volumes — chiefly ./data
(settings.json, the admin users, the auto-generated .secret_key, the audit
signing key, and the scheduler database) and ./certificates. Because state
lives in those volumes and not in the image, upgrading is just pulling a
newer image and recreating the container; your configuration, certificates, and
login carry forward.
# Recommended: take a backup first (Settings → Backup, or the API)
# Docker Compose
docker compose pull # fetch the new image
docker compose up -d # recreate the container (data/ + certificates/ persist)
# Plain docker run — stop/remove and re-run with the SAME volume mounts
docker pull fabriziosalmi/certmate:latest
docker rm -f certmate
docker run -d --name certmate --env-file .env -p 127.0.0.1:8000:8000 \
-v "$(pwd)/data:/app/data" -v "$(pwd)/certificates:/app/certificates" \
-v "$(pwd)/logs:/app/logs" -v "$(pwd)/backups:/app/backups" \
fabriziosalmi/certmate:latest
Settings-format migrations run automatically on boot. For production, pin a
version tag (e.g. fabriziosalmi/certmate:2.19) instead of :latest so repulls
don't surprise you with an unintended upgrade — the multi-platform build
publishes MAJOR, MAJOR.MINOR, and MAJOR.MINOR.PATCH tags.
Multi-Platform Builds
CertMate supports multi-platform Docker images for both ARM and AMD64 architectures.
Supported Architectures
| Platform | Description | Common Use Cases |
|---|---|---|
linux/amd64 | Intel/AMD 64-bit | Most cloud servers, desktops |
linux/arm64 | ARM 64-bit | Apple Silicon, ARM cloud instances |
linux/arm/v7 | ARM 32-bit v7 | Raspberry Pi 3+ — not published: buildable on request (see below), the release images are amd64 + arm64 |
linux/arm/v6 | ARM 32-bit v6 | Raspberry Pi 1, Zero |
Build Scripts
# Build for current platform only
./build-docker.sh
# Build for multiple platforms (ARM64 + AMD64)
./build-docker.sh -m
# Build and push to Docker Hub
./build-docker.sh -m -p -r YOUR_DOCKERHUB_USERNAME
# Dedicated multi-platform script
./build-multiplatform.sh -r USERNAME -v v1.0.0 -p
# Build for Raspberry Pi
./build-multiplatform.sh --platforms linux/arm/v7 -r USERNAME -p
Manual Docker Buildx
# Create and use buildx builder
docker buildx create --name certmate-builder --use
# Build for multiple platforms
docker buildx build --platform linux/amd64,linux/arm64 \
-t USERNAME/certmate:latest .
# Build and push
docker buildx build --platform linux/amd64,linux/arm64 \
-t USERNAME/certmate:latest --push .
Prerequisites for Multi-Platform
# Verify buildx support
docker buildx version
docker buildx inspect --bootstrap
# Enable QEMU emulation (if needed)
docker run --privileged --rm tonistiigi/binfmt --install all
Force Specific Platform
# Force AMD64 (e.g., on Apple Silicon for testing)
docker run --platform linux/amd64 --rm \
--env-file .env -p 8000:8000 certmate:latest
# Auto-detect (recommended)
docker run --rm --env-file .env -p 8000:8000 certmate:latest
Pushing to Docker Hub
# Login
docker login
# Tag and push
docker build -t USERNAME/certmate:latest .
docker push USERNAME/certmate:latest
# With version tag
docker build -t USERNAME/certmate:v1.0.0 .
docker push USERNAME/certmate:v1.0.0
CI/CD Integration
GitHub Actions
Required secrets:
DOCKERHUB_USERNAMEDOCKERHUB_TOKEN
# Manual trigger with custom platforms
gh workflow run docker-multiplatform.yml \
-f platforms="linux/amd64,linux/arm64,linux/arm/v7" \
-f push_to_registry=true
Production Tips
- Use secrets management: Docker secrets, Kubernetes secrets, or a secrets manager
- Enable TLS: Run behind a reverse proxy with TLS termination
- Monitor resources: Set CPU and memory limits
- Backup volumes: Regularly backup certificate and data volumes
- Update regularly: Keep the image updated with security patches
- Use layer caching for faster builds:
docker buildx build --cache-from type=registry,ref=USERNAME/certmate:cache .
Troubleshooting
Container Won't Start
docker logs certmate
docker exec certmate env
Health Check Fails
docker logs certmate
docker exec certmate curl -v http://localhost:8000/health
Permission Issues
docker exec certmate ls -la /app/certificates
docker exec certmate ls -la /app/data
Multi-Platform Build Issues
| Error | Solution |
|---|---|
| "multiple platforms not supported for docker driver" | docker buildx create --name multiplatform --use |
| "exec format error" | docker run --privileged --rm tonistiigi/binfmt --install all |
| Slow non-native builds | Normal due to emulation; use GitHub Actions for production |
| Cannot load multi-platform to local Docker | Use --load with single platform for local testing |
Image Sizes
Typical sizes per architecture:
- AMD64: ~200-300 MB
- ARM64: ~200-300 MB
- ARM v7: ~180-250 MB