GuidesDocker Test & Demo Stacks

Docker Test & Demo Stacks

Using the flokbn Docker test environment and the closed-loop firewall demo

flokbn ships two Docker Compose stacks that simulate attack traffic against a real nginx: a fast test stack used by the e2e suites, and a slow-paced demo stack with a closed-loop firewall, Prometheus, and a provisioned Grafana dashboard. Use them to test detection parameters, validate configurations, and watch flokbn work end to end.

Architecture

Both stacks share the same topology:

  • nginx (proxy) - web server receiving simulated traffic, logs to /var/log/nginx/access.log
  • Filebeat - ships logs to flokbn via the Lumberjack protocol
  • flokbn - runs in live mode with detection enabled; HTTP endpoints /stats, /bans, and Prometheus /metrics on port 8666
  • traffic clients (45 in the test stack, 44 in the demo) - spread across 5 Docker networks: four attack clusters (net1-net4) plus one slow negative-control client (172.30.99.32) that must never be banned

Ports

PortServicePublished to hostPurpose
9000flokbnyes (9000:9000)Lumberjack ingest (Filebeat connects here)
8666flokbnyes (8666:8666)HTTP stats: GET /stats, GET /bans, GET /metrics
80proxy (nginx)container-internalTarget for the simulated clients
9090prometheusyes (monitoring overlay)Prometheus UI / API
3000grafanayes (monitoring overlay)Grafana dashboard

The configs bind statsListen = "0.0.0.0:8666" so the published port is reachable from the host across the isolated compose network. That 0.0.0.0 bind is a documented demo-only exception - outside Docker, bind the stats server to localhost.

Quick Start (test stack)

From the repository root:

docker compose -f docker-compose.test.yml up --build

