Deployment
Expert · 20 min read · ▶ live playground · ✦ checkpoint
Your app is not really a web app until someone can reach it without sitting at your laptop. Deployment is making a Python application available outside your development machine with a server process and configuration that fit real users.
This lesson does not start servers or run external tools. The live Python examples model configuration and release checks with the standard library; every command-shaped or infrastructure-shaped example is static.
From your laptop to production
A development server is the local server you use while building, optimized for fast feedback and helpful errors. A production server is the server process used for real users, optimized for reliability, controlled logging, concurrency, and predictable restarts.
Think of a development server as a kitchen tasting spoon and a production server as the serving counter. The tasting spoon is perfect while cooking, but you do not serve a restaurant from it.
Static shape only - not run in this browser lesson:
# Development shape
fastapi dev app.py
flask --app app run --debug
python manage.py runserver
# Production shape
uvicorn app:app --host 0.0.0.0 --port 8000
gunicorn "app:create_app()" --workers 4
gunicorn app:app --worker-class uvicorn_worker.UvicornWorker --workers 4The exact command depends on your framework and host, the machine or platform running your app. The big idea is stable: development tools help you build; production tools keep the app reachable.
WSGI, ASGI, Uvicorn, and Gunicorn
WSGI is the older Python web server interface for synchronous web apps, and ASGI is the newer interface that also supports async apps and long-lived connections. An interface is a contract between two pieces of software, like a standard plug shape: the web server knows how to call your app, and your app knows what shape to answer with.
Uvicorn is an ASGI server often used to run FastAPI and other async-capable Python apps. Gunicorn is a production process manager, a tool that starts multiple worker processes and restarts them if needed; it can run WSGI apps directly or manage ASGI workers through the separate uvicorn-worker worker class.
apps = [
{"name": "Flask catalog", "interface": "WSGI", "async": False},
{"name": "Django admin site", "interface": "WSGI or ASGI", "async": False},
{"name": "FastAPI JSON API", "interface": "ASGI", "async": True},
]
for app in apps:
if app["interface"] == "ASGI":
server = "Uvicorn, or Gunicorn with uvicorn-worker"
elif app["interface"] == "WSGI":
server = "Gunicorn with WSGI workers"
else:
server = "choose based on the project's configured interface"
print(f"{app['name']}: {server}")Flask catalog: Gunicorn with WSGI workersDjango admin site: choose based on the project's configured interfaceFastAPI JSON API: Uvicorn, or Gunicorn with uvicorn-worker
You do not need to memorize every server flag. You need the map: framework app -> WSGI or ASGI interface -> production server process -> host.
Environment configuration
Environment configuration is settings your app reads from its running environment instead of hard-coded source code. An environment variable is a named value supplied by the operating system or hosting platform, like DATABASE_URL or APP_ENV.
A secret is a sensitive value such as an API token, password, signing key, or database credential. A config value is a non-secret setting such as APP_ENV=production or PAGE_SIZE=50. Never hard-code secrets into Python files, commit them to Git, or print them anywhere.
A local .env file is a developer-only text file that stores environment variables for your machine. It is a convenience, not a deployment strategy, and it should stay out of Git when it contains secrets.
import os
sample_environment = {
"APP_ENV": "production",
"PAGE_SIZE": "25",
"DATABASE_URL": "postgresql://app:<secret>@db.example/app",
}
old_environment = os.environ.copy()
try:
os.environ.update(sample_environment)
app_env = os.environ.get("APP_ENV", "development")
page_size = int(os.environ.get("PAGE_SIZE", "20"))
has_database_url = "DATABASE_URL" in os.environ
print("Environment:", app_env)
print("Page size:", page_size)
print("Database configured?", has_database_url)
print("Database URL:", "<hidden>")
finally:
os.environ.clear()
os.environ.update(old_environment)Environment: productionPage size: 25Database configured? TrueDatabase URL: <hidden>
That last line is the habit: prove the secret exists without showing it. Your app should be observable without leaking the keys.
Containers and Docker
A container is a packaged runtime environment for an app: code, Python, dependencies, and startup command bundled into a repeatable unit. An image is the built template for a container, like a packed shipping crate; a container is a running crate opened into a process.
Docker is a common tool for building and running containers. A Dockerfile is a recipe for creating an image, usually saying which base image to start from, what files to copy, what dependencies to install, and what command starts the app.
Static shape only - not run in this browser lesson:
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt ./
RUN python -m pip install --no-cache-dir -r requirements.txt
COPY . .
ENV APP_ENV=production
CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]This shape assumes requirements.txt includes the server dependency, such as uvicorn. Containers do not remove the need for good configuration. You still pass secrets at runtime, keep images rebuildable, run migrations deliberately, and read logs when something breaks.
Cloud deployment pieces
A cloud platform is rented infrastructure that runs your app on someone else's machines. A build is the step that turns your source code into something runnable, such as installing dependencies or creating a container image. A runtime is where the built app actually runs, such as a virtual machine, container host, or managed app host.
Logs are timestamped records from your running app and platform. A health check is a small endpoint or command that tells the platform whether the app is alive. A domain is the human-friendly address like api.example.com, and TLS is the encryption layer behind https://.
Database migrations are controlled changes to database shape, and a rollback is returning to a previous working release when the current one fails. Deployment is a release line, not one magic command.
release = {
"build_passed": True,
"env_vars_present": ["APP_ENV", "DATABASE_URL", "SECRET_KEY"],
"required_env_vars": ["APP_ENV", "DATABASE_URL", "SECRET_KEY"],
"migrations_ready": True,
"health_status": 200,
"rollback_version": "2026.07.03-1",
}
checks = [
("build passed", release["build_passed"]),
(
"required environment variables present",
set(release["required_env_vars"]).issubset(release["env_vars_present"]),
),
("database migrations ready", release["migrations_ready"]),
("health check returns 200", release["health_status"] == 200),
("rollback version recorded", bool(release["rollback_version"])),
]
for name, passed in checks:
marker = "OK" if passed else "BLOCKED"
print(f"{marker}: {name}")OK: build passedOK: required environment variables presentOK: database migrations readyOK: health check returns 200OK: rollback version recorded
Real platforms spell the commands differently, but the concerns stay recognizable:
Static shape only - not run in this browser lesson:
# Build
cloud build --from .
# Configure runtime environment
cloud env set APP_ENV=production
cloud env set DATABASE_URL=<managed database secret>
# Release
cloud deploy --image app:2026.07.03-2
# Observe
cloud logs --service book-api
cloud health --service book-api
# Recover
cloud rollback --to app:2026.07.03-1Do not judge a deployment story only by whether the first deploy succeeds. Judge it by whether you can see what is running, prove it is healthy, apply database changes safely, and recover when a release is wrong.
Try it - model a health response
This playground builds a small health-check response from app state. Change database_ok to False and rerun it; a platform would treat that as a signal to stop sending traffic or alert the team.
A health check should be boring, fast, and honest. It is not a full test suite; it is a signal that the running service can do its most basic work.
Checkpoint
Answer all three to mark this lesson complete
Section 23 now fits together: HTTP gives the request-response conversation, clients consume APIs carefully, servers handle routes and validate data, and deployment makes that server reachable and observable. From here, Python can talk to the web, serve the web, and ship real tools into the world.