How deployment works: from your laptop to the internet
Deploying is nothing more than running your program on a computer that never sleeps and has a public IP — this guide turns 'how do I put my app online' into boxes you already understand: a reverse proxy, gunicorn workers, environment variables, DNS, and free TLS. One simulator traces a request through the production stack; another shows exactly what a DEBUG page confesses to strangers.
mindmap — quick refresh
Prerequisites: How HTTP servers work (static vs dynamic, the production-pieces preview) and How WSGI works (the server ↔ app contract). How browsers work covered the DNS and TLS this guide leans on.
Three facts strip the mystique off deployment. First: "deploying" means nothing more than running your program on a computer that never sleeps and has a public IP address — the same python process you run locally, on different hardware. Second: localhost:8000 is unreachable from the internet by design — 127.0.0.1 is the loopback address, and packets to it never leave your machine, which is exactly why your experiments have been safe. Third: the dev server you've been using literally tells you not to deploy it — it's a single process wearing debugging conveniences that become security holes the moment strangers can reach them.
How HTTP servers work ended with a promise: real deployments are "a reverse proxy … spreading dynamic requests across a pool of application workers", and every deploy tutorial is "some arrangement of exactly these boxes." This guide is the full version of that promise. By the end, every hosting provider's dashboard should read as labels on boxes you already understand.
What a server actually is
A "server" in the deployment sense is a rented, always-on computer with a public IP address. The common form is a VPS (virtual private server): a slice of a machine in a datacenter, sold for a few dollars a month, running the same Linux you can run in a local terminal. Nothing about it is special-purpose — it has a CPU, RAM, a disk, and one thing your laptop doesn't: an IP address like 203.0.113.7 that any computer on the internet can route packets to, around the clock.
You operate it over SSH — the Secure Shell — which is best understood as a terminal on someone else's machine. You run ssh you@203.0.113.7 from your laptop, the connection is encrypted and authenticated with a key pair, and then every command you type executes over there instead of here. Everything you know from your local shell — cd, ls, python3, pip — transfers unchanged.
And your code gets there the way code moves everywhere else: through a Git remote (How Git works). The server — or the hosting platform standing in front of it — is just another remote. You git push, the remote end checks out the code, installs dependencies, and starts the process. Copying files by hand went out of style for a reason: a push is repeatable, and repeatable is what you want at 2 a.m. when a deploy goes wrong.
Why the dev server can't be the prod server
You could, mechanically, SSH in and run python manage.py runserver on that VPS. The tools themselves beg you not to. Django's documentation for runserver puts it in capitals:
DO NOT USE THIS SERVER IN A PRODUCTION SETTING. It has not gone through security audits or performance tests.
Python's http.server docs say the same thing about the static server from the HTTP-servers lab:
http.serveris not recommended for production. It only implements basic security checks.
Those warnings compress four concrete failures:
- One process, one request at a time. The dev server is the single-worker case from How HTTP servers work: while it renders one response, every other visitor waits. Fine for an audience of you; a queue for anyone else.
- DEBUG pages leak your code to strangers. Crash a Django view with
DEBUG = Trueand the response is a beautiful diagnostic page: the stack trace, your source lines, local variables, the SQL that ran, and your settings. On your laptop that page is a gift. On a public IP it's a confession — the second simulator below shows exactly what it hands over. - No TLS. The dev server speaks plain HTTP. No padlock, no encryption — passwords and cookies cross the network readable by anyone on the path.
- Crashes stay crashed. When the process dies — a bug, an out-of-memory kill, a reboot — nothing restarts it. Your site is down until a human notices.
Production servers exist to fix exactly these four things: many workers, no debug output, TLS at the front, and supervision that restarts what dies.
The standard stack: proxy, workers, static files
Here is the arrangement behind essentially every Python deployment, whether you assemble it or a platform does. Three boxes, front to back:
-
A reverse proxy — nginx or Caddy. It's the only process listening on the public ports (443 for HTTPS, 80 to redirect). It ends TLS (decrypts incoming traffic, so everything behind it can speak plain HTTP on the loopback interface), serves static files directly from disk — CSS, JavaScript, images, answered in microseconds without waking your app — and forwards dynamic requests to the box behind it. A minimal nginx version is short enough to read:
server { listen 443 ssl; server_name example.com; location /static/ { root /srv/myapp; # files: answered right here } location / { proxy_pass http://127.0.0.1:8000; # everything else: gunicorn } } -
A WSGI server — gunicorn is the common choice.
gunicorn --workers 3 config.wsgistarts a master process that forks 3 workers, each holding a copy of your application and running the accept → respond loop. The master itself serves nothing; it watches. Requests fan out across whichever workers are free — that's the worker-pool escape from the concurrency section of the HTTP-servers guide — and when a worker dies, the master forks a replacement. Crashes no longer stay crashed. -
Your app — the WSGI callable. Literally the
application(environ, start_response)contract you implemented in How WSGI works; a Django project'swsgi.pyexposes the same callable. Gunicorn calls it once per request, exactly likewsgirefdid on your laptop, just three processes wide.
Watch one request — then several — move through the assembled stack:
This diagram is the skeleton key. Every "deploy your app" tutorial — every provider dashboard, every YAML file — is some arrangement of exactly these boxes, with vendor paint on top.
Configuration and secrets
The code you deploy must be the same code you run on your laptop — the moment "the production version" is a separate edit, the two drift and you're debugging ghosts. But the two machines need different settings: a different database password, a different hostname, debugging on here and off there. The standard answer is environment variables — key-value pairs the operating system hands to a process at startup, read in Python with os.environ. The Twelve-Factor App methodology canonized this: config lives in the environment, not in the source, precisely because it "varies substantially across deploys [while] code does not."
# set on the server (or in the platform's dashboard) — never in the repo
export DEBUG=false
export SECRET_KEY='use-a-long-random-string-here'
export DATABASE_URL='postgres://app:the-real-password@localhost/school'
export ALLOWED_HOSTS='example.com'
Three rules follow, and Django's own deployment checklist opens with them:
DEBUGis off in production. The debug page that saves you hours locally hands strangers your stack traces, settings, and SQL. Same code, different environment — the simulator below makes the difference visceral.- Secrets never enter Git.
SECRET_KEY, database passwords, API tokens — a repository is a time machine, and a secret committed once is in the history forever, for every future clone. Keep secrets in environment variables (a local.envfile is fine for development — listed in.gitignore). ALLOWED_HOSTSnames your domain. Django refuses requests whoseHostheader isn't on this list — a guard against header-spoofing tricks — so production must list the real name:example.com.
Domains, DNS, and the padlock
The name. Users won't type 203.0.113.7; you buy a domain from a registrar (a few dollars a year) and create an A record — the line in your domain's DNS zone that says example.com → 203.0.113.7. That's the entire trick. The resolution machinery that turns the name into your IP — resolver, root, TLD, nameserver, caching with TTLs — is exactly the chain you stepped through in How browsers work; the A record is you writing the final answer that chain will find.
The padlock. HTTPS requires a TLS certificate: a file, signed by an authority browsers trust, proving that the server answering for example.com is legitimately example.com. Certificates used to cost real money and manual paperwork; since Let's Encrypt (2015) they are free and automated. The ACME protocol lets your server prove domain control and fetch a certificate with no human involved, renewing itself before the ~90-day expiry. Caddy does this entirely by default — give it a domain name and HTTPS just happens — and on nginx a small companion tool (certbot) does the same job. In 2026 there is no reason any site, including your first one, serves plain HTTP.
Your first deploy: rent the boxes pre-assembled
Honest triage for your actual first deploy: use a platform-as-a-service — PythonAnywhere, Render, and Railway are current examples (no step-by-steps here; dashboards change faster than guides). Here's the demystifying part: a PaaS is not an alternative to the stack in section 4 — it is the stack in section 4, pre-assembled. The platform runs the reverse proxy and the TLS automation, starts gunicorn (their docs will ask you for a "start command" — now you can write one), and installs your dependencies by reading the requirements.txt or lock file you learned to produce in How Python packages work. When the dashboard asks for "environment variables", that's section 5 verbatim.
The VPS route — renting the bare computer and assembling nginx + gunicorn + certificates yourself over SSH — is the same boxes by hand. It's worth doing once, later, for the same reason building the micro-framework was worth it: afterwards, nothing about hosting is magic. And one line for completeness: Docker (packaging the boxes into portable images), Kubernetes (orchestrating fleets of them), and CI/CD pipelines (running tests before every auto-deploy) all exist, all sit on top of this same picture, and can all wait.
Takeaways
- Deployment is running your program on an always-on computer with a public IP — a VPS is that computer, SSH is your terminal on it, and code arrives by Git push, not drag-and-drop.
- The dev server is a single debugging-friendly process — its own docs say "do not use in production": one request at a time, leaky DEBUG pages, no TLS, and crashes that stay crashed.
- The production stack is three boxes: a reverse proxy (owns :443, ends TLS, serves static files), a WSGI server (gunicorn master + N workers, self-healing), and your app — the same WSGI callable from the labs.
- Config lives in the environment, not the source: same code everywhere, settings from environment variables — DEBUG off, secrets out of Git,
ALLOWED_HOSTSset to your domain. - A domain is an A record and the padlock is free: point the name at your IP, let ACME (Caddy by default, certbot on nginx) fetch and renew the certificate.
- A PaaS is the same stack pre-assembled — its dashboard fields map one-to-one onto the boxes above, which is why you can now read any of them.
References
- Barnes, R., Hoffman-Andrews, J., McCarney, D., & Kasten, J. (2019). Automatic Certificate Management Environment (ACME) (RFC 8555). Internet Engineering Task Force. https://doi.org/10.17487/RFC8555
- Caddy. (n.d.). Automatic HTTPS. Retrieved August 22, 2026, from https://caddyserver.com/docs/automatic-https
- Django Software Foundation. (n.d.). Deployment checklist (Django documentation). Retrieved August 22, 2026, from https://docs.djangoproject.com/en/stable/howto/deployment/checklist/
- Django Software Foundation. (n.d.). django-admin and manage.py: runserver (Django documentation). Retrieved August 22, 2026, from https://docs.djangoproject.com/en/stable/ref/django-admin/#runserver
- Gunicorn. (n.d.). Design (Gunicorn documentation). Retrieved August 22, 2026, from https://docs.gunicorn.org/en/stable/design.html
- Internet Security Research Group. (n.d.). How it works. Let's Encrypt. Retrieved August 22, 2026, from https://letsencrypt.org/how-it-works/
- nginx. (n.d.). NGINX reverse proxy (NGINX admin guide). Retrieved August 22, 2026, from https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/
- Python Software Foundation. (n.d.). http.server — HTTP servers (Python documentation). Retrieved August 22, 2026, from https://docs.python.org/3/library/http.server.html
- Wiggins, A. (2017). The twelve-factor app: III. Config. Retrieved August 22, 2026, from https://12factor.net/config