> ## Documentation Index
> Fetch the complete documentation index at: https://help.freakhosting.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How to Secure Docker on a VPS: Best Practices and UFW Bypass

> Docker security best practices for a VPS: why published ports bypass UFW, how to bind them to 127.0.0.1, filter with DOCKER-USER and lock down the docker group.

Docker-published ports bypass UFW and firewalld, because Docker routes that traffic in the `nat` table before your firewall's input rules see it. To secure Docker on a VPS, publish private services on `127.0.0.1` only (`-p 127.0.0.1:8080:80`), put a reverse proxy in front of public ones, filter the rest in the `DOCKER-USER` chain, treat the `docker` group as root, and keep Docker and your images updated.

| Difficulty | Time | Works on | Needs |
| - | - | - | - |
| Intermediate | 30 minutes | Ubuntu, Debian, AlmaLinux, Rocky Linux with Docker Engine | Root or sudo access, [VNC console access](https://help.freakhosting.com/portals/vps-panel/how-to-enable-and-use-vnc-console-access.md) in case of a firewall mistake |

## Before you start

* Run commands as root or with `sudo`. Root users can leave out `sudo`.
* Find which containers publish ports, and on which address: `docker ps --format 'table {{.Names}}\t{{.Ports}}'`. `0.0.0.0:` or `[::]:` means the port is open to the internet.
* Firewall changes can lock you out. Keep your SSH session open and have the [VNC console](https://help.freakhosting.com/portals/vps-panel/how-to-enable-and-use-vnc-console-access.md) ready.

## Why does Docker bypass UFW?

UFW and firewalld filter traffic addressed to the server itself (the `INPUT` chain). When you publish a port, Docker adds a rule in the `nat` table that rewrites the traffic to the container's address before that happens, so it goes through the `FORWARD` chain instead. A `ufw deny 8080` rule never sees it. [Docker's documentation](https://docs.docker.com/engine/network/packet-filtering-firewalls/) states that Docker and UFW are incompatible in this way.

Don't fix this by setting `"iptables": false`. Docker's docs warn that it breaks container networking.

## How to keep container ports private

Publish a port on localhost when only something on the same server needs it, such as a reverse proxy, or when you reach it through an SSH tunnel:

```yaml theme={null}
    ports:
      - "127.0.0.1:8080:80"
```

Databases rarely need a published port at all. Containers in the same Compose project reach each other by service name (`db:3306`) without one.

### How to make localhost the default for every container

Create or edit `/etc/docker/daemon.json`:

```json theme={null}
{
  "ip": "127.0.0.1",
  "default-network-opts": {
    "bridge": {
      "com.docker.network.bridge.host_binding_ipv4": "127.0.0.1"
    }
  },
  "no-new-privileges": true,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
```

Check the file, then restart Docker:

```bash theme={null}
sudo dockerd --validate --config-file /etc/docker/daemon.json
sudo systemctl restart docker
```

| Setting | Effect |
| - | - |
| `ip` | Ports published without an address on the default bridge bind to `127.0.0.1`. |
| `default-network-opts` | The same for new user-defined networks, which Compose creates. Existing networks keep their setting until recreated. |
| `no-new-privileges` | Processes in containers can't gain extra privileges through setuid programs. |
| `log-opts` | Limits each container's log to three 10 MB files. |

After this, ports that must be public need the address written out, for example `"0.0.0.0:443:443"` for a reverse proxy. Swarm services ignore these settings.

## How to allow a public container port from one IP only

Docker processes the `DOCKER-USER` chain before its own rules, so rules you add there do apply to containers. This example lets only `198.51.100.20` reach a container published on port `5432`.

**1. Find your public interface**

```bash theme={null}
ip route show default
```

The name after `dev` is your interface, for example `eth0`.

**2. Add the rule**

```bash theme={null}
sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 5432 ! -s 198.51.100.20 -j DROP
```

`--ctorigdstport` matches the port the visitor connected to, before Docker translated it. Repeat with `ip6tables` if the container is also published on IPv6.

**3. Check it**

```bash theme={null}
sudo iptables -S DOCKER-USER
```

From another IP, the port now times out. See [check if a port is open from outside](https://help.freakhosting.com/portals/other/how-to-check-if-a-port-is-open-from-outside.md).

Rules added this way are lost at reboot. To restore them at boot, save this as `/etc/systemd/system/docker-user-rules.service`:

```ini theme={null}
[Unit]
Description=Extra firewall rules for Docker containers
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 5432 ! -s 198.51.100.20 -j DROP
ExecStop=/usr/sbin/iptables -D DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 5432 ! -s 198.51.100.20 -j DROP

[Install]
WantedBy=multi-user.target
```

Enable it with `sudo systemctl daemon-reload && sudo systemctl enable --now docker-user-rules`.

> [!WARNING]
>
> Never add a broad `DROP` to `DOCKER-USER` without a matching port or address. It can cut off every container, including their outgoing connections. Test one rule at a time.

## Who should have access to Docker?

* **The `docker` group equals root.** Any member can start a container that mounts `/` from the server and change any file. Add only people you would give full sudo rights. See [manage users and groups](https://help.freakhosting.com/portals/other/how-to-manage-users-and-groups-on-linux.md).
* **The Docker socket equals root.** Containers that mount `/var/run/docker.sock`, such as Portainer or Traefik, control Docker too, even with `:ro`. Use them only from projects you trust, and protect their web logins.
* **Don't open the Docker API on a TCP port.** Current Docker refuses to start with unauthenticated TCP. To manage Docker remotely, use SSH: `DOCKER_HOST=ssh://alex@203.0.113.10 docker ps`.

## Container settings that reduce risk

| Do | Avoid |
| - | - |
| Use official or verified images and pin a version tag | Unknown images from random accounts |
| Run the app as a non-root user if the image supports it (`user: "1000:1000"`) | `privileged: true` unless the project's docs require it |
| Drop capabilities the app doesn't need (`cap_drop: [ALL]`, then add back what it needs) | `network_mode: host` for apps that work with normal ports |
| Mount only the folders the app needs, read-only where possible (`:ro`) | Mounting `/`, `/etc` or the Docker socket "just in case" |
| [Update images](https://help.freakhosting.com/portals/other/how-to-update-docker-containers.md) and Docker Engine regularly | Leaving a container on the same image for years |

## What is rootless Docker?

In rootless mode, the Docker daemon and containers run as a normal user, so a container escape doesn't give root on the server. Install the `docker-ce-rootless-extras` package and run `dockerd-rootless-setuptool.sh install` as that user. Rootless mode can't bind ports below 1024 by default and has other limits, so read [Docker's rootless guide](https://docs.docker.com/engine/security/rootless/) first. [Podman](https://help.freakhosting.com/portals/other/how-to-install-podman.md) runs rootless by design.

## Frequently asked questions

### Is ufw-docker or editing after.rules official?

No. Docker's documentation recommends the `DOCKER-USER` chain or binding ports to a specific address. Third-party scripts that rewrite UFW's rule files can work, but they aren't covered by Docker's docs.

### Does firewalld have the same problem?

Yes. Docker creates a firewalld zone called `docker` with target `ACCEPT` and a forwarding policy into it, so published ports are reachable whatever your other zones say. Use the same fixes: bind to `127.0.0.1` or filter in `DOCKER-USER`.

### Should I use a reverse proxy for every web app?

For anything public, yes. A reverse proxy such as [Nginx Proxy Manager](https://help.freakhosting.com/portals/other/how-to-set-up-nginx-proxy-manager.md) or [Traefik](https://help.freakhosting.com/portals/other/how-to-set-up-traefik-with-docker.md) gives you HTTPS, one place to add access rules, and only ports 80 and 443 open.

### Do I still need UFW if I use Docker?

Yes. UFW still protects SSH and anything not running in Docker. See [set up a firewall with UFW](https://help.freakhosting.com/portals/other/how-to-set-up-a-firewall-with-ufw.md).

## Related guides

> [card] [VPS and dedicated server guides](https://help.freakhosting.com/portals/other/vps-and-dedicated-server-guides.md)
>
> Every guide in this section.

> [card] [Set up a firewall with UFW](https://help.freakhosting.com/portals/other/how-to-set-up-a-firewall-with-ufw.md)
>
> Protect SSH and host services.

> [card] [nftables and iptables](https://help.freakhosting.com/portals/other/how-to-use-nftables-and-iptables.md)
>
> Work with firewall rules directly.

> [card] [Linux server security checklist](https://help.freakhosting.com/portals/other/linux-server-security-checklist.md)
>
> Harden the rest of the server.

> [card] [Fix locked out after a firewall change](https://help.freakhosting.com/portals/other/how-to-fix-locked-out-after-firewall-change.md)
>
> Get back in through the console.

> [card] [Install Podman](https://help.freakhosting.com/portals/other/how-to-install-podman.md)
>
> A daemonless, rootless alternative.

***

> [!NOTE]
>
> **Checked against the official Docker documentation on [packet filtering and firewalls](https://docs.docker.com/engine/network/packet-filtering-firewalls/), [Docker with iptables](https://docs.docker.com/engine/network/firewall-iptables/), [port publishing](https://docs.docker.com/engine/network/port-publishing/), [Docker security](https://docs.docker.com/engine/security/) and [rootless mode](https://docs.docker.com/engine/security/rootless/)** on 4 November 2026; `daemon.json` validated with `dockerd --validate`, the localhost default binding and `no-new-privileges` tested on Ubuntu 24.04 LTS with Docker Engine 29.9.2, and the `DOCKER-USER` rule and systemd unit syntax checked. Commands can differ on other distributions or releases.

> [!NOTE]
>
> ### Need Extra Help?
>
> If you encounter any issues, our support team is ready to assist:
>
> * **Live Chat:** Quick assistance via our website.
> * **Support Ticket:** [Open a Ticket](https://freakhosting.com/clientarea/submitticket.php)
> * **Discord:** [Join our Community](https://discord.gg/freakhosting)
> * **Email:** [support@freakhosting.com](mailto:support@freakhosting.com)

> [!TIP]
>
> ### Save on Your Hosting
>
> Ready to upgrade your hosting? Use code **KB20** at checkout for **20% off** your first order!

***

**Last Updated:** November 2026


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.