Quickstart with Docker

This guide provides instructions for running your own certificate authority using a plain Docker container. Using this setup allows you to run django-ca in an isolated environment that can be easily updated, but use external resources for a web server, database and cache.

Another use case for this guide is to integrate the image into a Docker Swarm or Kubernetes setup and use the instructions here as a template.

Note

If you just want to get a CA up and running quickly, why not try Quickstart with Docker Compose?

This tutorial will give you a CA with

  • A root and intermediate CA.

  • A browsable admin interface (using plain http)

  • Certificate revocation using CRLs and OCSP.

  • (Optional) ACMEv2 support (= get certificates using certbot).

TLS support for the admin interface is just a standard TLS setup for NGINX, so this setup is left as an exercise to the reader.

Requirements

This guide assumes that you have moderate knowledge of running servers, installing software and how TLS certificates work.

The guide assumes you use a dedicated server to set up your certificate authority and does not account for potential conflicts with other software like ports, directories or container names.

A certificate authority needs plain HTTP for CRL and OCSP access and HTTPS for ACMEv2. Using standard ports are strongly recommended (clients might fail otherwise). If you do not need any of the mentioned features, you can use all other features from the command line and do not need a web server.

Setup DNS

Before you set up your certificate authority, you need a domain name that points to the server where you install django-ca. Since the domain is encoded in CA certificates, it cannot be easily changed later.

This guide assumes that ca.example.com is the name that points to the server where you are setting up your certificate authority.

Required software

To run django-ca, you need Docker. You will also need at least a supported database and a web server (like NGINX or Apache) to serve static files.

In our guide, we are going to run PostgreSQL as a database, Redis as a cache and NGINX as a front-facing web server each in a separate Docker container. Please refer to your operating system installation instructions for how to install the software on your own.

Note

Starting dependencies as Docker containers serves us well for this guide, but makes the guide technically almost identical to just using docker-compose. If you do not already have all the software already set up or want to integrate django-ca into an unsupported orchestration setup like Docker Swarm or Kubernetes, you probably really want to just use docker-compose!

On Debian/Ubuntu, simply do:

user@host:~$ sudo apt update
user@host:~$ sudo apt install docker.io

If you want to run docker as a regular user, you need to add your user to the docker group and log in again:

user@host:~$ sudo adduser `id -un` docker
user@host:~$ sudo su `id -un`

Initial configuration

django-ca requires some initial configuration (like where to find the PostgreSQL server) to run and the domain name you have set up above.

Django requires a SECRET_KEY to run, and it should be shared between the Celery worker and the Gunicorn instance. Generate a sufficiently long secret key and set it as SECRET_KEY below:

user@host:~$ cat /dev/urandom | tr -dc '[:alnum:][:print:]' | tr -d '"\\ ' | fold -w \
>    ${1:-50} | head -n 1

To provide initial configuration (and any later configuration), create a file called localsettings.yaml and add at least these settings (and adjust to your configuration):

localsettings.yaml
# Configuration for django-ca. You can add/update settings here and then
# restart your containers.

# Secret key used for session handling etc, must be a long, random string.
# Generate with:
#
#   cat /dev/urandom | tr -dc '[:alnum:][:print:]' | tr -d '"\\ ' | fold -w ${1:-50} | head -n 1
#
SECRET_KEY: "changeme"

# Where to find your database
DATABASES:
    default:
        ENGINE: django.db.backends.postgresql
        HOST: "postgres"
        PORT: 5432
        PASSWORD: "changeme"

# Shared, persistent cache
CACHES:
    default:
        BACKEND: django.core.cache.backends.redis.RedisCache
        LOCATION: redis://redis:6379
        OPTIONS:
            db: "1"

# django-ca will use Celery as an asynchronous task worker
CELERY_BROKER_URL: redis://redis:6379/0

# Default hostname to use when generating CRLs and OCSP responses
CA_DEFAULT_HOSTNAME: "ca.example.com"

Please see Custom settings for a list of available settings.

Note that you can pass simple configuration variables also via environment variables prefixed with DJANGO_CA_. For example, you could also configure the broker URL with:

user@host:~$ docker run -e DJANGO_CA_CELERY_BROKER_URL=... ...

NGINX configuration

NGINX requires a configuration file, so you first need to create it. A minimal example would be:

nginx.conf
upstream django_ca_frontend {
    server frontend:8000;
}

