skip to content
Sean Thawe
Table of Contents

I like running my own systems on my own hardware. Not because it’s the easiest way — it rarely is — but because the machines are mine, the data is mine, and when something breaks I can walk over and fix it myself. So most of my services run as Docker containers on a couple of Linux boxes on my own tailnet, Odoo among them.

That setup served me well for months — open the web client from my laptop and go. Then one midnight it stopped, in the most confusing way possible: the service was running fine, but every other machine on the network simply couldn’t reach it.

Ping worked. The container was healthy. docker ps showed the port published correctly. And yet the browser on my laptop sat there loading forever.

What You’ll Learn in This Guide

  • Why “the service is running” and “the service is reachable” are two completely different things.
  • How to check listening ports and published ports properly.
  • Why Docker’s port publishing silently bypasses UFW.
  • Why a firewall check can look guilty while being completely innocent.
  • The one tailscaled restart that fixed everything.

Part 1: The Symptom — Ping Works, Browsers Hang

The setup: Odoo 19 runs as a Docker container on a Linux server on my tailnet. From my laptop, visiting the server on port 8069 used to load the Odoo login page instantly.

Then at midnight: nothing. The page just spins. No error, no connection refused — the browser hangs forever. That detail matters: a “connection refused” is the service saying “I’m here but not listening.” A hang means the traffic never reaches the service at all.

First things first, I checked the server itself:

Terminal window
docker ps | grep odoo
6fda...2894 odoo-19-dockerized-odoo-19 "/entrypoint.sh odoo" 4 months ago Up 15 hours 0.0.0.0:8069->8069/tcp odoo-19-dockerized-odoo-19-1

The container is Up, and the port publish is exactly right: 0.0.0.0:8069->8069/tcp. Odoo is listening on all interfaces too:

Terminal window
ss -tlnp | grep 8069
LISTEN 0 4096 0.0.0.0:8069 0.0.0.0:*
LISTEN 0 4096 [::]:8069 [::]:*

I could even log into Odoo’s web client — from the server itself. So the service was demonstrably healthy, and the problem had to be somewhere in the network between the server and the rest of the tailnet.

Explanation: docker ps shows the published port mapping — how the container’s port is exposed on the host. ss -tlnp shows what’s actually listening. Both healthy rules out every service-level cause in one step.

Part 2: The Red Herring — The Firewall Looks Guilty

tailscale ping to the server returned pongs, so the tailnet link itself seemed fine. What could block a port between two healthy machines?

My eye landed on the firewall. The server runs UFW, and it was active:

Terminal window
sudo ufw status
Status: active
To Action From
-- ------ ----
53317/udp ALLOW Anywhere
53317/tcp ALLOW Anywhere
22/tcp ALLOW Anywhere
172.17.0.1 53/udp ALLOW 172.16.0.0/12

Port 8069 is not in the allow list. The firewall must be dropping the traffic. Case closed, right?

Almost — and here’s where it gets subtle. Docker’s published ports bypass UFW entirely. When you publish -p 8069:8069, Docker injects its own iptables rules (a DNAT rule that forwards inbound traffic straight to the container network). The host firewall never even sees it. UFW not listing 8069 is normal and expected — it was never the thing allowing that port.

To be thorough I checked Docker’s own rules anyway:

Terminal window
sudo iptables -L -n | grep 8069
ACCEPT tcp -- 0.0.0.0/0 172.18.0.3 tcp dpt:8069

That’s Docker’s forwarding rule — accepting traffic from anywhere, destined for the Odoo container. The port path through Docker was open.

Part 3: The Pivot — Test Every Port, Not Just the One You Care About

Here’s the move that cracked it. Instead of re-testing port 8069 in isolation, I scanned the server’s whole TCP port range from my laptop:

  • Port 22 (SSH) — timeout
  • Port 80 — timeout
  • Port 443 — timeout
  • Port 8069 (Odoo) — timeout

Port 22 is explicitly allowed by UFW — it’s right there in the ufw status output. And it was also timing out. That single fact kills the firewall theory: UFW cannot be blocking a port it explicitly allows.

The real pattern was now visible: every TCP connection to the server timed out, while ping (ICMP) worked fine. That’s not a firewall signature. That’s a broken tunnel — the tailnet link was carrying ping traffic but dropping everything else.

Part 4: The Fix — Restart the Tunnel

The server’s tailnet client had gone stale: it was still advertising its routes and answering pings, but real traffic was silently vanishing into a tunnel that had stopped working. The fix was one command:

Terminal window
sudo systemctl restart tailscaled

Seconds later, from my laptop:

Terminal window
curl http://<server>:8069/web/login

The Odoo login page came back instantly. The whole outage was a stale tailscaled daemon — not Odoo, not Docker, not UFW.

Explanation: tailscaled is the daemon that maintains the encrypted tunnel. After a long uptime it can end up half-dead: the control plane (ping, status) still responds, but the data plane (real TCP traffic) doesn’t. A restart re-establishes the tunnel and everything flows again.

What I’d Do Differently Next Time

Looking back, the expensive mistake was blaming the firewall because it looked guilty. The cheap fix was scanning every port and noticing that a UFW-allowed port failed too. Three things I’ll remember:

  1. “Running” is not “reachable.” A healthy container and a healthy listener prove the service is fine — the network is guilty until proven otherwise.
  2. Test ports you don’t care about. If a port your firewall explicitly allows is also dead, you’ve just ruled out the firewall.
  3. ICMP working ≠ TCP working. Ping takes a different path than real traffic. If pings succeed but every TCP port times out, suspect the tunnel itself before the host.

References & Further Reading