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.
- Check what listens and where:
sudo ss -ltnp. Nginx should show0.0.0.0:80(and 443); Gunicorn can stay on a socket or127.0.0.1. - Open the server firewall:
sudo ufw allow 'Nginx Full'(orfirewall-cmd --add-service=http --add-service=https --permanent). - Open the cloud firewall or security group for TCP 80 and 443.
- Test by IP from outside:
curl -v http://<server-ip>/. - Check DNS:
dig +short example.commust return the server's public IP. - Do not expose
runserveror 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.
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-servertags). - 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.
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:8000may be reachable even when ufw says it is blocked. - IPv6-only or IPv4-only listeners:
listen 80;covers IPv4; addlisten [::]: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
- Complete the standard setup with Deploy Django With Gunicorn and Nginx on Ubuntu.
- Get DNS right with Point a Domain to a Django Server.
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.