server {
    listen       80;
    server_name  ca.example.com;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        # we don't want nginx trying to do something clever with
        # redirects, we set the Host: header above already.
        proxy_redirect off;
        proxy_pass http://django_ca_frontend;
    }
    location /static/ {
        root   /usr/share/nginx/html/;
    }
}

Recap

By now, there should be two configuration files in your local directory: localsettings.yaml configures django-ca, and nginx.conf configures NGINX itself:

user@host:~$ ls
nginx.conf localsettings.yaml

Start django-ca

After configuration, start service dependencies, django-ca itself and finally NGINX, then create an admin user and some initial certificate authorities.

Start dependencies

As mentioned before, we will start services that django-ca depends upon (like PostgreSQL) as Docker containers in this guide. In practice, you do not need the custom network setup below, unless you intend to run some of the services this way.

Create a Docker network and start PostgreSQL and Redis:

user@host:~$ docker network create django-ca
user@host:~$ docker run --name postgres --network=django-ca -e POSTGRES_PASSWORD=changeme \
>    -v pgdata:/var/lib/postgresql -d postgres
user@host:~$ docker run --name redis --network=django-ca -d redis

Choose Docker image

The Docker image is published in a Debian (the default) and an Alpine Linux based variant. In the examples below, we use the default, Debian based variant. For example, if you use 3.0.0, you can either choose:

  • mathiasertl/django-ca:3.0.0

  • mathiasertl/django-ca:3.0.0-alpine

The above images are updated if packaging issues or security vulnerabilities in dependencies are discovered. If you want to be sure that you can download the exact same image later, the same tags are also published with a datestamp suffix, e.g. mathiasertl/django-ca:3.0.0-20251231.

Changed in version 2.5.0: Docker images now use a datestamp (e.g. “20251231”) as suffix, instead of an increasing integer.

Added in version 2.3.0: Docker images are now also published to the GitHub container registry at ghcr.io.

Starting with 2.3.0, GitHubs container registry also stores the same django-ca images:

  • ghcr.io/mathiasertl/django-ca:3.0.0

  • ghcr.io/mathiasertl/django-ca:3.0.0-20251231

  • ghcr.io/mathiasertl/django-ca:3.0.0-alpine

  • ghcr.io/mathiasertl/django-ca:3.0.0-alpine-20251231

Verify attestations

Added in version 2.3.0: Docker image attestations where added. Earlier images do not have attestations.

user@host:~$ gh attestation verify \
>    oci://index.docker.io/mathiasertl/django-ca:3.0.0 \
>    -R mathiasertl/django-ca
...
✓ Verification succeeded
...

Start django-ca

django-ca (usually) consists of three containers (using the same image):

  1. A celery beat daemon for periodic tasks.

  2. A Celery worker to handle asynchronous tasks

  3. A Gunicorn-based WSGI server for HTTP endpoints

You thus need to start three containers with slightly different configuration:

user@host:~$ docker run -d \
>    -e DJANGO_CA_STARTUP_COLLECTSTATIC=0 \
>    -e DJANGO_CA_STARTUP_WAIT_FOR_CONNECTIONS=postgres:5432 \
>    -v `pwd`/localsettings.yaml:/usr/src/django-ca/ca/conf/localsettings.yaml \
>    -v backend_ca_dir:/var/lib/django-ca/certs/ \
>    -v shared_ca_dir:/var/lib/django-ca/certs/ca/shared/ \
>    -v ocsp_key_dir:/var/lib/django-ca/certs/ocsp/ \
>    --name=beat --network=django-ca \
>    mathiasertl/django-ca:3.0.0 \
>    celerybeat.sh -l warning
user@host:~$ docker run -d \
>    -e DJANGO_CA_STARTUP_COLLECTSTATIC=0 \
>    -e DJANGO_CA_STARTUP_GENERATE_CRLS=0 \
>    -e DJANGO_CA_STARTUP_GENERATE_OCSP_KEYS=0 \
>    -e DJANGO_CA_STARTUP_MIGRATE=0 \
>    -e DJANGO_CA_STARTUP_WAIT_FOR_CONNECTIONS=postgres:5432 \
>    -e DJANGO_CA_STARTUP_WAIT_FOR_SECRET_KEY_FILE=1 \
>    -v `pwd`/localsettings.yaml:/usr/src/django-ca/ca/conf/localsettings.yaml \
>    -v backend_ca_dir:/var/lib/django-ca/certs/ \
>    -v shared_ca_dir:/var/lib/django-ca/certs/ca/shared/ \
>    -v ocsp_key_dir:/var/lib/django-ca/certs/ocsp/ \
>    --name=backend --network=django-ca \
>    mathiasertl/django-ca:3.0.0 \
>    celery.sh -l warning
user@host:~$ docker run -d \
>    -e DJANGO_CA_STARTUP_GENERATE_CRLS=0 \
>    -e DJANGO_CA_STARTUP_GENERATE_OCSP_KEYS=0 \
>    -e DJANGO_CA_STARTUP_MIGRATE=0 \
>    -e DJANGO_CA_STARTUP_WAIT_FOR_CONNECTIONS=postgres:5432 \
>    -e DJANGO_CA_STARTUP_WAIT_FOR_SECRET_KEY_FILE=1 \
>    -v `pwd`/localsettings.yaml:/usr/src/django-ca/ca/conf/localsettings.yaml \
>    -v static:/usr/share/django-ca/static/ \
>    -v frontend_ca_dir:/var/lib/django-ca/certs/ \
>    -v shared_ca_dir:/var/lib/django-ca/certs/ca/shared/ \
>    -v ocsp_key_dir:/var/lib/django-ca/certs/ocsp/ \
>    -v nginx_config:/usr/src/django-ca/nginx/ \
>    --name=frontend --network=django-ca \
>    mathiasertl/django-ca:3.0.0

