This guide covers building, deploying, and running CertMate in Docker — including multi-platform support for ARM and AMD64.
# 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:latestdocker 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:latestThe 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
docker history certmate:latest
docker inspect certmate:latest | grep -i env
docker run --rm certmate:latest find / -name "*.env" 2>/dev/nullCreate 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=INFOdocker 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:latestdocker 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| 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) |
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 |
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 |
No | When set, unified backups are encrypted at rest (.zip.enc, PBKDF2-SHA256 + Fernet). The same passphrase is required to restore them. Unset = legacy cleartext .zip 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.
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 -dThe 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.)
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:latestA 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:latestAlternatively 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:latestservices:
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.
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:latestSettings-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.
CertMate supports multi-platform Docker images for both ARM and AMD64 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 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# 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 .# Verify buildx support
docker buildx version
docker buildx inspect --bootstrap
# Enable QEMU emulation (if needed)
docker run --privileged --rm tonistiigi/binfmt --install all# 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# 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.0Required 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- 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 .
docker logs certmate
docker exec certmate envdocker logs certmate
docker exec certmate curl -v http://localhost:8000/healthdocker exec certmate ls -la /app/certificates
docker exec certmate ls -la /app/data| 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 |
Typical sizes per architecture:
- AMD64: ~200-300 MB
- ARM64: ~200-300 MB
- ARM v7: ~180-250 MB