docker.recipes
Security8 min read

Docker and Firewalls (UFW, iptables)

Understand how Docker bypasses UFW and iptables, and learn to configure firewalls correctly for container security.

01The Docker Firewall Problem

Docker manipulates iptables directly to enable container networking. This means: **UFW rules are bypassed!** If you expose a container port, it's accessible from the internet even if UFW blocks that port. **Why this happens:** 1. Docker inserts rules in the DOCKER chain 2. These rules are processed before UFW's INPUT chain 3. Docker rules allow traffic directly to containers This is one of the most common security mistakes in self-hosted Docker setups.

If you're using UFW and Docker together, your containers may be exposed to the internet right now. Check your setup!

02Checking If You're Exposed

Before fixing the problem, verify your current exposure.
1# Check what's listening on all interfaces
2sudo ss -tlnp | grep docker
3
4# Check iptables rules Docker has added
5sudo iptables -L DOCKER -n -v
6
7# From another machine or phone (different network), try to access:
8# curl http://your-public-ip:exposed-port
9
10# Check UFW status
11sudo ufw status verbose
12
13# Look for exposed ports in compose files
14grep -r "ports:" docker-compose.yml
15
16# Ports bound to 0.0.0.0 are potentially exposed
17docker ps --format '{{.Ports}}' | grep 0.0.0.0

Use a tool like nmap from outside your network to verify which ports are actually accessible.

03Solution 1: Bind to Localhost

The simplest solution is to bind container ports only to localhost (127.0.0.1). Use a reverse proxy for external access.
1services:
2 # WRONG - exposed to everyone
3 database-bad:
4 image: postgres:16
5 ports:
6 - "5432:5432" # Binds to 0.0.0.0
7
8 # CORRECT - only accessible from host
9 database-good:
10 image: postgres:16
11 ports:
12 - "127.0.0.1:5432:5432" # Binds to localhost only
13
14 # Use reverse proxy for web services
15 traefik:
16 image: traefik:v3.0
17 ports:
18 - "80:80" # Only these are intentionally public
19 - "443:443"
20
21 webapp:
22 image: myapp:latest
23 # No ports exposed! Accessed via Traefik
24 labels:
25 - "traefik.enable=true"
26 - "traefik.http.routers.app.rule=Host(`app.example.com`)"

This is the recommended approach. Only expose ports that truly need to be public (usually just 80 and 443 for your reverse proxy).

04Solution 2: The DOCKER-USER Chain

Docker provides a DOCKER-USER chain for custom firewall rules that Docker won't overwrite. Add rules here to filter traffic to containers.
1# The DOCKER-USER chain is processed BEFORE Docker's rules
2# Add your filtering rules here
3
4# Allow established connections
5sudo iptables -I DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
6
7# Allow from local network
8sudo iptables -I DOCKER-USER -s 192.168.1.0/24 -j RETURN
9
10# Allow from specific IPs
11sudo iptables -I DOCKER-USER -s 1.2.3.4 -j RETURN
12
13# Drop everything else going to Docker containers
14sudo iptables -A DOCKER-USER -j DROP
15
16# Make rules persistent
17sudo apt install iptables-persistent
18sudo netfilter-persistent save
19
20# Or use a script in /etc/rc.local

Get the order right! RETURN rules must come before DROP. Test carefully to avoid locking yourself out.

05Solution 3: UFW + Docker Fix

You can configure Docker to respect UFW by preventing Docker from manipulating iptables, but this breaks container networking. A better approach uses the ufw-docker project.
1# Option A: ufw-docker (recommended)
2# Install the helper script
3sudo wget -O /usr/local/bin/ufw-docker https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
4sudo chmod +x /usr/local/bin/ufw-docker
5
6# Install the UFW rules
7sudo ufw-docker install
8
9# Now manage Docker ports through ufw-docker
10sudo ufw-docker allow webapp 80/tcp
11sudo ufw-docker allow webapp 443/tcp
12
13# List Docker rules
14sudo ufw-docker status
15
16# Option B: Disable Docker's iptables (not recommended)
17# /etc/docker/daemon.json
18{
19 "iptables": false
20}
21# WARNING: This breaks container networking!

The ufw-docker script provides the best balance of UFW integration and working container networking.

06Using Docker Networks for Isolation

Don't expose ports at all—use Docker networks and only expose through a reverse proxy.
1services:
2 # Reverse proxy - the only thing exposed
3 traefik:
4 image: traefik:v3.0
5 ports:
6 - "80:80"
7 - "443:443"
8 networks:
9 - frontend
10 volumes:
11 - /var/run/docker.sock:/var/run/docker.sock:ro
12
13 # Web app - no ports, accessed via Traefik
14 webapp:
15 image: myapp:latest
16 networks:
17 - frontend
18 - backend
19 labels:
20 - "traefik.enable=true"
21 - "traefik.http.routers.app.rule=Host(`app.example.com`)"
22
23 # Database - completely isolated
24 database:
25 image: postgres:16
26 # NO PORTS - only webapp can access via backend network
27 networks:
28 - backend
29 volumes:
30 - db_data:/var/lib/postgresql/data
31
32networks:
33 frontend:
34 backend:
35 internal: true # No external access at all
36
37volumes:
38 db_data:

07Using Cloud Provider Firewalls

If running on cloud infrastructure, use the provider's firewall as your first line of defense. These operate outside your server and can't be bypassed by Docker.
1# AWS Security Groups, Azure NSG, GCP Firewall Rules
2# These filter traffic before it reaches your server
3
4# Example: Only allow SSH and HTTPS
5# Inbound rules:
6# - Port 22/tcp from YOUR_IP (SSH)
7# - Port 80/tcp from 0.0.0.0/0 (HTTP for redirect)
8# - Port 443/tcp from 0.0.0.0/0 (HTTPS)
9# - All other inbound: DENY
10
11# For home servers behind a router:
12# Only port forward 80 and 443 to your Docker host
13# Keep all other ports closed
14
15# Verify external access (from different network)
16nmap -Pn your-public-ip

Cloud firewalls are the most reliable solution. Docker can't bypass rules that are enforced before traffic reaches your server.