You can also use different versions of the Docker image, including images based on Alpine Linux. Please see the Docker Hub page for more information about available tags.

Start NGINX

NGINX unfortunately will crash if you haven’t started django-ca first (due to the name of the frontend container not resolving yet). So you have to start NGINX after the frontend container:

user@host:~$ docker run --name nginx --network=django-ca -p 80:80 \
>    -v static:/usr/share/nginx/html/static/ \
>    -v `pwd`/nginx.conf:/etc/nginx/conf.d/default.conf -d nginx

You are now able to view the admin interface at http://ca.example.com/admin/. You cannot log in yet, as you haven’t created a user yet.

Verify setup

To verify that everything worked correctly, check if all containers started successfully. You should see that all containers you just started (postgres, redis, beat, backend and frontend) are up and running:

user@host:~$ docker ps
CONTAINER ID   IMAGE  COMMAND                  CREATED         STATUS         PORTS                                 NAMES
...            nginx  "/docker-entrypoint.…"   5 minutes ago   Up ...         0.0.0.0:80->80/tcp, [::]:80->80/tcp   nginx
...

You can also run the deployment checks for your setup:

user@host:~$ docker exec beat manage check --deploy
System check identified no issues (2 silenced)
user@host:~$ docker exec backend manage check --deploy
System check identified no issues (2 silenced)
user@host:~$ docker exec frontend manage check --deploy
System check identified no issues (2 silenced)

Create admin user and set up CAs

It’s finally time to create a user for the admin interface and some certificate authorities.

user@host:~$ docker exec -it backend manage createsuperuser
...
user@host:~$ docker exec backend manage init_ca --path-length=1 Root CN=Root
<root-serial>
user@host:~$ docker exec backend manage init_ca \
>    --path=ca/shared/ --parent=Root \
>    Intermediate CN=Intermediate
<intermediate-serial>

Use your CA

Usage is very similar to the usage in Docker Compose, for example to sign a certificate:

user@host:~$ docker exec -i backend manage sign_cert --ca=Intermediate \
>    --subject="CN=example.com"
Please paste the CSR:
-----BEGIN CERTIFICATE REQUEST-----
...

Build your own container

If you want to build the container by yourself, simply clone the repository from GitHub and execute:

$ DOCKER_BUILDKIT=1 docker build -t django-ca .

Update

When updating, first check the ChangeLog for any breaking changes. Under Update you’ll also find notes on any manual steps you might need to take.

Docker does not support updating containers very well on its own. Upgrading them means stopping and removing the old container and starting a new one with the same options:

user@host:~$ docker ps
CONTAINER ID   IMAGE                          ...    NAMES
...            mathiasertl/django-ca:1.28.0   ...    frontend
...            mathiasertl/django-ca:1.28.0   ...    backend
user@host:~$ docker kill frontend backend
user@host:~$ docker rm frontend backend
user@host:~$ docker run ... mathiasertl/django-ca:1.29.0 frontend
user@host:~$ docker run ... mathiasertl/django-ca:1.29.0 backend

The docker run command must use at least the same volume options as the previous command.