$linuxjunkies
>

Install BunkerWeb for nginx-based WAF

Deploy BunkerWeb as an nginx-based WAF using Docker Compose, configure ModSecurity with OWASP CRS, enable bot blocking, and verify malicious traffic is blocked.

AdvancedUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

Before you start

  • A Linux server with a public IP and ports 80/443 open in your firewall
  • Docker Engine 24+ and Docker Compose v2 plugin installed
  • A domain name with DNS A record pointing to your server
  • Root or sudo access, and your user in the docker group

BunkerWeb is an open-source, nginx-based Web Application Firewall that ships ModSecurity, bot detection, rate limiting, and a plugin API in a single container image. It sits in front of your application stack and enforces security rules before a request ever reaches your app. This guide walks through a production-ready Docker Compose deployment, enabling ModSecurity with OWASP CRS, loading the antibot plugin, and verifying that malicious traffic is actually blocked.

Architecture Overview

BunkerWeb runs as a container (bunkerity/bunkerweb) alongside a scheduler container (bunkerity/bunkerweb-scheduler) that manages configuration and plugin lifecycle. Your app containers sit behind BunkerWeb on an internal Docker network. Externally, only ports 80 and 443 are exposed.

  • bunkerweb — the nginx-based reverse proxy / WAF engine
  • bunkerweb-scheduler — watches config, runs jobs, manages Let's Encrypt
  • bunkerweb-ui (optional) — web dashboard for rule management
  • Your app containers — only reachable on the internal bw-services network

Prerequisites and System Requirements

You need Docker Engine 24+ and Docker Compose v2 (the docker compose plugin, not the legacy docker-compose binary). BunkerWeb's scheduler requires write access to a shared volume and a working DNS resolver inside the Docker network. A server with at least 1 GB RAM is recommended; ModSecurity with OWASP CRS adds roughly 150–250 MB RSS per worker.

Install Docker on Debian/Ubuntu

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt-get update && sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Install Docker on Fedora / RHEL 9 / Rocky 9

sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker

Install Docker on Arch

sudo pacman -Sy docker docker-compose
sudo systemctl enable --now docker

Directory Layout

Keep all BunkerWeb configuration in one directory so it is easy to back up and version-control.

mkdir -p ~/bunkerweb/{data,letsencrypt}
cd ~/bunkerweb

Docker Compose File

Create docker-compose.yml. The environment variables are BunkerWeb's primary configuration mechanism — no separate nginx.conf editing required.

cat > docker-compose.yml << 'EOF'
services:

  bunkerweb:
    image: bunkerity/bunkerweb:1.5.10
    restart: unless-stopped
    ports:
      - "80:8080"
      - "443:8443"
    environment:
      - SERVER_NAME=example.com
      - API_WHITELIST_IP=127.0.0.0/8 10.20.30.0/24
    networks:
      - bw-universe
      - bw-services
    volumes:
      - bw-data:/data

  bunkerweb-scheduler:
    image: bunkerity/bunkerweb-scheduler:1.5.10
    restart: unless-stopped
    depends_on:
      - bunkerweb
    environment:
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - bw-data:/data
    networks:
      - bw-universe

  myapp:
    image: nginx:alpine          # replace with your actual app image
    restart: unless-stopped
    networks:
      - bw-services
    labels:
      - "bunkerweb.SERVER_NAME=example.com"
      - "bunkerweb.USE_REVERSE_PROXY=yes"
      - "bunkerweb.REVERSE_PROXY_URL=/"
      - "bunkerweb.REVERSE_PROXY_HOST=http://myapp:80"
      - "bunkerweb.USE_MODSECURITY=yes"
      - "bunkerweb.USE_MODSECURITY_CRS=yes"
      - "bunkerweb.USE_BAD_BEHAVIOR=yes"
      - "bunkerweb.BAD_BEHAVIOR_BAN_TIME=3600"
      - "bunkerweb.USE_ANTIBOT=captcha"
      - "bunkerweb.ANTIBOT_URI=/challenge"
      - "bunkerweb.USE_LIMIT_REQ=yes"
      - "bunkerweb.LIMIT_REQ_RATE=20r/s"
      - "bunkerweb.USE_GZIP=yes"
      - "bunkerweb.HTTPS_PROTOCOLS=TLSv1.2 TLSv1.3"

volumes:
  bw-data:

networks:
  bw-universe:
    ipam:
      driver: default
      config:
        - subnet: 10.20.30.0/24
  bw-services:
EOF

Replace example.com with your actual domain and update API_WHITELIST_IP to match the subnet you defined. BunkerWeb discovers app configuration by reading Docker container labels via the scheduler — no config file reload needed when you add or remove services.

ModSecurity and OWASP CRS Configuration

The labels USE_MODSECURITY=yes and USE_MODSECURITY_CRS=yes enable ModSecurity 3 in detection mode by default. To switch to full blocking mode, add these labels to your app service:

      - "bunkerweb.MODSECURITY_SEC_RULE_ENGINE=On"
      - "bunkerweb.USE_MODSECURITY_CRS=yes"
      - "bunkerweb.MODSECURITY_CRS_PARANOIA_LEVEL=2"

Paranoia Level 1 is the CRS default and has very few false positives. Level 2 catches more attack patterns but may block legitimate complex requests (e.g., long JSON bodies). Start at PL1 in production and tune upward.

Custom ModSecurity Rules

