Self-Host Fail2ban: Block Brute-Force Attacks in 15 Minutes
Every server on the internet faces constant brute-force attacks. SSH login attempts, WordPress login floods, SMTP auth hammering — bots are relentless. Fail2ban is your lightweight, battle-tested defense that automatically bans offending IPs after repeated failed attempts. In this guide, you'll deploy Fail2ban with Docker and protect your services in under 15 minutes.
Why Self-Host Fail2ban?
- Zero-trust defense: Don't rely solely on cloud firewalls — add a local layer
- Universal protection: Works with SSH, Nginx, Traefik, WordPress, mail servers, and more
- Lightweight: Uses minimal resources (~10MB RAM)
- Proven: 20+ years of production use, default on most Linux distributions
What You'll Need
- A VPS or server (1 vCPU, 512MB RAM minimum)
- Docker & Docker Compose installed
- Root or sudo access
- ~5 minutes of focused time
Step 1: Create the Docker Compose File
Create a directory for Fail2ban:
mkdir -p ~/fail2ban/config
cd ~/fail2ban
Create docker-compose.yml:
version: '3.8'
services:
fail2ban:
image: crazymax/fail2ban:latest
container_name: fail2ban
network_mode: host
cap_add:
- NET_ADMIN
- NET_RAW
volumes:
- ./config:/data
- /var/log:/var/log:ro
- /var/log/auth.log:/var/log/auth.log:ro
environment:
- TZ=UTC
- F2B_LOG_LEVEL=INFO
- F2B_DB_PURGE_AGE=1d
restart: unless-stopped
Key configuration:
network_mode: host— Required for iptables/nftables to workcap_add: NET_ADMIN, NET_RAW— Required for modifying firewall rules/var/log:/var/log:ro— Read access to system logs
Step 2: Configure Fail2ban
Create the main configuration:
cat > config/jail.local << 'EOF'
[DEFAULT]
# Ban IP after 3 failed attempts within 10 minutes
findtime = 10m
maxretry = 3
# Ban for 1 hour
bantime = 1h
# Use iptables
backend = auto
# Send email alerts (optional)
# destemail = [email protected]
# sender = [email protected]
# action = %(action_mwl)s
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 1h
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 3
[nginx-botsearch]
enabled = true
filter = nginx-botsearch
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 2
[traefik-auth]
enabled = true
filter = traefik-auth
port = http,https
logpath = /var/log/traefik/access.log
maxretry = 3
EOF
Create a custom filter for Traefik:
mkdir -p config/filter.d
cat > config/filter.d/traefik-auth.conf << 'EOF'
[Definition]
failregex = ^<HOST> .* "(GET|POST|PUT|DELETE).* HTTP/[0-9.]+" 401
^<HOST> .* "(GET|POST|PUT|DELETE).* HTTP/[0-9.]+" 403
ignoreregex =
EOF
Step 3: Start Fail2ban
docker compose up -d
Verify it's running:
docker compose logs --tail 20
You should see output like:
fail2ban | 2024-01-15 10:30:00,123 fail2ban.server [1]: INFO Starting Fail2ban v1.0.2
fail2ban | 2024-01-15 10:30:00,456 fail2ban.jail [1]: INFO Creating new jail 'sshd'
fail2ban | 2024-01-15 10:30:00,789 fail2ban.jail [1]: INFO Jail 'sshd' started
Step 4: Verify Protection
Check jail status:
docker exec -it fail2ban fail2ban-client status
Check specific jail (e.g., SSH):
docker exec -it fail2ban fail2ban-client status sshd
Test the ban (from another machine, try failed SSH logins):
# On another machine, run this 4 times:
ssh wronguser@your-server-ip
Check if the IP got banned:
docker exec -it fail2ban fail2ban-client status sshd
You should see the banned IP in the "Banned IP list" section.
Step 5: View and Manage Bans
List banned IPs:
docker exec -it fail2ban fail2ban-client status sshd | grep "Banned IP list"
Unban an IP manually:
docker exec -it fail2ban fail2ban-client set sshd unbanip 192.168.1.100
Check logs in real-time:
docker exec -it fail2ban tail -f /var/log/fail2ban.log
Advanced: Permanent Bans and Whitelisting
Add to config/jail.local:
[DEFAULT]
# Your home IP (never ban)
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24 your-home-ip
# Permanent ban after 3 temporary bans
bantime.increment = true
bantime.factor = 1
bantime.maxtime = 1w
bantime.multipliers = 1 2 4 8 16 32 64
Restart to apply:
docker compose restart
Integration with Cloudflare (Optional)
If using Cloudflare, ban at the edge:
cat > config/action.d/cloudflare.conf << 'EOF'
[Definition]
# Get your Cloudflare API token with Zone:Edit permissions
# Required: actionstart, actionstop, actioncheck, actionban, actionunban
actionstart =
actionstop =
actioncheck =
actionban = curl -s -X POST "https://api.cloudflare.com/client/v4/zones/<cftoken>/firewall/access_rules/rules" \
-H "Authorization: Bearer <cfapikey>" \
-H "Content-Type: application/json" \
--data '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"Fail2ban <name>"}'
actionunban = curl -s -X DELETE "https://api.cloudflare.com/client/v4/zones/<cftoken>/firewall/access_rules/rules/<ip>" \
-H "Authorization: Bearer <cfapikey>"
[Init]
cftoken = your-zone-id
cfapikey = your-api-token
EOF
Verification Checklist
- [ ] Container running:
docker ps | grep fail2ban - [ ] Jails active:
docker exec fail2ban fail2ban-client status - [ ] SSH protection working: failed logins trigger ban
- [ ] Logs visible:
docker logs fail2ban - [ ] Firewall rules applied:
sudo iptables -L -n | grep fail2ban
Next Steps
- Monitor: Set up Uptime Kuma for service monitoring
- Visualize: Add Fail2ban metrics to Grafana
- Centralize: Forward logs to a central server for analysis
- Expand: Protect WordPress, mail servers, VPN endpoints
Quick Reference
| Command | Purpose |
|---|---|
docker compose up -d | Start Fail2ban |
docker exec fail2ban fail2ban-client status | List all jails |
docker exec fail2ban fail2ban-client status sshd | SSH jail status |
docker exec fail2ban fail2ban-client set sshd unbanip IP | Unban an IP |
docker exec fail2ban fail2ban-client reload | Reload config |
Fail2ban is now protecting your server. Most brute-force attempts will be blocked automatically, and you'll have visibility into attack patterns. For a complete security stack, consider pairing this with CrowdSec for community-driven threat intelligence.
Last updated: August 2026 | Sysbrix Self-Hosting Guides