Generated male engineer teaching scene with laptop and conceptual Docker/UFW port diagram.

WIUNIX ACADEMY

Why is your Docker port open when UFW says it is blocked?

Trace an existing HTTP container's port binding, compare local and external access, then restrict an intended private backend and verify the result.

Intermediate20 min readUpdated
Make this guide yours

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Debian 13Reviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment8 editable values

THE OUTCOME

What will you have at the end?

Identify unintended direct access to an HTTP backend, restrict its host mapping and verify the authorized path.

01External requestPublic host → published port → Docker NAT
02Private backendHost proxy → 127.0.0.1:8080 → 80
03AcceptanceRuntime binding + local health + independent external probe
Supported operating systems and versions

Documented rootful Docker Engine ≥28, ordinary NAT bridge and single-file Compose v2 path; no hands-on installation matrix is claimed.

Before you begin

  • Existing single-file Compose HTTP service, rootful Docker ≥28 and ordinary NAT bridge.
  • Deployment operator access, host curl and Python 3, independent client and a maintenance window.

UFW says active. Your container may still be reachable.

A healthy HTTP application and a closed perimeter are different outcomes. This guide investigates one existing Compose service, then restricts its host mapping and compares local and external access. It does not deliberately expose a new test service.

Docker documents that published bridge traffic can be diverted before the UFW INPUT/OUTPUT rules. Treat a wildcard mapping as a reason to investigate; actual reachability also depends on upstream firewalls and routing.

Check that this recipe fits your deployment

Use a single Linux host with rootful Docker Engine 28 or newer, a normal NAT bridge and Compose v2. You need an existing HTTP service, its original project directory, one compose.yaml file, and authority to recreate that service during a short maintenance window.

Stop if the project depends on multiple Compose files, overrides, profiles or special project flags: use the owning deployment procedure instead. Swarm ingress, host/macvlan/ipvlan networking, rootless Docker, Docker Desktop, routed gateways and direct routing need a different analysis. A containerized proxy must reach its backend over a shared private network; its own loopback is not the host.

Keep your SSH session open and have console recovery. This article changes one application's port mapping, not the host firewall. Localhost is not authentication, TLS or isolation from other local users.

Use your public values before copying

Open the settings, replace the example address and names, then Apply. Never enter secrets. Values stay in this browser tab's session storage for this language and environment; they are not sent to the server. The documentation address is not a real target.

The service must speak plain HTTP on its selected container port. Set host and container ports to the CURRENT mapping, not a new port you wish to use. The SSH forwarding port is a separate free port on your workstation.

01

LET’S BUILD

Read the engine and current project first

Run on the Docker host as the existing deployment operator. Check that the daemon is rootful, the server major version is at least 28, and there is no unexpected override or Compose environment. Stop and use the owning procedure if the conditions differ.

bash

Docker host

cd '/srv/myapp' || exit 1
sudo docker version --format '{{.Server.Version}}'
sudo docker info --format '{{json .SecurityOptions}}'
sudo docker compose version
test -f compose.yaml || exit 1
find . -maxdepth 1 -type f \( -name '*compose*.yml' -o -name '*compose*.yaml' \) -print
printenv | sort | sed -n '/^COMPOSE_/p'
sudo docker compose -f compose.yaml config --services
sudo docker compose -f compose.yaml ps
How do I know it worked?

Only the intended single-file project is selected; the HTTP service appears and its operator confirms the supported engine/network scope.

02

LET’S BUILD

Inspect the actual running port bindings

Run on the host. Inspect only the selected fields; avoid sharing full container inspection because environment values may contain credentials. NetworkMode must refer to an ordinary bridge network. List every attached network and inspect its driver and gateway options.

bash

Docker host

cd '/srv/myapp' || exit 1
cid=$(sudo docker compose -f compose.yaml ps -q 'web')
test -n "$cid" || exit 1
sudo docker inspect "$cid" --format '{{json .HostConfig.NetworkMode}}'
sudo docker inspect "$cid" --format '{{json .NetworkSettings.Ports}}'
sudo docker port "$cid"
for net in $(sudo docker inspect "$cid" --format '{{range $name, $value := .NetworkSettings.Networks}}{{$name}} {{end}}'); do
  sudo docker network inspect "$net" --format '{{.Name}} {{.Driver}} {{json .Options}}'
done
sudo ufw status verbose
EXAMPLE OUTPUT
80/tcp -> 0.0.0.0:8080
80/tcp -> [::]:8080
How do I know it worked?

Record the current bindings. The illustrative output shows wildcard IPv4/IPv6 mappings, not proof of Internet access. UFW status alone cannot establish isolation.

03

LET’S BUILD

Compare a local request with an independent probe

On the Docker host, make a credential-free request to the existing HTTP service. Any HTTP status, even 401 or 403, proves an HTTP listener answered. Do not send application credentials over this diagnostic HTTP request.

bash

Docker host

curl --noproxy '*' --connect-timeout 3 --max-time 5 -sS -o /dev/null -w 'HTTP %{http_code}\n' 'http://127.0.0.1:8080/'
How do I know it worked?

An HTTP response establishes the local baseline. A connection error means diagnose the app before changing its mapping.

Explore all tutorials ↗