Skip to main content

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:
ServiceContainerPortPurpose
c2-serverpython:3.11-slim8443 (internal)FastAPI beacon handler
nginxnginx:stable-alpine443, 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

1

Install Docker

Install Docker Engine and Docker Compose plugin:
sudo apt update
sudo apt install docker.io docker-compose-plugin -y
2

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):
id c2server
Expected output: uid=1000
3

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.
4

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

1

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;
2

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
3

Verify container status

Check both containers are running:
docker compose ps
Expected output:
NAME        IMAGE              STATUS
c2-nginx    c2-nginx           Up
c2-server   c2-server          Up
4

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.
5

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:
docker compose logs -f
View logs from a specific service:
docker compose logs c2-server
docker compose logs nginx

Restart Services

Restart both containers:
docker compose restart
Restart a specific service:
docker compose restart c2-server

Stop the Stack

Stop containers but preserve networks and volumes:
docker compose down
Stop and remove all resources:
docker compose down --remove-orphans

Rebuild After Code Changes

Rebuild images and restart:
docker compose up -d --build

Troubleshooting

SymptomCauseFix
502 Bad Gatewayc2-server not runningCheck docker compose logs c2-server for errors
404 on /beaconNginx config mismatchVerify proxy_pass http://c2-server:8443/beacon in nginx_docker.conf
Permission denied on certUID mismatchConfirm id c2server returns uid=1000
Container restart loopMissing config.pyCopy from common/config_example.py
No log filesNo requests receivedLogs created on first agent beacon
Volume mount errorPath doesn’t existCreate 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

Build docs developers (and LLMs) love