Skip to content

All lessons

Area 07 // DeploymentLesson 108 / 14013 min read

What deploying means, and environment configs

Deploying means taking a specific version of your app from your laptop to servers where real users can reach it, usually through separate environments (development, staging and production) that run the same code with different configuration supplied from outside the code.

Think of it like this

Analogy

A theatre show rehearses in a practice room (dev), then has a full dress rehearsal on the real stage with lights and costumes but no paying audience (staging), then opens to the public (production). It's the same script every time; what changes is the room, the lighting cues and who's watching.

The problem it solves

"It works on my machine" isn't a product. Real users need your app running on servers, reachable at a URL, 24 hours a day, with real keys, real data and enough capacity. Getting there safely means testing in something close to production before users see it, and running the same build everywhere so what you tested is what you ship, with settings that differ per environment kept out of the code.

How it works: dev, staging and prod

  1. Deploying means: build a specific version (an image from a commit: Docker), place it on servers, start it with the right configuration, route traffic to it once healthy (health checks), and monitor it (alerts).
  2. Development: your laptop or a dev container; mock or cheap models, fake data, debug logging.
  3. Staging: production-like infrastructure and config, test data, its own keys and budgets; every merge to main deploys here automatically; eval gates and smoke tests run against it.
  4. Production: real users and data; deploys are promoted from staging (same image), often gradually (release strategies).
  5. Optional extras: preview environments per pull request, a load-test environment.
  6. Keep them similar (same image, same infrastructure code: IaC) so staging results predict production.

How it works: environment configs

  1. Separate config from code (the "twelve-factor" principle): anything that varies between environments comes from outside: environment variables, mounted config files, a config service.
  2. What's config in an AI app: provider and model names, max_tokens and budgets, feature flags, database and vector-store URLs, rate limits, log level, and secrets (from a secrets manager).
  3. One settings object: load and validate config at start-up into a typed object (Pydantic Settings is a common choice); refuse to start if something's missing or invalid.
  4. Sensible defaults for dev, explicit values for staging and prod.
  5. Version non-secret config in git (reviewed like code); keep secrets out.
  6. Locally, a git-ignored .env file plus a committed .env.example listing every variable.
One image, three environments; only the configuration changes.

See it in code

Needs
Nothing: plain Python 3. Pydantic Settings does the same with less code.

Typed settings per environment, overridable by environment variables, that refuse to start without a required secret.

settings.py
import os
from dataclasses import dataclass
 
# One codebase, many environments: everything that differs between dev, staging and prod comes from config.
DEFAULTS = {
    "dev":     {"LLM_PROVIDER": "mock",      "LLM_MODEL": "",                 "LOG_LEVEL": "DEBUG", "MAX_TOKENS": "500"},
    "staging": {"LLM_PROVIDER": "anthropic", "LLM_MODEL": "claude-haiku-5-5", "LOG_LEVEL": "INFO",  "MAX_TOKENS": "500"},
    "prod":    {"LLM_PROVIDER": "anthropic", "LLM_MODEL": "claude-haiku-5-5", "LOG_LEVEL": "WARNING", "MAX_TOKENS": "800"},
}
 
@dataclass(frozen=True)
class Settings:
    env: str
    llm_provider: str
    llm_model: str
    log_level: str
    max_tokens: int
 
def load_settings() -> Settings:
    env = os.environ.get("APP_ENV", "dev")
    cfg = {**DEFAULTS[env], **{k: v for k, v in os.environ.items() if k in DEFAULTS[env]}}  # env vars win
    if cfg["LLM_PROVIDER"] != "mock" and not os.environ.get("ANTHROPIC_API_KEY"):
        raise RuntimeError(f"[{env}] ANTHROPIC_API_KEY is missing")      # fail fast at start-up
    return Settings(env, cfg["LLM_PROVIDER"], cfg["LLM_MODEL"], cfg["LOG_LEVEL"], int(cfg["MAX_TOKENS"]))
 
print(load_settings())
os.environ.update(APP_ENV="prod", MAX_TOKENS="600")
try:
    load_settings()
