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 4

The 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 workers
Django admin site: choose based on the project's configured interface
FastAPI 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: production
Page size: 25
Database configured? True
Database 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 passed
OK: required environment variables present
OK: database migrations ready
OK: health check returns 200
OK: 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-1

Do 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.

python — playgroundlive
⌘/Ctrl + Enter to run

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.

+50 XP on completion