Chapter 8 — Scenario: Python Development Environment
Installing Python packages directly on Windows can quickly become a mess —
conflicting versions between projects, packages that behave differently on
Windows vs Linux, and the constant temptation to install things globally and
forget about them. A Docker-based development environment solves all of this:
every project gets its own clean container with exactly the right Python version
and dependencies, isolated from everything else, and guaranteed to behave the
same way on any machine.
In this chapter you'll build a FastAPI development environment in Docker,
connect VS Code to it so you can edit, run, and debug code as if Python were
installed locally — because as far as VS Code is concerned, it is.
The problem with installing Python directly on Windows
Without Docker
• Multiple Python versions conflict (3.10, 3.11, 3.12)
• pip installs are global unless you remember venv
• Package built for Linux doesn't work on Windows
• "It works on my machine" — breaks on the server
• Setting up a second machine means reinstalling everything
• Cleaning up a failed project is painful
With Docker
• Each project has its own Python version, pinned exactly
• Dependencies are inside the container — nothing leaks
• Linux container → same behaviour as your Linux server
• New machine: pull the image, open VS Code, done
• Delete the container → environment is completely gone
• requirements.txt is the only record you need
We'll build this in two stages: first a basic development container you can
use from the terminal, then the VS Code Dev Containers integration that makes
it feel like a native Python install.
fastapi-project/
├── .devcontainer/ # VS Code Dev Containers config
│ └── devcontainer.json
├── app/
│ ├── main.py # FastAPI application
│ └── __init__.py
├── tests/
│ └── test_main.py
├── Dockerfile # Development image
├── Dockerfile.prod # Production image (leaner)
├── requirements.txt # Runtime dependencies
├── requirements-dev.txt # Dev-only (pytest, black, etc.)
└── .dockerignore
A development Dockerfile is different from a production one — it prioritises
iteration speed and developer tools over a small image size.
# Dockerfile (development)
FROM python:3.12-slim
# Install system tools useful during development
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl git vim \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
# Install runtime dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Install dev-only dependencies (testing, linting, formatting)
COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements-dev.txt
# Don't copy source here — we'll bind-mount it for live editing
EXPOSE 8000
# Start uvicorn with --reload so it restarts on file changes
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
# requirements.txt — runtime
fastapi==0.111.0
uvicorn[standard]==0.30.1
pydantic==2.7.1
# requirements-dev.txt — development only
pytest==8.2.2
pytest-asyncio==0.23.7
httpx==0.27.0 # for testing FastAPI routes
black==24.4.2 # code formatter
ruff==0.4.8 # fast linter
mypy==1.10.0 # type checker
# .dockerignore
.git
__pycache__
*.pyc
.env
.env.*
.venv
venv
.pytest_cache
.mypy_cache
.ruff_cache
*.egg-info
dist/
.devcontainer/
Before setting up VS Code integration, confirm everything works from the
terminal. The key technique here is bind-mounting your source code so that
uvicorn's --reload picks up every file change instantly.
Terminal — build and run with live reload
# Build the dev image
$ docker build -t fastapi-dev:latest .
[+] Building 14.2s (8/8) FINISHED
# Run with source code bind-mounted
# Windows PowerShell — use ${PWD} for the current directory
$ docker run -d `
--name fastapi-dev `
-p 8000:8000 `
-v "${PWD}:/workspace" `
fastapi-dev:latest
a7c3f9...
# Confirm it started and auto-reload is active
$ docker logs fastapi-dev
INFO: Will watch for changes in these directories: ['/workspace']
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [1] using WatchFiles
INFO: Started server process [8]
INFO: Application startup complete.
# Edit app/main.py on your Windows machine — uvicorn sees the change
WARNING: WatchFiles detected changes in 'app/main.py'. Reloading...
INFO: Application startup complete.
No rebuild needed when you edit code. Because the source is
bind-mounted, changes on your Windows filesystem are immediately visible inside
the container. You only rebuild the image when you change
requirements.txt (to install new packages).
Running commands inside the dev container
Terminal — one-off commands in the container
# Run the test suite
$ docker exec fastapi-dev pytest tests/ -v
========================= test session starts ==========================
tests/test_main.py::test_root_returns_200 PASSED [100%]
========================== 1 passed in 0.42s ===========================
# Format all code with black
$ docker exec fastapi-dev black app/
reformatted app/main.py
All done! ✨ 🍰 ✨
# Run the linter
$ docker exec fastapi-dev ruff check app/
All checks passed!
# Open an interactive shell inside the container
$ docker exec -it fastapi-dev bash
root@a7c3f9:/workspace# python -c "import fastapi; print(fastapi.__version__)"
0.111.0
root@a7c3f9:/workspace# exit
# Install a new package (then add it to requirements.txt)
$ docker exec fastapi-dev pip install sqlalchemy
# Don't forget: also add sqlalchemy to requirements.txt and rebuild
The terminal workflow is powerful but you still edit files in Windows while
running them in Linux. VS Code Dev Containers goes further: VS Code itself
connects inside the container. Your editor, IntelliSense, debugger,
linter, and terminal all run in the same Linux environment as the application.
There's no mismatch — what VS Code sees is exactly what runs.
🖥️
Windows Host
Displays the VS Code window. Stores your files. Nothing else.
⬡
VS Code Server
Runs INSIDE the container. Has Python, linters, debugger.
🐳
Dev Container
Linux · Python 3.12 · All packages · uvicorn --reload
Prerequisites
- VS Code with the Dev Containers extension installed
(
ms-vscode-remote.remote-containers)
- Docker Desktop running
- That's it — no Python on Windows required
The devcontainer.json file
Create a .devcontainer/devcontainer.json file in your project.
This tells VS Code how to build and configure the dev container:
{
"name": "FastAPI Dev",
// Use our Dockerfile rather than a pre-built image
"build": {
"dockerfile": "../Dockerfile",
"context": ".."
},
// Forward port 8000 from the container to localhost:8000
"forwardPorts": [8000],
// VS Code extensions to install inside the container
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.black-formatter",
"charliermarsh.ruff",
"ms-python.mypy-type-checker",
"humao.rest-client"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"editor.formatOnSave": true,
"editor.defaultFormatter": "ms-python.black-formatter",
"terminal.integrated.defaultProfile.linux": "bash"
}
}
},
// Run uvicorn automatically when the container starts
"postStartCommand": "uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload &",
// Mount the project at /workspace (default for Dev Containers)
"workspaceFolder": "/workspace",
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached"
}
Opening the project in a Dev Container
Three ways to open:
1. Open VS Code in your project folder → a notification appears bottom-right:
"Reopen in Container" — click it.
2. Press F1 → type Dev Containers: Reopen in Container → Enter.
3. Click the blue >< icon in the bottom-left corner of VS Code
→ Reopen in Container.
VS Code builds the image (first time only, ~30s), starts the container,
installs the extensions inside it, and reconnects. The status bar shows
Dev Container: FastAPI Dev in blue when you're inside the
container. From here, the integrated terminal is a Linux bash shell inside
the container — python, pip, pytest
all run there.
The first open is slow; every subsequent open is instant.
The image is cached — Docker only rebuilds it if you change the Dockerfile
or requirements files. When you close VS Code and reopen the folder later,
it reconnects to the existing container (or starts a fresh one in seconds
if you stopped it).
🔍
IntelliSense & autocomplete
The Python extension runs inside the container and sees all installed packages. Import completions, type hints, and docstrings work for FastAPI, Pydantic, and everything in requirements.txt.
🐛
Full debugger
Set breakpoints in VS Code, press F5, and the debugger attaches to the running FastAPI process inside the container. Step through code, inspect variables, evaluate expressions — as if Python were local.
🧪
Test explorer
The Testing panel discovers and runs pytest tests inside the container. Click the play button next to any test to run it individually, with pass/fail shown inline in the editor.
✨
Format on save
The devcontainer.json enables black formatting on every save. Code is automatically reformatted when you press Ctrl+S — no manual black invocation needed.
🔴
Inline linting
ruff and mypy highlight problems directly in the editor as you type. Unused imports, type mismatches, and style violations show up with squiggly underlines before you even save.
🔗
Port forwarding
Port 8000 is automatically forwarded. Visit http://localhost:8000 or http://localhost:8000/docs in your Windows browser and reach the FastAPI server running inside the container.
Configuring the debugger
Add a .vscode/launch.json to configure the debugger for
FastAPI (this file lives on the Windows side, VS Code reads it when connecting
to the container):
{
"version": "0.2.0",
"configurations": [
{
"name": "FastAPI (uvicorn)",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": ["app.main:app", "--host", "0.0.0.0", "--port", "8000"],
"jinja": true,
"justMyCode": true
}
]
}
Don't run uvicorn --reload when debugging. The reloader
forks a child process that the debugger can't attach to. Stop the auto-reload
uvicorn first (Ctrl+C in the terminal), then press F5 to start
under the debugger.
The development image has git, vim, pytest, black, and mypy — none of which
belong in production. Keep a separate Dockerfile.prod for
deploying the application:
# Dockerfile.prod (production — lean and non-root)
FROM python:3.12-slim
WORKDIR /app
# Runtime deps only — no dev tools
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application source (not the whole project root)
COPY app/ ./app/
# Run as non-root
RUN useradd -m appuser && chown -R appuser /app
USER appuser
EXPOSE 8000
# No --reload in production
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
Terminal — build and compare dev vs prod image sizes
$ docker build -t fastapi-dev:latest -f Dockerfile .
$ docker build -t fastapi-prod:latest -f Dockerfile.prod .
$ docker images fastapi-dev fastapi-prod --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY TAG SIZE
fastapi-dev latest 312MB ← dev tools add ~150MB
fastapi-prod latest 161MB ← only what's needed to run
FastAPI applications typically read configuration from environment variables
(database URLs, secret keys, API keys). Never put these in the Dockerfile —
use a .env file for local development and pass them at runtime.
# .env (local development — add to .dockerignore and .gitignore!)
DATABASE_URL=sqlite:///./dev.db
SECRET_KEY=dev-secret-key-not-for-production
DEBUG=true
# Pass the .env file when starting the container
docker run -d \
--name fastapi-dev \
-p 8000:8000 \
-v "${PWD}:/workspace" \
--env-file .env \
fastapi-dev:latest
# Or pass individual variables with -e
docker run -d \
--name fastapi-dev \
-p 8000:8000 \
-v "${PWD}:/workspace" \
-e DATABASE_URL=sqlite:///./dev.db \
-e SECRET_KEY=dev-secret \
fastapi-dev:latest
Read them in FastAPI using Pydantic's BaseSettings:
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str = "sqlite:///./dev.db"
secret_key: str
debug: bool = False
class Config:
env_file = ".env"
settings = Settings()
Dev Containers and .env files: Add an envFile
entry to devcontainer.json to automatically load the .env file
when VS Code opens the container:
"runArgs": ["--env-file", "${localWorkspaceFolder}/.env"]
Exercises
- Build the dev environment. Create the project structure from Step 1. Write a minimal
app/main.py with a single GET / route that returns {"message": "hello"}. Build the dev image and run it with the bind mount. Edit the return message, save the file, and watch uvicorn's reload message appear in docker logs -f fastapi-dev — confirm the change shows at http://localhost:8000 without restarting the container.
- Add a route and run the tests. Add a
GET /items/{item_id} route that returns {"item_id": item_id}. Write a test in tests/test_main.py using httpx.AsyncClient that calls that route and asserts the response. Run docker exec fastapi-dev pytest tests/ -v and confirm the test passes. Change the route to return a wrong value and watch the test fail.
- Set up VS Code Dev Containers. Install the Dev Containers extension, create the
.devcontainer/devcontainer.json from Step 4, and use Reopen in Container. Open app/main.py and confirm that typing from fastapi import gives IntelliSense completions for FastAPI classes. Open the integrated terminal and run python --version — confirm it reports Python 3.12 (Linux, not your Windows Python).
- Use the debugger. Add the
.vscode/launch.json from Step 5. Stop the auto-running uvicorn in the Dev Container terminal (Ctrl+C), then press F5 to start under the debugger. Set a breakpoint on the return line of your / route, visit http://localhost:8000 in your browser, and confirm VS Code pauses at the breakpoint and shows the local variables. Press F5 to continue.
- Compare dev and prod image sizes. Build both
Dockerfile and Dockerfile.prod. Run docker image history fastapi-prod:latest and confirm there are no pytest, black, ruff, or mypy layers — only the runtime packages. Note the size difference. Consider: which other developer tools might you be tempted to leave in a production image by accident?
Next: Chapter 9 — Scenario: Working with the Claude API
Chapter 9 builds a dedicated Claude API development container: a Python
environment pre-configured for working with the Anthropic SDK, with safe
.env key handling so your API key never ends up baked into an
image or committed to git, plus a simple script structure for running
Claude-powered experiments from the terminal or VS Code.