Fix a Django Site That Runs but Cannot Be Reached

Fix a Django site that runs on the server but times out in the browser: check what listens and on which address, open ufw and cloud firewalls, stop binding to 127.0.0.1, and confirm DNS points at the server.

Problem statement

On the server, everything looks fine: Gunicorn is running, curl http://localhost returns your page. From a browser the site times out with ERR_CONNECTION_TIMED_OUT, or refuses with ERR_CONNECTION_REFUSED. This is one of the most common problems on a first deployment.

Between the browser and Django sit several layers: DNS, the cloud provider's firewall, the server's own firewall, and the address the web server listens on. curl localhost on the server skips all of them except the last. Work outward from the server, testing each layer, and the blocked one shows itself.

Layers between browser and DjangoDNSname to right IPCloud firewallsecurity group rulesServer firewallufw / firewalldListener0.0.0.0:80 or 443Localhost tests skip every layer except the listener, so test from outside too.
  1. Check what listens and where: sudo ss -ltnp. Nginx should show 0.0.0.0:80 (and 443); Gunicorn can stay on a socket or 127.0.0.1.
  2. Open the server firewall: sudo ufw allow 'Nginx Full' (or firewall-cmd --add-service=http --add-service=https --permanent).
  3. Open the cloud firewall or security group for TCP 80 and 443.
  4. Test by IP from outside: curl -v http://<server-ip>/.
  5. Check DNS: dig +short example.com must return the server's public IP.
  6. Do not expose runserver or Gunicorn directly on port 8000; put Nginx in front.

Step-by-step solution

1. See what is listening

sudo ss -ltnp
LISTEN  0  511  0.0.0.0:80        users:(("nginx",pid=901,...))
LISTEN  0  2048 127.0.0.1:8000    users:(("gunicorn",pid=1203,...))

This is a healthy layout: Nginx listens publicly, Gunicorn privately. If Nginx is missing, start it. If you are trying to reach Gunicorn on :8000 directly from the browser, note it only listens on 127.0.0.1, so remote connections are refused. That is correct; reach it through Nginx instead.

runserver defaults to 127.0.0.1:8000 too. runserver 0.0.0.0:8000 makes it reachable, but it is a development server and should not face the internet.

2. Test from outside by IP

From your laptop:

curl -v --connect-timeout 5 http://203.0.113.10/
  • Timeout: packets are dropped by a firewall (cloud or server).
  • Connection refused: the host is reachable but nothing listens on that port, or a firewall actively rejects.
  • A response: the server side works; the problem is DNS or HTTPS.
what curl from outside saysResult from outsideLayer to fixtimeout by IPcloud firewall or ufwdrops port 80/443refused by IPnothing on 0.0.0.0:80; start Nginxworks by IP, not by nameDNS points elsewhere or not propagatedhttp works, https times outport 443 closed in a firewall:8000 refusedGunicorn on 127.0.0.1by design; use NginxTesting by IP first separates network problems from DNS problemsdjango-deployment.com
Always test from a machine outside the server and its private network.

3. Open the server firewall

Ubuntu with ufw:

sudo ufw status verbose
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Allow SSH before enabling ufw, or you lock yourself out. RHEL family with firewalld:

sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload

Oracle Cloud images also ship restrictive iptables rules on the instance itself; add rules for 80 and 443 there or use their documented method.

4. Open the cloud firewall

This layer is outside the server and is checked first. Each provider names it differently:

  • AWS: Security Group inbound rules for the instance.
  • Google Cloud: VPC firewall rules (allow http-server, https-server tags).
  • Azure: Network Security Group.
  • DigitalOcean: Cloud Firewalls; Hetzner: Firewalls; Oracle: Security Lists or NSGs.

Add inbound TCP 80 and 443 from 0.0.0.0/0 (and ::/0 for IPv6). Keep SSH limited to your IP where possible.

5. Check DNS

dig +short example.com A
dig +short example.com AAAA

The A record must match the server's public IP (curl -4 ifconfig.me on the server). An AAAA record pointing at an old or unconfigured IPv6 address makes some visitors fail while others succeed. New records may take time to appear on every resolver.

test from the inside out1curl localhost on the serverapp and Nginx work2ss -ltnp shows 0.0.0.0:80public listener exists3ufw or firewalld allows 80/443server firewall open4Cloud firewall allows 80/443provider layer open5dig name returns server IPDNS correctThe first step that fails is the layer to fixdjango-deployment.com
Each step adds one layer, so a failure points at exactly one place.

Verification check: from outside the server, curl -I http://example.com returns a response from Nginx, and after HTTPS is set up, curl -I https://example.com does too.

Explanation

A request from a browser passes through name resolution, the provider's network filtering, the host's packet filter, and finally a listening socket. Tests run on the server itself, such as curl localhost, use the loopback interface and bypass the network filters entirely, so they prove only that the application responds. Cloud firewalls are especially easy to miss because they are configured in a web console, not on the server, and default to allowing only SSH on many providers.

Edge cases / notes

  • Docker port publishing bypasses ufw on many systems because Docker writes its own iptables rules; a container published on 0.0.0.0:8000 may be reachable even when ufw says it is blocked.
  • IPv6-only or IPv4-only listeners: listen 80; covers IPv4; add listen [::]:80; for IPv6.
  • Corporate networks may block unusual ports; test standard ports from a phone on mobile data.
  • Fail2ban may have banned your own IP after failed SSH attempts; check fail2ban-client status.

Next steps

FAQ

Why does curl localhost work but the browser times out?

Local requests skip the firewalls. A timeout from outside means a cloud or server firewall drops the traffic.

Should I open port 8000 for Gunicorn?

No. Keep Gunicorn private and expose Nginx on 80 and 443.

Can I use runserver 0.0.0.0:8000 in production?

It makes the dev server reachable, but it is not built for production traffic or security. Use Gunicorn behind Nginx.

Why do some visitors reach the site and others not?

Often an AAAA record or DNS propagation: some resolvers or IPv6 clients get a different address.

Keep going