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:

  • .dockerignore excludes all .env files 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

VariableRequiredDescription
SECRET_KEYNoFlask secret key for sessions (auto-generated if unset)
SECRET_KEY_FILENoPath 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_TOKENNo (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_FILENoPath to a file containing the API bearer token (takes precedence over API_BEARER_TOKEN)
LOG_LEVELNoINFO (default), DEBUG, WARNING, ERROR
CERTMATE_BACKUP_PASSPHRASESet it if you want automatic backups you can restore fromEncrypts 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_TOKENNoCloudflare DNS provider token
AWS_ACCESS_KEY_IDNoAWS Route53 access key
AWS_SECRET_ACCESS_KEYNoAWS 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

PlatformDescriptionCommon Use Cases
linux/amd64Intel/AMD 64-bitMost cloud servers, desktops
linux/arm64ARM 64-bitApple Silicon, ARM cloud instances
linux/arm/v7ARM 32-bit v7Raspberry Pi 3+ — not published: buildable on request (see below), the release images are amd64 + arm64
linux/arm/v6ARM 32-bit v6Raspberry 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_USERNAME
  • DOCKERHUB_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

  1. Use secrets management: Docker secrets, Kubernetes secrets, or a secrets manager
  2. Enable TLS: Run behind a reverse proxy with TLS termination
  3. Monitor resources: Set CPU and memory limits
  4. Backup volumes: Regularly backup certificate and data volumes
  5. Update regularly: Keep the image updated with security patches
  6. 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

ErrorSolution
"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 buildsNormal due to emulation; use GitHub Actions for production
Cannot load multi-platform to local DockerUse --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

← Back to DocumentationInstallation →Architecture →