except RuntimeError as e:
    print("refused to start:", e)
os.environ["ANTHROPIC_API_KEY"] = "placeholder-from-secret-store"
print(load_settings())
Output
Settings(env='dev', llm_provider='mock', llm_model='', log_level='DEBUG', max_tokens=500)
refused to start: [prod] ANTHROPIC_API_KEY is missing
Settings(env='prod', llm_provider='anthropic', llm_model='claude-haiku-5-5', log_level='WARNING', max_tokens=600)

MAX_TOKENS=600 from the environment overrode the prod default of 800, without touching code. Model IDs are as of November 2026.

A real example

Example

The staging environment that paid for itself

A team deployed straight from laptops to production. A release that switched to a new embedding model worked locally (where the index was rebuilt), but production still had the old index: every search returned nonsense for 40 minutes.

They added a staging environment built from the same infrastructure code, with its own copy of a sanitised index and its own provider keys. Merges to main deploy to staging, where smoke tests and the eval gate run against the real stack; production gets the identical image only after staging passes. The next embedding change failed in staging, not in front of customers.

The easy way

Start here

Start with two environments: local and production, plus preview deploys if your host offers them. Put every setting in environment variables, list them in .env.example, load them once at start-up with validation, and use separate keys for local and production.

When you grow: the production setup

Teams run dev, staging and prod (plus per-PR previews) from shared infrastructure code, promote one immutable image through them, keep non-secret config in versioned files with secrets injected from a manager, validate typed settings at start-up, use separate accounts or projects per environment for isolation, and alert on config drift.

Trade-offs

What you gain

  • Problems surface in staging, not in front of users.
  • Same build everywhere means fewer surprises.
  • Config changes don't need code changes.

What it costs you

  • Extra environments cost money and upkeep.
  • Staging is never perfectly like production (data, traffic).
  • Many settings across environments can become confusing.

When not to use it

  • A weekend prototype with no users can skip staging; still keep config out of code.

Common mistakes

  • Building a different image per environment.
  • Hard-coding model names, URLs or limits in many files.
  • Sharing one API key between dev and prod.
  • Discovering a missing setting only when a user hits that code path.
  • Staging so different from prod that it proves nothing.

Interview questions

Try answering out loud first, then open the model answer.

What does 'deploying' an AI application involve?

Model answer

Building a versioned artifact (usually a container image) from a specific commit, provisioning or updating infrastructure, starting the new version with environment-specific config and secrets, verifying health and readiness, shifting traffic gradually, monitoring quality, latency, errors and cost, and being able to roll back quickly.

How do you manage configuration across environments?

Model answer

Keep one codebase and one image; supply environment-specific values via environment variables or mounted config, with secrets from a secrets manager; validate everything at start-up into a typed settings object; version non-secret config in git; document variables in an example file; and use separate credentials and accounts per environment.

Why have a staging environment?

Model answer

To test the exact release candidate on production-like infrastructure, config and integrations before users see it: running smoke tests, eval gates and migrations, catching environment-specific failures (missing secrets, index mismatches, permissions) and practising deployments and rollbacks safely.

Quiz

Pick an answer to see whether it’s right and why.

  1. 01What is a staging environment for?
  2. 02What should differ between staging and production?
  3. 03Why validate config at start-up?
  4. 04Where should the model name and max_tokens for production come from?
  5. 05Why shouldn't dev use the production API key?

0 of 5 answered

Key terms

Deployment
Putting a specific version of your app onto servers with the right configuration, sending traffic to it once healthy, and watching it.
Environment (dev, staging, prod)
A separate place where the app runs with its own config, data and keys, such as development, staging for testing, and production for real users.
Staging
A production-like environment where a release is tested with real infrastructure but no real users before it goes to production.
Production
The environment real users use. Changes reach it only after passing tests, evals and staging.
Environment variable
A named value set outside your code (in the shell, a .env file or a server's settings), used for secrets and config.
Twelve-factor app
A widely used set of principles for web services; one of them is keeping configuration in the environment, separate from the code.

Before and after

Progress is saved in this browser only.