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.
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-servicesnetwork
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 thedockergroup:sudo usermod -aG docker $USERthen 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=1and check logs for rule IDs, then addSecRuleRemoveByIdentries to your custom conf. - Let's Encrypt fails — verify DNS for
SERVER_NAMEresolves 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-servicesnetwork withdocker network inspect bw-services. - High memory usage — each nginx worker loads ModSecurity + CRS. Set
WORKER_PROCESSES=2in the bunkerweb environment if you are on a low-RAM VPS.
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
Manage Secrets with Ansible Vault
Encrypt Ansible secrets with AES-256 using ansible-vault: encrypt files and inline vars, automate with password files, and isolate group-level secrets with vault IDs.
AppArmor Explained
Learn how AppArmor profiles work, how to switch between enforce and complain mode, create new profiles, and diagnose access denials on Ubuntu, Debian, and Arch.
Apply CIS Benchmarks with OpenSCAP
Use OpenSCAP and scap-security-guide to evaluate, report on, and remediate Linux systems against CIS Benchmarks — covering install, eval, and automation.
How to Audit a Linux System with auditd
Set up auditd on Linux to track file access, syscalls, and privilege use. Covers persistent rules, file watches, ausearch, and aureport across major distros.