Docker instancer

Deploy the rCTF Docker instancer with the bundled Compose stack and configure Docker-backed instanced challenges.

Edit this page View Markdown

The Docker instancer runs per-team challenge services on a standalone Docker host. It creates the containers, networks, volumes, expiration records, and Traefik routes needed for each instance. The bundled Compose deployment includes everything required to run it.

For the participant controls, common instancerConfig fields, and available endpoint types, see Instancer.

Deployment

The deployment files are in deploy/docker-instancer/.

  • deploy/
    • docker-instancer/
      • compose.yml Traefik, Redis, and tiny-instancer service
      • .env.example Required environment variables
      • Dockerfile Python 3.14 runtime image
      • conf/ Traefik static and dynamic config
      • certs/ TLS certificate mount point

The Compose stack exposes Traefik on 80, 443, and 1337. The instancer API itself binds to 127.0.0.1:12237 on the host and to tiny-instancer:1337 inside the rctf_network Docker network.

Warning (Hosting the instancer on a separate machine)

The bundled compose.yml binds the instancer API to 127.0.0.1:12237 because the default deployment expects rCTF and the instancer to share the rctf_network Docker network on the same host. The loopback bind is intentional.

If you split the instancer onto its own host (which we recommend, since it isolates challenge workloads from the platform), change the port mapping to bind globally:

deploy/docker-instancer/compose.yml
ports:
- '12237:1337'

The API is authenticated by a shared AUTH_TOKEN, but the endpoint shouldn’t be reachable from anything other than rCTF. Put a host firewall in front of it that only allows the rCTF host’s source IP.

The environment file requires shared auth, Redis, and public instance host settings:

deploy/docker-instancer/.env
DEV_ENV=false
BIND_PORT=1337
WEB_WORKERS=1
USE_PROXY_HEADERS=true
AUTH_TOKEN=<shared-secret>
INSTANCES_HOST=instancer.example.com
REDIS_HOST=cache
REDIS_PORT_NUMBER=6379
REDIS_PASSWORD=<redis-password>
TRAEFIK_PERMANENT_REDIRECT_MIDDLEWARE_NAME=permanent-https-redirect@file

The Docker instancer stack starts from the repository root:

Terminal window
docker compose -f deploy/docker-instancer/compose.yml up -d

The same token belongs in rCTF:

rctf.d/instancer.yaml
instancers:
docker:
name: instancers/docker
options:
apiUrl: http://tiny-instancer:1337
authToken: <shared-secret>

When rCTF is outside the rctf_network Docker network, the host-mapped API URL is:

rctf.d/instancer.yaml
instancers:
docker:
name: instancers/docker
options:
apiUrl: http://127.0.0.1:12237
authToken: <shared-secret>

Traefik terminates TLS for every https and tcp-ssl endpoint. Each instance is served at <hostPrefix>-<uid>.<INSTANCES_HOST>, so a single wildcard certificate for *.<INSTANCES_HOST> covers every per-team instance. The static config (conf/tls.yml) points Traefik at two files in the mounted certs/ directory:

  • deploy/
    • docker-instancer/
      • certs/
        • fullchain.pem Certificate + intermediate chain
        • privkey.pem Private key

Instance hostnames are one label deep, so a single-level wildcard covers every <hostPrefix>. Generate the certificate with an ACME client that supports DNS-01, since wildcard certificates cannot use HTTP-01. With Certbot:

Terminal window
certbot certonly --manual --preferred-challenges dns -d '*.instancer.example.com'

Copy the issued files into the mount, matching the names Traefik expects:

Terminal window
cp /etc/letsencrypt/live/instancer.example.com/fullchain.pem deploy/docker-instancer/certs/fullchain.pem
cp /etc/letsencrypt/live/instancer.example.com/privkey.pem deploy/docker-instancer/certs/privkey.pem

Traefik loads these at startup and doesn’t watch the file config (watch: false), so restart the Traefik container after renewing or replacing the certificate.

Docker daemon address pool

Every instanced challenge creates its own Docker network, so the daemon’s default address pool can run out after roughly 30 networks. When that happens, docker network create fails with could not find an available, non-overlapping IPv4 address pool.

Expand the pool by adding the following to /etc/docker/daemon.json on the instancer host:

/etc/docker/daemon.json
{
"default-address-pools": [
{
"base": "100.64.0.0/10",
"size": 24
}
]
}

This splits the CGNAT range into 16,384 /24 networks. Add more pools if your deployment needs additional space.

Restart Docker after editing the file with systemctl restart docker. Existing networks keep their original ranges.

Docker challenge config

The Docker provider accepts a Docker Compose-like object under config:

challenge.yaml
instancerConfig:
challengeIntegrationId: web-demo
timeoutMilliseconds: 600000
extendable: true
config:
services:
app:
image: ghcr.io/example/web-demo:latest
environment:
FLAG: flag{example}
networks:
- internal
expose:
- '8080'
read_only: true
mem_limit: 128m
cpus: 0.5
pids_limit: 128
networks:
internal:
internal: true
expose:
- kind: https
hostPrefix: web-demo
containerName: app
containerPort: 8080
title: Challenge

The top-level Docker config supports services, networks, and volumes. At least one service is required after defaults are applied.

Each service supports these field groups:

Fields Purpose
image Docker image. This is required for each explicit service.
hostname, environment, command, entrypoint, working_dir, user Process and runtime metadata.
networks, network_mode, dns, dns_opt, dns_search, extra_hosts, expose Network configuration.
volumes, tmpfs, shm_size, read_only Filesystem configuration.
privileged, security_opt, cap_add, cap_drop Container privilege controls.
mem_limit, cpus, pids_limit, ulimits, sysctls Resource and kernel limits.
healthcheck, labels, restart Health, metadata, and restart behavior.

Explicit services default to a read-only root filesystem, no-new-privileges, dropped Linux capabilities, 6m memory, 1.0 CPU, 1024 PIDs, and nofile soft and hard limits of 1024.

Services may only reference networks declared under config.networks. Named volume mounts may only reference volumes declared under config.volumes. Absolute and relative bind-style mount sources aren’t checked against the volume map.

Network isolation

Services do not join Docker’s shared default bridge unless configured to do so. With no networks, expose, or network_mode, a service gets network_mode: none.

When several challenge services need to communicate, declare a user-defined network with internal: true and attach each service to it. They can then reach one another without reaching the host or internet. Use network_mode: bridge only when the service specifically needs Docker’s default bridge.

Type to search.