Mount a custom rules file for site-specific overrides. Create custom-modsec.conf:

cat > custom-modsec.conf << 'EOF'
# Disable a CRS rule causing false positives for your app's API
SecRuleRemoveById 920350
# Block requests with a specific bad User-Agent string
SecRule REQUEST_HEADERS:User-Agent "@contains MaliciousBot" \
  "id:9000001,phase:1,deny,status:403,msg:'Blocked bad UA'"
EOF

Mount it into the bunkerweb container by adding to the bunkerweb service's volumes block:

      - ./custom-modsec.conf:/etc/bunkerweb/custom-modsec.conf:ro

Then add the label to your app service: bunkerweb.MODSECURITY_CUSTOM_RULES_FILE=/etc/bunkerweb/custom-modsec.conf.

Bot Blocking with the Antibot Plugin

The USE_ANTIBOT=captcha label activates BunkerWeb's built-in antibot challenge. Clients that don't solve the challenge are blocked. Available modes are cookie, javascript, captcha (hCaptcha), and recaptcha. For hCaptcha, add your site and secret keys:

      - "bunkerweb.USE_ANTIBOT=captcha"
      - "bunkerweb.ANTIBOT_HCAPTCHA_SITEKEY=your-hcaptcha-sitekey"
      - "bunkerweb.ANTIBOT_HCAPTCHA_SECRET=your-hcaptcha-secret"

For straightforward bot scraper blocking without user-visible challenges, combine USE_BAD_BEHAVIOR=yes with CrowdSec or the greylist plugin instead of antibot. The USE_BAD_BEHAVIOR setting tracks anomaly scoring across requests and bans IPs that exceed the threshold within a time window.

Starting the Stack

docker compose up -d
docker compose logs -f bunkerweb-scheduler

The scheduler will detect the myapp container labels, generate the nginx configuration, and signal BunkerWeb to reload. This takes 10–30 seconds on first boot.

Verification

Check BunkerWeb is serving traffic

curl -I http://example.com

You should receive a 200 (or your app's expected response). A 444 or connection refused means the scheduler hasn't applied config yet — wait a few seconds and retry.

Test ModSecurity blocks SQL injection

curl -i "http://example.com/?id=1'+OR+'1'='1"
# Expect: HTTP/1.1 403 Forbidden

Test bot challenge is served

curl -i -A "python-requests/2.31" http://example.com/
# Expect: 200 with challenge page HTML, or 403 depending on antibot mode

Inspect BunkerWeb logs

docker compose logs bunkerweb | grep -E "(BLOCK|DENIED|403)" | tail -30

Enabling HTTPS with Let's Encrypt

Add these labels to your app service. BunkerWeb's scheduler handles certificate issuance and renewal automatically via its built-in ACME client:

      - "bunkerweb.AUTO_LETS_ENCRYPT=yes"
      - "[email protected]"

Ensure ports 80 and 443 are reachable from the internet before enabling this. The certificate is stored in the bw-data volume and renewed before expiry.

Troubleshooting

  • Scheduler shows "permission denied" on Docker socket — your user or the container must have access to /var/run/docker.sock. Add your user to the docker group: sudo usermod -aG docker $USER then re-login.
  • All requests return 403 — ModSecurity paranoia level is too high or a CRS rule is triggering on legitimate traffic. Drop to MODSECURITY_CRS_PARANOIA_LEVEL=1 and check logs for rule IDs, then add SecRuleRemoveById entries to your custom conf.
  • Let's Encrypt fails — verify DNS for SERVER_NAME resolves to your server's public IP and that port 80 is reachable from the internet (not firewalled). Check the scheduler log for ACME error details.
  • App not proxied after adding labels — the scheduler only detects containers on the same Docker socket and network. Confirm the app is on the bw-services network with docker network inspect bw-services.
  • High memory usage — each nginx worker loads ModSecurity + CRS. Set WORKER_PROCESSES=2 in the bunkerweb environment if you are on a low-RAM VPS.
tested on:Ubuntu 24.04Debian 12Fedora 40Rocky 9

Frequently asked questions

Can BunkerWeb be used without Docker, installed directly on the host?
Yes. BunkerWeb provides packages for Debian/Ubuntu and a Linux standalone installer, but the Docker Compose method is the recommended and best-supported deployment path. Direct installs require manual systemd unit configuration for the scheduler.
How do I whitelist a legitimate IP that is being blocked by ModSecurity?
Add a SecRule to your custom-modsec.conf using `SecRule REMOTE_ADDR "@ipMatch 1.2.3.4" "id:9000010,phase:1,allow,ctl:ruleEngine=Off"`, or use BunkerWeb's WHITELIST_IP label on the app service.
Does BunkerWeb support multiple virtual hosts behind the same instance?
Yes. Deploy multiple app containers, each with their own bunkerweb.SERVER_NAME label set to a different domain. The scheduler generates separate server blocks for each.
What is the difference between USE_BAD_BEHAVIOR and USE_ANTIBOT?
USE_BAD_BEHAVIOR tracks cumulative anomaly scores per IP and bans IPs that exceed a threshold, making it invisible to users. USE_ANTIBOT presents an active challenge (JavaScript proof-of-work or CAPTCHA) to distinguish humans from bots on each new session.
How do I update BunkerWeb to a newer version?
Update the image tags in docker-compose.yml to the new version, then run `docker compose pull && docker compose up -d`. The bw-data volume preserves certificates and state across upgrades.

Related guides