Docker instancer
Deploy the rCTF Docker instancer with the bundled Compose stack and configure Docker-backed instanced challenges.
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:
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:
DEV_ENV=falseBIND_PORT=1337WEB_WORKERS=1USE_PROXY_HEADERS=true
AUTH_TOKEN=<shared-secret>INSTANCES_HOST=instancer.example.com
REDIS_HOST=cacheREDIS_PORT_NUMBER=6379REDIS_PASSWORD=<redis-password>
TRAEFIK_PERMANENT_REDIRECT_MIDDLEWARE_NAME=permanent-https-redirect@fileThe Docker instancer stack starts from the repository root:
docker compose -f deploy/docker-instancer/compose.yml up -dThe same token belongs in rCTF:
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:
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:
certbot certonly --manual --preferred-challenges dns -d '*.instancer.example.com'Copy the issued files into the mount, matching the names Traefik expects:
cp /etc/letsencrypt/live/instancer.example.com/fullchain.pem deploy/docker-instancer/certs/fullchain.pemcp /etc/letsencrypt/live/instancer.example.com/privkey.pem deploy/docker-instancer/certs/privkey.pemTraefik 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 createcould 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:
{ "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
Docker challenge config
The Docker provider accepts a Docker Compose-like object under config:
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: ChallengeThe 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.