NeuroDynamic.Tech
Sign inJoin

Docker Compose for real: one file, whole service — and the firewall trap

A real service running from one small text file, surviving reboots — then I prove, with real output, that Ubuntu's firewall does not protect Docker's published ports, and show you the fix.

The founder · 10 min read ·

GOING FURTHER

Tested for real: I ran every command in this guide top-to-bottom on a fresh Ubuntu 24.04 virtual machine (4 CPUs, 8 GB RAM) on 13 August 2026. Versions at test time: Docker Engine 29.7.2, Docker Compose v5.4.0, Uptime Kuma 2.5.0, on Ubuntu 24.04.4 LTS.

Found a problem? Tell me via the contact page.

What you'll end up with

A real service — Uptime Kuma, a status-monitoring app — running from one small text file. It survives reboots, and its data survives upgrades. Then the part most guides skip. I prove, with real output from a second machine, that Ubuntu's firewall does not protect Docker's published ports. Then I show you the fix.

NAME          IMAGE                    STATUS                   PORTS
uptime-kuma   louislam/uptime-kuma:2   Up (healthy)             127.0.0.1:3001->3001/tcp

Who this is for

GOING FURTHER. It assumes you installed Docker with the Docker install guide, including the docker group step. One deliberate branch near the end.

Time and cost

My machine time was about 15 minutes, including a reboot and firewall work. Allow 45 minutes end to end. Free. The Uptime Kuma image is about 500 MB.

Words you'll meet

  • Compose file — a text file describing a service: which image, which ports, where data lives. One file, whole service. See /basics.
  • Named volume — a storage area Docker manages for a container. It outlives the container, which is the whole point.
  • Published port — a door Docker opens so the outside world can reach a container.
  • UFW — Uncomplicated Firewall, Ubuntu's built-in firewall tool.
  • Binding — which network addresses a port listens on. 0.0.0.0 means "every address, everyone"; 127.0.0.1 means "this machine only".
  • iptables — the low-level packet rulebook in Linux. Both UFW and Docker write rules into it. That shared rulebook is where today's trap lives.

Placeholders

YOUR-SERVER-IP means your server's address on your network (find it with ip -4 addr). Everything else is copy-paste as written.

Before you start

  • Docker installed per the Docker install guide — that guide is the only prerequisite.
  • A second machine on the same network (a laptop is fine) for the firewall proof. You can follow without one, but seeing it yourself is the lesson.

The steps

Step 1 — Write the compose file

One folder per service keeps things tidy. Make one and open a new file in it.

mkdir -p ~/uptime-kuma
nano ~/uptime-kuma/compose.yaml

Paste exactly this, then save (Ctrl+O, Enter) and exit (Ctrl+X):

services:
  uptime-kuma:
    image: louislam/uptime-kuma:2
    container_name: uptime-kuma
    volumes:
      - kuma-data:/app/data
    ports:
      - "3001:3001"
    restart: unless-stopped

volumes:
  kuma-data:

Four lines carry the weight. image is what to run. volumes gives the app a permanent home for its data. ports opens door 3001. restart: unless-stopped means "come back after reboots unless I stopped you on purpose".

Check it worked.

cat ~/uptime-kuma/compose.yaml

You should see the file back, exactly as pasted. Indentation matters in this format — two spaces, no tabs.

Step 2 — Start it

From the service's folder, one command downloads the image and starts everything the file describes.

cd ~/uptime-kuma
docker compose up -d

You should see something like (trimmed — the download scrolls for a while):

 Image louislam/uptime-kuma:2 Pulled
 Volume uptime-kuma_kuma-data  Created
 Network uptime-kuma_default  Created
 Container uptime-kuma  Started

The important line is Container uptime-kuma Started.

In my test the download and start took 37 seconds. The -d means detached: it runs in the background and gives your terminal back.

Check it worked.

curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3001
302

302 is a redirect — Uptime Kuma is answering and pointing new visitors at its setup page. From your laptop's browser, http://YOUR-SERVER-IP:3001 shows that page. (Remember this works from the laptop — it matters in step 4.)

Step 3 — Reboot and make sure it comes back

A service you must remember to restart is a chore, not a service. Reboot the machine.

sudo reboot

Your connection drops. Wait a minute, log back in, then:

Check it worked.

docker ps

Your output will vary, but look for:

CONTAINER ID   IMAGE                    STATUS                            PORTS                    NAMES
1fc5ee841e0c   louislam/uptime-kuma:2   Up 5 seconds (health: starting)   0.0.0.0:3001->3001/tcp   uptime-kuma

The important part is Up in the STATUS column.

Nobody started that. restart: unless-stopped did. In my test it was up 5 seconds after I could log in.

Step 4 — Turn on the firewall, block the port… and watch Docker ignore it

Here is the trap this guide exists for. Let's walk into it deliberately.

First, allow SSH — do this before enabling the firewall or you lock yourself out:

sudo ufw allow OpenSSH
Rules updated
Rules updated (v6)

