rCTF Docs
Overview

Docker instancer

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

The Docker instancer is a Python FastAPI service that manages Docker containers, networks, volumes, Redis instance locks, Redis expirations, and Traefik labels. It’s the lightweight option for per-team challenge instances and is what the bundled Compose stack ships out of the box.

For the participant lifecycle, the common instancerConfig fields, and endpoint-kind semantics, see Instancer.

Deployment#

The bundled deployment files live under 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
instancerProvider:
name: instancer/docker-instancer
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
instancerProvider:
name: instancer/docker-instancer
options:
apiUrl: http://127.0.0.1:12237
authToken: <shared-secret>

The Traefik TLS config expects these files:

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

Docker daemon address pool#

Every instanced challenge creates its own user-defined Docker network. Docker carves those networks out of the daemon’s default address pool, which on a fresh install is only 172.17.0.0/16 plus a small chunk of 192.168.0.0/16. That’s enough for ~30 networks before docker network create starts failing with could not find an available, non-overlapping IPv4 address pool. For a production CTF this runs out fast.

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 carves 100.64.0.0/10 (the CGNAT range, safe to use internally) into /24 subnets, giving you 16384 Docker networks. That’s enough for ~8192 concurrent instances in the worst case (each challenge typically creates two networks). Add more default-address-pools entries if you need still more room.

Restart Docker after editing the file (systemctl restart docker) so it picks up the new pool. Networks already created on the old pool keep working but stick with 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:

FieldsPurpose
imageDocker image. This is required for each explicit service.
hostname, environment, command, entrypoint, working_dir, userProcess and runtime metadata.
networks, network_mode, dns, dns_opt, dns_search, extra_hosts, exposeNetwork configuration.
volumes, tmpfs, shm_size, read_onlyFilesystem configuration.
privileged, security_opt, cap_add, cap_dropContainer privilege controls.
mem_limit, cpus, pids_limit, ulimits, sysctlsResource and kernel limits.
healthcheck, labels, restartHealth, 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.

Esc

Start typing to search the docs.