Overview
Docker Compose provides a reproducible deployment method that isolates the C2 server and Nginx redirector in containers with automatic dependency management and environment configuration.
The Docker deployment exposes port 443 to the host network. Ensure your firewall is configured to restrict access to authorized agents only.
Architecture
The Docker Compose stack consists of two services:
| Service | Container | Port | Purpose |
|---|
c2-server | python:3.11-slim | 8443 (internal) | FastAPI beacon handler |
nginx | nginx:stable-alpine | 443, 80 (host) | TLS termination and traffic filtering |
Both containers communicate over an isolated bridge network (c2-internal). The agent connects to port 443, which forwards valid beacon requests to the backend on port 8443.
Prerequisites
Install Docker
Install Docker Engine and Docker Compose plugin:sudo apt update
sudo apt install docker.io docker-compose-plugin -y
Add user to docker group
Grant your user permission to run Docker commands without sudo:sudo usermod -aG docker c2server
newgrp docker
Verify your UID is 1000 (required for volume permissions):Expected output: uid=1000 Generate TLS certificates
Create self-signed certificates with SAN extension:cd /home/c2server/c2-framework
mkdir -p certs
openssl req -x509 -newkey rsa:4096 \
-keyout certs/server.key -out certs/server.crt \
-days 365 -nodes \
-subj "/CN=c2.lab.internal" \
-addext "subjectAltName=DNS:c2.lab.internal,IP:192.168.100.10"
See TLS Certificates for detailed certificate generation. Set file permissions
The container runs as UID 1000. Set correct permissions on mounted paths:chmod 644 certs/server.crt
chmod 640 certs/server.key
chmod 755 logs/
chmod 644 common/config.py
Docker Compose Configuration
The docker-compose.yml file at the project root defines the full stack:
services:
c2-server:
build:
context: . # build from project root where Dockerfile lives
dockerfile: Dockerfile
container_name: c2-server
restart: unless-stopped
volumes:
- ./certs:/app/certs:ro # mount certs read-only
- ./common/config.py:/app/common/config.py:ro # mount live config
- ./logs:/app/logs # persist logs outside container
expose:
- "8443" # only reachable by nginx service, not host
networks:
- c2-internal
environment:
- LAB_MODE=1
- BEHIND_NGINX=1
nginx:
build:
context: ./redirector
dockerfile: Dockerfile.nginx
container_name: c2-nginx
restart: unless-stopped
ports:
- "443:443" # expose HTTPS to host network
- "80:80" # HTTP redirect to HTTPS
volumes:
- ./redirector/nginx_docker.conf:/etc/nginx/conf.d/c2.conf:ro
- ./certs:/etc/nginx/certs:ro # nginx reads certs from this path
- ./redirector/site:/var/www/html:ro # fake website root
- /dev/null:/etc/nginx/conf.d/default.conf:ro
- ./redirector/nginx_main.conf:/etc/nginx/nginx.conf:ro
depends_on:
- c2-server
networks:
- c2-internal
networks:
c2-internal:
driver: bridge # isolated network — nginx reaches server by service name
Key Configuration Points
- Volume mounts: Certificates, logs, and config are mounted from the host to persist data and allow live configuration updates
- Exposed vs published ports: Port 8443 is exposed only to the internal network; port 443 is published to the host
- Environment variables:
BEHIND_NGINX=1 tells the server to trust X-Real-IP headers from the reverse proxy
- Restart policy:
unless-stopped ensures containers restart after reboot
Deployment Steps
Verify nginx_docker.conf uses container paths
Confirm the Nginx config references Docker-mounted certificate paths:ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
And uses the Docker service name for proxying:proxy_pass http://c2-server:8443/beacon;
Build and start the stack
From the project root:cd /home/c2server/c2-framework
docker compose up -d
Expected output:[+] Running 2/2
✔ Container c2-server Started
✔ Container c2-nginx Started
Verify container status
Check both containers are running:Expected output:NAME IMAGE STATUS
c2-nginx c2-nginx Up
c2-server c2-server Up
Test beacon endpoint
A 400 response confirms Nginx forwarded the request to the backend:curl -k --resolve c2.lab.internal:443:127.0.0.1 \
-X POST https://c2.lab.internal/beacon \
-H 'Content-Type: application/octet-stream' \
-d 'test' -o /dev/null -w '%{http_code}\n'
Expected: 400 (backend rejected invalid protocol message)A 502 response means the c2-server container is not running. Check logs with docker compose logs c2-server.
Verify fake website is served
Confirm other paths serve the decoy site:curl -k --resolve c2.lab.internal:443:127.0.0.1 \
https://c2.lab.internal/ -o /dev/null -w '%{http_code}\n'
Expected: 200
Managing the Stack
View Logs
View live logs from both containers:
View logs from a specific service:
docker compose logs c2-server
docker compose logs nginx
Restart Services
Restart both containers:
Restart a specific service:
docker compose restart c2-server
Stop the Stack
Stop containers but preserve networks and volumes:
Stop and remove all resources:
docker compose down --remove-orphans
Rebuild After Code Changes
Rebuild images and restart:
docker compose up -d --build
Troubleshooting
| Symptom | Cause | Fix |
|---|
502 Bad Gateway | c2-server not running | Check docker compose logs c2-server for errors |
404 on /beacon | Nginx config mismatch | Verify proxy_pass http://c2-server:8443/beacon in nginx_docker.conf |
| Permission denied on cert | UID mismatch | Confirm id c2server returns uid=1000 |
| Container restart loop | Missing config.py | Copy from common/config_example.py |
| No log files | No requests received | Logs created on first agent beacon |
| Volume mount error | Path doesn’t exist | Create logs/ and certs/ directories |
Security Considerations
- Never commit
certs/server.key to git
- Use
docker compose down before backup to ensure database consistency
- Logs in
logs/ directory contain agent IP addresses and session metadata
- The
c2-internal network is isolated from the host — only port 443 is exposed
Next Steps