Now switch the firewall on. It will warn you about SSH; we just allowed it, so answer y:

sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

The important line is Firewall is active and enabled on system startup.

Now explicitly deny our service's port:

sudo ufw deny 3001/tcp
Rules updated
Rules updated (v6)

Check the firewall believes you.

sudo ufw status verbose
Status: active
Default: deny (incoming), allow (outgoing), deny (routed)

To                         Action      From
--                         ------      ----
22/tcp (OpenSSH)           ALLOW IN    Anywhere
3001/tcp                   DENY IN     Anywhere

The important lines are deny (incoming) and 3001/tcp DENY IN.

Denied, in writing. Now go to your second machine and ask for the page anyway:

curl -s -o /dev/null -w "%{http_code}\n" --max-time 5 http://YOUR-SERVER-IP:3001
302

It answered. Default deny incoming, an explicit deny rule on 3001 — and the page is served to the whole network regardless.

To prove the firewall itself is fine, I ran a plain non-Docker web server on the same machine on port 8000. I denied that port too, then asked from the laptop again:

curl -s -o /dev/null -w "%{http_code}\n" --max-time 5 http://YOUR-SERVER-IP:8000
000

Timeout. Blocked, exactly as ordered. The firewall works — for everything except Docker.

Why

UFW writes its rules into iptables, Linux's packet rulebook. Docker writes its own port-forwarding rules into the same rulebook — at a point traffic reaches before UFW's rules are consulted. So for published ports, Docker's "let it in" wins and UFW's "deny" is never even read. Neither tool is broken. They're both writing in the same book, and Docker's rule is reached before UFW's deny rule. This bit confused me too, and it has quietly exposed a lot of home servers.

Step 5 — The fix: publish to localhost only

The port line in the compose file currently says "every address". Change it to "this machine only". Edit the file:

nano ~/uptime-kuma/compose.yaml

Change the ports line from "3001:3001" to:

    ports:
      - "127.0.0.1:3001:3001"

Apply the change — compose notices the difference and rebuilds only what changed:

docker compose up -d
 Container uptime-kuma  Recreate
 Container uptime-kuma  Started

Check it worked. On the server, note the PORTS column now names 127.0.0.1:

docker ps --format "table {{.Names}}\t{{.Ports}}"
NAMES         PORTS
uptime-kuma   127.0.0.1:3001->3001/tcp

The server can still reach it:

curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3001
302

And from the laptop, the same request that sailed through before:

curl -s -o /dev/null -w "%{http_code}\n" --max-time 5 http://YOUR-SERVER-IP:3001
000

Timeout. The door only opens from inside the machine now. Your data survived the rebuild, by the way — that's the named volume doing its job.

The branch: but I wanted to reach it from my laptop

Two honest options:

  1. Keep 127.0.0.1 and add a doorman. Put a reverse proxy (like Caddy) or a VPN (like Tailscale) in front. This is the pattern I use and recommend; my tunnel and Tailscale guides cover it.
  2. Teach Docker to respect your firewall via the DOCKER-USER iptables chain — a rules area Docker checks and lets you win in. It is the deeper fix, but it is hand-written iptables. The ufw-docker project documents it well. Advanced path; use only if you are ready to maintain iptables rules.

Something went wrong?

  • You see Aborted after ufw enable → it means the y/n prompt was answered with anything but y (this bit me: my first scripted run aborted here and the firewall silently stayed off) → run sudo ufw status; if it says Status: inactive, run sudo ufw enable again and type y.
  • You see Skipping adding existing rule → it means the rule was already there from an earlier attempt → harmless, carry on.
  • You see 000 from curl on the laptop in step 4 where I saw 302 → it means the trap didn't spring for you — check you're testing the Docker port (3001), not an ordinary one, and that docker ps shows 0.0.0.0:3001 not 127.0.0.1:3001.

Undo all of this

Tested for real. From the service folder:

Stop and remove the container and its network — data survives this:

cd ~/uptime-kuma
docker compose down
 Container uptime-kuma  Removed
 Network uptime-kuma_default  Removed

I checked: docker volume ls still showed uptime-kuma_kuma-data afterwards. To delete the data too, add -v:

docker compose down -v
 Volume uptime-kuma_kuma-data  Removed

The important line is Volume uptime-kuma_kuma-data Removed.

Remove the image and the folder:

docker image rm louislam/uptime-kuma:2
rm -r ~/uptime-kuma

Put the firewall back how you found it (this disables it and wipes the rules — skip if you're keeping UFW on):

sudo ufw reset

Check it worked. docker ps -a shows no uptime-kuma; docker volume ls shows no kuma-data; sudo ufw status says whatever you chose it to say.

Where to go next


Last tested: 13 August 2026 on Ubuntu 24.04.4. Versions: Docker Engine 29.7.2, Compose v5.4.0, Uptime Kuma 2.5.0.


Tried it? Improved it?

Tell the forum what worked and what didn’t: real experience beats recommendations, and the best answers get folded back into this guide with credit.

Related guides