My portfolio was live in Docker and Nginx was running fine — curl localhost returned the right HTML.
But when I hit the public IP from a browser, nothing. Just a timeout. This is the full debugging story.
The Situation
I had deployed my portfolio on an Oracle Cloud Free Tier VM (1 CPU, 1 GB RAM, Ubuntu).
Docker was running a container with Nginx serving static files on port 8080, with a host mapping of 80:8080.
Everything checked out locally.
Step 1 — Eliminate Docker First
Testing from inside the VM confirmed Docker and Nginx were not the problem:
curl localhost # Returns correct HTML — container is healthy docker ps # PORTS: 0.0.0.0:80->8080/tcp — mapping looks right
Step 2 — Check the OCI Security List
Oracle Cloud has its own network security layer — the Security List on the subnet. Port 80 (and 443) ingress rules need to be explicitly added. I confirmed they were present. Still timing out.
Step 3 — Check if Nginx is Binding Correctly
sudo lsof -i :80 # COMMAND PID USER FD TYPE DEVICE NODE NAME # nginx 1459 root ... *:http
Nginx was correctly listening on all interfaces (*:http). Not the issue.
Step 4 — tcpdump Reveals the Truth
Running tcpdump on the VM while hitting the public IP from my browser told the real story:
sudo tcpdump -i any port 80 # Packets arrived at the VM — but no response was sent back
Packets were making it through the OCI Security List and reaching the VM. But something on the OS level was dropping them silently.
Root Cause — iptables DROP Rule
Oracle Cloud VMs ship with a default iptables rule that drops all inbound traffic
not explicitly allowed. The rule was processed before Nginx could handle the request:
sudo iptables -L INPUT --line-numbers -n # Chain INPUT (policy ACCEPT 0 packets) # 1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED # 2 ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0 # 3 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 (lo interface) # 4 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 dport 22 # 5 REJECT all -- 0.0.0.0/0 0.0.0.0/0 <-- EVERYTHING ELSE DROPPED
Port 80 was not in the ACCEPT rules. Line 5 was rejecting it.
The Fix
sudo iptables -I INPUT 5 -p tcp --dport 80 -j ACCEPT sudo iptables -I INPUT 6 -p tcp --dport 443 -j ACCEPT
Using -I INPUT 5 inserts the rule before the REJECT line — order matters in iptables.
If you append with -A, the REJECT rule wins because it's evaluated first.
Making It Persistent
sudo apt install iptables-persistent -y sudo netfilter-persistent save
Without this, the rules reset on reboot.
What I Learned
There are three separate firewall layers on an OCI VM — all three need to allow traffic:
- OCI Security List — network-level, configured in the OCI console
- OS iptables — kernel-level, configured on the VM itself
- UFW / application firewall — not present on Oracle's default image, but worth checking
tcpdump is the fastest way to determine at which layer packets are being dropped.
If you see traffic arriving but no response leaving — it's the OS firewall, not the cloud security group.