Most people deploy their first API the same way: expose it on 0.0.0.0 and hope for the best.
That's a mistake.
A production-ready API should never face the internet directly. Here's how to set it up properly, and why each layer exists.
Keep the API private by design
When your application starts, the bind address is a security decision.
-
0.0.0.0means "accept connections from anywhere." Anyone who discovers the port can talk straight to your process. -
127.0.0.1means "accept connections only from this machine." Nothing outside the server can reach it. Always bind to127.0.0.1. Your application was never meant to be the public face of the service. Something else should handle that job.
Put a reverse proxy in front
A reverse proxy sits between the internet and your application. Every request arrives at the proxy first. The proxy decides what to do, then forwards the request only when appropriate.
With Nginx the flow is simple:
Browser → Nginx (HTTPS, port 443) → Your API (HTTP, 127.0.0.1:3000)
Nginx is the only process the outside world ever touches. Your API works behind a locked door. Nginx holds the only key.
This separation delivers several practical benefits:
Nginx terminates TLS. Your application speaks plain HTTP and never needs to know about certificates. Encryption lives in one place instead of being duplicated across every service.
Nginx can log, rate-limit, and route without changing your code. Adding another service later is just another location block.
If the application crashes, the failure stays private. A raw stack trace never reaches the browser. Nginx can return a controlled response instead.
A minimal location block looks like this:
location /api/ {
proxy_pass http://127.0.0.1:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
The proxy_pass line does the forwarding. The header lines are essential. Without them, every request appears to come from Nginx itself and your application loses the real client information.
Verify both directions. Confirm the API responds correctly through the public domain. Then confirm it does not respond when you hit the server IP directly on the application port. A working reverse proxy is not only "the front door works." It is also "there is no back door."
Let systemd keep the process alive
Starting an application in a terminal is fine for development. It is not acceptable in production. Close the session and the process dies.
systemd exists to start, stop, and supervise long-running services. A minimal service file looks like this:
[Service]
User=deploy
WorkingDirectory=/home/deploy/projects/api
ExecStart=/home/deploy/projects/api/venv/bin/uvicorn main:app - host 127.0.0.1 - port 3000
Restart=always
RestartSec=5
Restart=always combined with RestartSec=5 is the key.
In plain English: if the process ever stops for any reason, wait five seconds and start it again. No human intervention required.
Test it the hard way. Send a kill -9 to the process. Within seconds a new process appears with a fresh PID. Reboot the server. When the machine comes back, the service is already running because it was enabled on boot.
That is the real value of Restart=always. The system recovers while you are offline.
Make the deployment idempotent
The final piece is a single script that installs dependencies, deploys the code, configures systemd, and configures Nginx. The script must be safe to run more than once.
Run it on a fresh server and it builds everything from scratch. Run it again on the same server and nothing breaks or duplicates. That property is called idempotency.
A deploy script you cannot safely re-run is a script you will eventually be afraid to use. Design for the second run from the beginning.
Practical rules that hold up
Bind the application to 127.0.0.1. Let Nginx be the only process the internet can reach. Hand the responsibility of staying alive to systemd. Write every deployment step so it can be repeated safely.
These are not advanced techniques. They are the baseline difference between a service that works for a demo and one that remains correct months later with no one watching it.
Full example: github.com/tesddev/fastapi-nginx-service

Top comments (0)