Watch detections in a separate terminal (flokbn logs leveled progress lines to stderr; for machine-readable data query http://localhost:8666/stats):

docker compose -f docker-compose.test.yml logs -f flokbn

Check the ban list and jail state:

docker compose -f docker-compose.test.yml exec flokbn cat /data/blocklist.txt
docker compose -f docker-compose.test.yml exec flokbn cat /data/jail.json

The jail file uses a tiered cell structure. See Internals for the data model.

Stop the environment:

docker compose -f docker-compose.test.yml down       # stop containers
docker compose -f docker-compose.test.yml down -v    # also remove volumes

Jail state persists

The jail file lives in the flokbn_data volume and survives restarts: a plain down/up reloads old bans. Use down -v for a clean run.

Demo: closed-loop firewall with monitoring

The demo stack adds enforcement and observability on top of the same topology.

Not an actual firewall

The demo does not run a real firewall (no iptables/nftables). Enforcement is a deliberately simple deny mechanism that stands in for one: a small poller inside the nginx container fetches GET /bans from flokbn every 2 s and renders the CIDRs into nginx deny rules, so banned clients get HTTP 403. In a real deployment the same /bans endpoint (or the ban file) would drive iptables, nftables, or your WAF - the signal flow is identical, only the enforcement point changes.

Denied requests are excluded from the shipped log, so the sliding window decays, the ban expires, traffic reappears and gets re-banned - the loop closes. Prometheus scrapes flokbn (GET /metrics) and an nginx log exporter; Grafana is fully provisioned with a 14-panel dashboard.

Signal flow

clients: net1-net4 (attack pacing) + 172.30.99.32 (negative control)
   │  HTTP requests
   ▼
nginx proxy ──── 403 for CIDRs in deny.conf (denied requests are NOT logged)
   │  access.log (only requests that got through)
   ▼
Filebeat ─── Lumberjack protocol, port 9000
   ▼
flokbn live ─── sliding windows → clustering → jail (escalating stages)
   │
   ├──► /data/jail.json + /data/blocklist.txt   (files)
   └──► HTTP :8666 ── GET /stats · GET /bans · GET /metrics
            │                │
            │                │  banpoller (inside the nginx container)
            │                │  polls /bans every 2 s, writes deny.conf,
            │                │  reloads nginx ──► loop closed
            │                ▼
            │           nginx deny.conf
            ▼
      Prometheus :9090 ──► Grafana :3000   (monitoring overlay)

While it runs, watch the loop from the host: curl localhost:8666/stats for the JSON snapshot of the last iteration, curl localhost:8666/bans for exactly what the poller sees (both return 503 until the first detection iteration completes).

docker compose -f docker-compose.demo.yml \
               -f docker-compose-firewall-with-monitoring.demo.yml \
               up --build -d

Then open:

Traffic is paced so the four attack clusters cross the detection threshold at clearly different times - bans land staggered across the first two minutes:

TimeBanned CIDRCluster
~20 s172.16.16.32/27net4, 32 IPs (main attack)
~50 s172.16.3.32/28net3, 5 IPs
~79 s172.16.1.32/28 (or /30)net1, 4 IPs
~113 s172.16.2.32/28 (or /31)net2, 2 IPs

The pacing comes from the demo config’s min_size=450: time-to-ban is roughly 450 divided by the cluster’s request rate. The negative-control client (172.30.99.32, 1 request per 3 s) stays far below every threshold and keeps humming with 200s throughout - it must never be banned. Stage-1 bans expire after 10 minutes, traffic returns, and re-detection escalates repeat offenders to longer stages - the dashboard’s ban timeline shows the cycle per CIDR.

The monitoring overlay is pacing-neutral; it also composes with the fast test base (-f docker-compose.test.yml -f docker-compose-firewall-with-monitoring.demo.yml), which is what the firewall e2e suite uses. The test and demo stacks pin the same subnets, so only one can run at a time.

Expected Results (test stack)

With the fast pacing and low thresholds of docker-test-config.toml, all four attack networks are detected and jailed within seconds. Detections land on the balanced subtree of each cluster rather than the full /24 (clustering only reports a node whose children are evenly loaded), so expect CIDRs like:

NetworkTypical detectionClients
net1 (172.16.1.0/24)172.16.1.32/30 or /284
net2 (172.16.2.0/24)172.16.2.32/31 or /282
net3 (172.16.3.0/24)172.16.3.32/285
net4 (172.16.16.0/24)172.16.16.32/27 (+ .64 via the coarser sets)33
net5 (172.30.99.0/24)never detected1 (negative control)

Test Configuration

The test stack mounts docker-test-config.toml, the demo stack docker-test-config.demo.toml. The structure (abridged):

[global]
jailFile = "/data/jail.json"
banFile = "/data/blocklist.txt"

[live]
port = "9000"
statsListen = "0.0.0.0:8666"

[live.general_detection]
slidingWindowMaxTime = "5m"
slidingWindowMaxSize = 10000
sleepBetweenIterations = 1
clusterArgSets = [
    [50, 24, 32, 0.2],
]
useForJail = [true]

[live.aggressive_detection]
slidingWindowMaxTime = "2m"
slidingWindowMaxSize = 5000
sleepBetweenIterations = 1
clusterArgSets = [
    [10, 20, 28, 0.15],
    [20, 16, 24, 0.2],
]
useForJail = [true, true]

These parameters are tuned for quick detection in the test environment; the demo variant uses min_size 450 and depth >= 24 in every set to pace the bans and keep detections per-network. Production values should be higher. See Clustering for tuning guidance.

Edit without rebuilding (the config is bind-mounted, read at startup):

nano docker-test-config.toml
docker compose -f docker-compose.test.yml restart flokbn
docker compose -f docker-compose.test.yml logs -f flokbn

Building Custom Images

From the repository root (the Dockerfile lives in the flokbn/ subdirectory):

docker build -t flokbn:latest ./flokbn

The Dockerfile uses a multi-stage build (Go build, then a small Alpine runtime image).

Running Static Mode in Docker

docker run -v /var/log/nginx:/logs flokbn:latest \
  static --logfile /logs/access.log \
  --clusterArgSets 1000,24,32,0.1 --plain

With config file:

docker run -v /etc/flokbn:/config -v /var/log:/logs \
  flokbn:latest static --config /config/flokbn.toml --plain

Production Docker Deployment

# docker-compose.prod.yml
version: '3.8'
services:
  flokbn:
    image: flokbn:latest
    restart: always
    ports:
      - "8080:8080"            # Lumberjack ingest ([live] port)
      - "127.0.0.1:8666:8666"  # HTTP stats (statsListen), host-local only
    volumes:
      - /etc/flokbn/config.toml:/config/config.toml:ro
      - /var/lib/flokbn:/data
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G

For better performance, use network_mode: host to eliminate Docker network overhead.

Troubleshooting

No Detections

COMPOSE="docker compose -f docker-compose.test.yml"
$COMPOSE ps                                            # All containers running?
$COMPOSE logs clients_net1                             # Attack clients working?
$COMPOSE exec proxy tail -f /var/log/nginx/access.log  # Logs being generated?
$COMPOSE exec filebeat filebeat test output            # Filebeat connected?

If bans existed before a restart, remember the jail persists in the flokbn_data volume - a stale wide ban suppresses all traffic and with it any new detections. down -v resets it.

Container Crashes

docker compose -f docker-compose.test.yml ps -a
docker compose -f docker-compose.test.yml logs --tail=100 flokbn

Permission Issues

docker compose -f docker-compose.test.yml exec flokbn ls -la /data