The adage “it works on my machine” is a relic of the past that modern engineering teams can no longer afford. As we step into 2025, the landscape of Python web development has matured significantly. The fragmentation of tooling we experienced in the early 2020s has converged into a set of highly efficient, Rust-powered standards that prioritize speed, reproducibility, and developer experience (DX).
For mid-to-senior Python developers, setting up a development environment is no longer just about pip install. It is about architecting a supply chain for your code—from the first keystroke in your IDE to the final container in a Kubernetes cluster.
In this comprehensive guide, we will build a production-ready Python web development environment from scratch. We will move beyond legacy tools and embrace the 2025 standard stack: uv for package management, Ruff for linting, Docker for isolation, and VS Code Dev Containers for seamless standardized development.
Why This Stack? #
Before we write code, we must justify our architectural choices. In high-performance teams, tool selection is based on benchmarkable metrics: speed, determinism, and maintenance overhead.
The Evolution of Python Tooling #
The shift from pure Python tools to Rust-backed binaries has revolutionized the ecosystem.
| Feature | Pip + Virtualenv (Legacy) | Poetry (2020-2024 Era) | uv (Modern Standard) |
|---|---|---|---|
| Language | Python | Python | Rust |
| Resolution Speed | Slow | Moderate | Instant (<100ms) |
| Lockfile Handling | Manual/Fragile | Good (poetry.lock) |
Excellent (Universal) |
| Python Version Mgmt | External (pyenv) | External | Built-in |
| Disk Usage | High (Duplicate venvs) | High | Optimized (Global Cache) |
| CI/CD Integration | Verbose | Moderate | Seamless |
We are choosing uv because it unifies Python version management, package resolution, and virtual environment creation into a single, incredibly fast binary.
1. Prerequisites and Initial Setup #
To follow this guide, ensure your host machine is equipped with the following:
- Docker Desktop (or OrbStack on macOS) with the Docker CLI available.
- VS Code with the Remote - Containers extension installed.
- Git for version control.
- Terminal Access (Bash/Zsh/PowerShell).
We do not strictly need Python installed on your host machine because uv will handle Python versions for us, and eventually, we will move entirely inside a container.
Installing uv #
If you haven’t migrated yet, install uv.
macOS/Linux:
curl -LsSf https://astral.sh/uv/install.sh | shWindows:
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"2. Project Initialization and Dependency Management #
Let’s simulate a real-world scenario: building a high-performance FastAPI backend service. We will avoid global installations entirely.
Step 2.1: Initialize the Project #
Create your project directory and initialize it with a specific Python version (e.g., Python 3.15).
mkdir pro-web-service
cd pro-web-service
uv init --python 3.15This creates a .python-version file and a basic pyproject.toml.
Step 2.2: Managing Dependencies #
In modern Python, requirements.txt is an artifact for deployment, not a source of truth for development. The pyproject.toml is the standard.
Add our core dependencies (FastAPI and Uvicorn) and development dependencies (Ruff and Pytest).
# Add runtime dependencies
uv add fastapi uvicorn[standard] pydantic-settings httpx
# Add development dependencies (not included in prod builds)
uv add --dev ruff pytest pytest-asyncioStep 2.3: The pyproject.toml Configuration
#
Your pyproject.toml is the heart of your project. Below is a production-grade configuration. It configures the build system and strict linting rules using Ruff.
[project]
name = "pro-web-service"
version = "0.1.0"
description: "High-performance Python Web Service"
readme = "README.md"
requires-python = ">=3.15"
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.30.0",
"pydantic-settings>=2.4.0",
"httpx>=0.27.0",
]
[dependency-groups]
dev = [
"ruff>=0.5.0",
"pytest>=8.3.0",
"pytest-asyncio>=0.23.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# --- Tool Configuration ---
[tool.ruff]
# Target Python 3.15
target-version = "py315"
line-length = 100
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort (import sorting)
"B", # flake8-bugbear (potential bugs)
"UP", # pyupgrade (modernize syntax)
"N", # pep8-naming
"SIM", # flake8-simplify
]
ignore = []
[tool.ruff.lint.isort]
known-first-party = ["app"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
pythonpath = ["."]
testpaths = ["tests"]Pro Tip: The
[tool.ruff]section eliminates the need for separateisortorflake8configurations. Ruff handles import sorting and formatting significantly faster.
3. Developing the Application Skeleton #
Before containerizing, let’s create a minimal verifiable application structure.
mkdir app
touch app/main.pyFile: app/main.py
from fastapi import FastAPI
from pydantic import BaseModel
import uvicorn
app = FastAPI(title="Pro Web Service", version="1.0.0")
class HealthResponse(BaseModel):
status: str
version: str
environment: str = "production"
@app.get("/health", response_model=HealthResponse)
async def health_check():
"""
K8s readiness probe endpoint.
"""
return HealthResponse(
status="ok",
version="1.0.0"
)
if __name__ == "__main__":
# Local debugging entry point
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)You can test this locally by running:
uv run app/main.pyuv run automatically ensures the virtual environment is synced before execution.
4. Containerization Strategy #
Docker is the industry standard for delivery. However, a naive Dockerfile results in bloated images and slow builds. We will use a Multi-Stage Build to separate the build environment from the runtime environment.
The Docker Build Workflow #
Visualizing the build process helps understand where optimization occurs.
The Optimized Dockerfile #
This Dockerfile uses uv for lightning-fast dependency installation and copies only the resulting virtual environment to the final image.
File: Dockerfile
# Stage 1: Builder
FROM python:3.15-slim-bookworm AS builder
# Install uv (The only tool we need)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
# Set working directory
WORKDIR /app
# Enable bytecode compilation for faster startup
ENV UV_COMPILE_BYTECODE=1
# Ensure uv doesn't check for updates during build
ENV UV_NO_CACHE=1
# Copy dependency definition files
COPY pyproject.toml uv.lock ./
# Install dependencies into a virtual environment at /app/.venv
# --frozen ensures we stick exactly to the lockfile
# --no-dev excludes development dependencies like pytest/ruff
RUN uv sync --frozen --no-dev --no-install-project
# Stage 2: Runtime
FROM python:3.15-slim-bookworm
# Create a non-root user for security
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
# Copy the virtual environment from the builder stage
COPY --from=builder /app/.venv /app/.venv
# Copy application code
COPY app ./app
# Set environment variables
# Add the virtual env to PATH so we can use 'uvicorn' directly
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 8000
# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]Docker Compose for Local Services #
Rarely does an app run in isolation. You likely need a database or cache.
File: docker-compose.yml
services:
web:
build: .
ports:
- "8000:8000"
environment:
- ENVIRONMENT=local
volumes:
# Mount source code for hot-reloading (optional in pure Docker, required in Dev Containers)
- ./app:/app/app
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
depends_on:
- redis
redis:
image: redis:7-alpine
ports:
- "6379:6379"5. The Holy Grail: VS Code Dev Containers #
While Docker composes services, Dev Containers compose your development experience. This solves the issue where developers on Windows, macOS, and Linux have different terminal tools, extensions, or environment variables.
With Dev Containers, your IDE runs inside the Docker container.
Step 5.1: Configuration #
Create a .devcontainer directory.
File: .devcontainer/devcontainer.json
{
"name": "Python 3.15 Pro Dev",
"build": {
"dockerfile": "../Dockerfile",
"target": "builder"
},
"customizations": {
"vscode": {
"settings": {
"python.defaultInterpreterPath": "/app/.venv/bin/python",
"python.formatting.provider": "none",
"editor.formatOnSave": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
}
},
"extensions": [
"ms-python.python",
"charliermarsh.ruff",
"tamasfe.even-better-toml",
"redhat.vscode-yaml"
]
}
},
"remoteUser": "root",
"postCreateCommand": "uv sync",
"runArgs": ["--network=pro-web-service_default"]
}Key Concepts in devcontainer.json:
#
- Target: Builder: We use the
builderstage of our Dockerfile because theruntimestage (slim) lacks the build tools and dev dependencies we need for editing code. - Extensions: We enforce that every developer on the team has the Ruff and Python extensions installed automatically.
- Settings: We force
formatOnSaveusing Ruff, ensuring code style consistency across the team regardless of personal preference. - Network: We attach to the docker-compose network so the dev container can talk to Redis/Postgres.
6. Advanced Workflow & Best Practices #
Now that the environment is set up, here is how you leverage it for maximum efficiency.
Lockfile Management #
In the past, pip freeze was messy. With uv, your lockfile (uv.lock) is cross-platform.
When you add a dependency:
uv add requestsThis updates pyproject.toml and regenerates uv.lock instantly. Commit both files to Git.
Performance Profiling #
In a containerized environment, you can’t always just attach a GUI profiler. However, with this setup, you can use CLI tools easily.
Since we are in a Dev Container, we can install profiling tools without polluting a global namespace:
uv add --dev py-spyTo generate a flame graph of your running app:
# Assuming the app is running in the background with PID 123
sudo py-spy record -o profile.svg --pid 123Handling Secrets #
Never commit .env files. In this setup:
- Local: Use a
.envfile (gitignored).pydantic-settingsreads this automatically. - Docker: Pass via
env_fileindocker-compose.yml. - Production: Inject secrets as environment variables via your orchestrator (Kubernetes/AWS ECS).
7. Common Pitfalls and Solutions #
| Problem | Cause | Solution |
|---|---|---|
| “Module not found” inside Docker | Virtual Environment path mismatch | Ensure PATH env var includes /.venv/bin in the Dockerfile. |
| Slow Docker Builds | COPY . . before installing deps |
Always copy pyproject.toml and uv.lock first, install deps, then copy source code. |
| File Permission Errors | Running as root vs non-root | Use chown in Dockerfile or map user IDs in devcontainer.json. |
| Windows Line Endings (CRLF) | Git autocrlf settings | Add a .gitattributes file forcing * text=auto eol=lf. |
8. Conclusion #
By adopting this stack—uv for package management, Ruff for linting, and Dev Containers for environment isolation—you have eliminated a vast class of configuration problems.
You have moved from “it works on my machine” to “it works in the container,” which is effectively the same as “it works in production.”
Key Takeaways:
- Speed: Rust-based tooling (uv, Ruff) saves hours of waiting time per week.
- Isolation: Docker multi-stage builds keep production images tiny and secure.
- Consistency: Dev Containers enforce tooling versions and IDE settings via code.
As we progress through 2025, the focus of Python development is shifting towards these integrated, high-performance workflows. The time spent setting this up today will pay dividends in velocity and stability for years to come.