| name | devcontainer-workflow |
| description | DevContainer configuration for consistent development environments with Docker, multi-stage builds, non-root users, environment management, Docker-in-Docker support, and Python with uv. Activate when working with .devcontainer/, devcontainer.json, Dockerfile, or container-based development workflows. |
DevContainer Workflow
Guidelines for setting up and maintaining consistent development environments using DevContainers with Docker, featuring non-root user configurations, environment management, multi-stage builds, and Python-focused patterns.
Quick Start Checklist
- Create
.devcontainer/directory structure - Configure
devcontainer.jsonwith runtime settings - Create multi-stage
Dockerfilefor development environment - Set up non-root
vscodeuser with proper permissions - Configure volume mounts (home, SSH, docker socket)
- Create
.env.exampletemplates for configuration - Set up
postCreateCommandwith Makefile integration - Configure VS Code extensions in customizations
- Test container build and entry
- Verify all development tools are accessible
Directory Structure
project/
├── .devcontainer/
│ ├── devcontainer.json # Container runtime configuration
│ ├── Dockerfile # Multi-stage development build
│ ├── .env.example # Dev-specific config template
│ └── .env # Dev-specific config (gitignored)
├── .env.example # Shared config template (committed)
├── .env # Shared config (gitignored)
├── Makefile # Development task automation
└── pyproject.toml # Python project configuration
Container Configuration
Multi-Stage Dockerfile Pattern
Use multi-stage builds to separate development, testing, and production stages:
# Base stage with common dependencies
FROM python:3.14-slim AS base
RUN apt-get update && apt-get install -y --no-install-recommends \
bash \
curl \
git \
make \
zsh \
zsh-autosuggestions \
zsh-syntax-highlighting \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Development stage with full tooling
FROM base AS development
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID
# Create non-root user
RUN groupadd --gid $USER_GID $USERNAME && \
useradd --uid $USER_UID --gid $USER_GID -m $USERNAME -s /bin/zsh && \
apt-get update && apt-get install -y --no-install-recommends \
sudo \
vim \
&& rm -rf /var/lib/apt/lists/* && \
echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME && \
chmod 0440 /etc/sudoers.d/$USERNAME
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /workspace
USER $USERNAME
# Configure shell
RUN echo 'source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh' >> ~/.zshrc && \
echo 'source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh' >> ~/.zshrc && \
echo 'eval "$(uv generate-shell-completion zsh)"' >> ~/.zshrc
# Production stage (minimal footprint)
FROM base AS production
RUN useradd --create-home --shell /bin/bash appuser
WORKDIR /app
USER appuser
COPY --from=development /workspace /app
RUN /app/.venv/bin/pip install --no-cache-dir -e .
Python 3.14+ with UV Installation
Ensure uv is installed in development stage:
# Install uv package manager
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
# (Later in USER vscode section)
# Copy project files for dependency installation
COPY --chown=vscode:vscode pyproject.toml uv.lock* ./
RUN uv sync --no-dev || uv sync
Non-Root User Configuration
User Setup in Dockerfile
Always use a non-root user for security. Standard name is vscode:
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID
# Create user with home directory and shell
RUN groupadd --gid $USER_GID $USERNAME && \
useradd --uid $USER_UID --gid $USER_GID -m $USERNAME -s /bin/zsh && \
apt-get update && apt-get install -y --no-install-recommends sudo && \
echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME && \
chmod 0440 /etc/sudoers.d/$USERNAME && \
rm -rf /var/lib/apt/lists/*
# Set user for all subsequent commands
USER $USERNAME
Permissions for Mounted Volumes
Ensure proper ownership of workspace and home directories:
# After copying project files
COPY --chown=vscode:vscode . /workspace
# Ensure workspace permissions
RUN mkdir -p /workspace && chown -R vscode:vscode /workspace
devcontainer.json User Configuration
{
"remoteUser": "vscode"
}
Environment Management
.env File Strategy
Use multiple .env files for different scopes:
- Project root
.env- Shared across all environments (gitignored) .devcontainer/.env- Development-specific overrides (gitignored).env.example- Template for shared config (committed).devcontainer/.env.example- Template for dev config (committed)
Root .env.example
# Shared configuration (safe for version control)
PROJECT_NAME=my-project
LOG_LEVEL=info
PYTHON_VERSION=3.14
DEBUG=false
.devcontainer/.env.example
# Development-specific settings
DEBUG=true
LOG_LEVEL=debug
PYTHONUNBUFFERED=1
HOT_RELOAD=true
Loading Environment in devcontainer.json
{
"runArgs": [
"--env-file", "${localWorkspaceFolder}/.env",
"--env-file", "${localWorkspaceFolder}/.devcontainer/.env"
]
}
Order matters: later files override earlier ones. Local .devcontainer/.env overrides shared .env.
Docker-in-Docker Support
When to Use
- Building Docker images within the devcontainer
- Running integration tests with containers
- Testing Docker Compose configurations
- Local CI/CD pipeline simulation
Configuration
Add Docker-in-Docker feature to devcontainer.json:
{
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest",
"moby": true
}
},
"mounts": [
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
]
}
Security Considerations
Mounting the Docker socket grants the container full Docker daemon access:
- Use only in trusted development environments
- Never use in untrusted code environments
- Ensure user in container is trusted
- Consider using Docker contexts for isolation if running sensitive containers
Testing with Docker Compose
.PHONY: test-integration
test-integration:
docker compose -f docker-compose.test.yml up -d
uv run pytest tests/integration/ -v --tb=short
docker compose -f docker-compose.test.yml down
.PHONY: build-image
build-image:
docker build -t my-app:latest --target production .
Development Tools
Shell: Zsh Configuration
Configure zsh with plugins for better development experience:
RUN apt-get update && apt-get install -y --no-install-recommends \
zsh \
zsh-autosuggestions \
zsh-syntax-highlighting \
&& rm -rf /var/lib/apt/lists/*
USER vscode
RUN echo 'source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh' >> ~/.zshrc && \
echo 'source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh' >> ~/.zshrc && \
echo 'setopt HIST_FIND_NO_DUPS' >> ~/.zshrc && \
echo 'setopt SHARE_HISTORY' >> ~/.zshrc
VS Code Extensions
Configure recommended extensions in devcontainer.json:
{
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.black-formatter",
"charliermarsh.ruff",
"ms-azuretools.vscode-docker",
"eamodio.gitlens",
"ms-vscode.makefile-tools",
"GitHub.copilot"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.formatting.provider": "black",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"terminal.integrated.defaultProfile.linux": "zsh"
}
}
}
}
Essential Development Tools
Include in Dockerfile for all development environments:
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
git \
curl \
wget \
jq \
&& rm -rf /var/lib/apt/lists/*
Volume Mounts
Home Directory Persistence
Preserve shell history, configurations, and VS Code data across container rebuilds:
{
"mounts": [
"source=${localWorkspaceFolderBasename}-home,target=/home/vscode,type=volume"
]
}
Benefits of dynamic naming with ${localWorkspaceFolderBasename}:
- Unique volume per workspace (supports multiple projects)
- Prevents conflicts with other projects
- Clear naming:
project-name-home
SSH Key Access
Enable git operations and remote access with SSH keys:
{
"mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,readonly"
]
}
On Windows, use:
{
"mounts": [
"source=${localEnv:USERPROFILE}\\.ssh,target=/home/vscode/.ssh,type=bind,readonly"
]
}
Configure SSH for git:
# In devcontainer or post-create
ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts 2>/dev/null
git config --global core.sshCommand "ssh -i ~/.ssh/id_rsa"
Docker Socket for DinD
{
"mounts": [
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
]
}
Add user to docker group in container:
RUN groupadd docker || true && \
usermod -aG docker vscode
Post-Create Commands
Makefile-Driven Initialization
Use postCreateCommand to invoke a Makefile target:
{
"postCreateCommand": "make initialize"
}
Makefile Example
.PHONY: initialize
initialize: deps env-setup
@echo "Development environment initialized"
.PHONY: deps
deps:
@echo "Installing dependencies with uv..."
uv sync --extra dev
.PHONY: env-setup
env-setup:
@echo "Setting up environment..."
mkdir -p logs tmp .cache
test -f .env || cp .env.example .env
test -f .devcontainer/.env || cp .devcontainer/.env.example .devcontainer/.env
@echo ".env files created from templates"
.PHONY: hooks
hooks:
@echo "Setting up git hooks..."
pre-commit install || true
.PHONY: clean
clean:
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete
rm -rf .pytest_cache htmlcov .coverage .mypy_cache
uv pip cache prune
.PHONY: test
test:
uv run pytest tests/ -v --tb=short
.PHONY: check
check:
uv run pytest tests/ -v
uv run ruff check .
uv run mypy src/ --ignore-missing-imports
.PHONY: run
run:
uv run python -m myapp.cli
Complete devcontainer.json Example
{
"name": "Python Development Environment",
"description": "Development container with Python 3.14, uv, and Docker-in-Docker",
"image": "mcr.microsoft.com/devcontainers/python:3.14",
"dockerFile": "../Dockerfile",
"target": "development",
"context": "..",
"runArgs": [
"--env-file", "${localWorkspaceFolder}/.env",
"--env-file", "${localWorkspaceFolder}/.devcontainer/.env"
],
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest",
"moby": true
}
},
"mounts": [
"source=${localWorkspaceFolderBasename}-home,target=/home/vscode,type=volume",
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,readonly"
],
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.black-formatter",
"charliermarsh.ruff",
"ms-azuretools.vscode-docker",
"eamodio.gitlens",
"ms-vscode.makefile-tools"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.formatting.provider": "black",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/bin/zsh",
"args": ["-l"]
}
}
}
}
},
"postCreateCommand": "make initialize",
"postStartCommand": "git config --global --add safe.directory /workspace",
"remoteUser": "vscode",
"remoteEnv": {
"PATH": "/home/vscode/.local/bin:${containerEnv:PATH}"
}
}
Python with UV Patterns
Project Setup with uv
Initialize project with uv and modern Python:
# In container or locally
uv new my-project --python 3.14
cd my-project
uv sync
Dependencies Management
# Add production dependency
uv add requests fastapi
# Add development dependency
uv add --group dev pytest pytest-cov black ruff mypy
# Add optional group
uv add --group notebook jupyter ipykernel
# Sync all dependencies
uv sync --extra dev
# Run with uv
uv run python -m myapp.cli
uv run pytest tests/ -v
pyproject.toml Structure for Development
[project]
name = "my-project"
version = "0.1.0"
description = "Project description"
requires-python = ">=3.14"
dependencies = [
"fastapi>=0.104.0",
"requests>=2.31.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"black>=23.9.0",
"ruff>=0.10.0",
"mypy>=1.5.0",
"pre-commit>=3.3.0",
]
[tool.uv]
dev-dependencies = ["pytest", "black", "ruff", "mypy"]
[tool.black]
line-length = 88
target-version = ["py314"]
[tool.ruff]
line-length = 88
target-version = "py314"
[tool.mypy]
python_version = "3.14"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
Makefile UV Integration
.PHONY: install
install:
uv sync --extra dev
.PHONY: test
test:
uv run pytest tests/ -v --cov=src --cov-report=term-missing
.PHONY: lint
lint:
uv run ruff check src/ tests/
uv run mypy src/
.PHONY: format
format:
uv run black src/ tests/
uv run ruff check --fix src/ tests/
.PHONY: run
run:
uv run python -m myapp.cli
Installing from Dockerfile During Build
COPY --chown=vscode:vscode pyproject.toml uv.lock* ./
RUN if [ -f uv.lock ]; then \
uv sync --no-dev; \
else \
uv sync; \
fi
Testing Support
Unit Testing with pytest
Configure pytest in pyproject.toml:
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "-v --tb=short --strict-markers"
markers = [
"unit: unit tests",
"integration: integration tests",
"slow: slow tests",
]
Integration Testing with Docker Compose
Create docker-compose.test.yml:
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: testpass
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
Test Makefile Targets
.PHONY: test test-unit test-integration test-all
test: test-unit
test-unit:
uv run pytest tests/unit/ -v -m "not slow"
test-integration:
docker compose -f docker-compose.test.yml up -d
uv run pytest tests/integration/ -v || true
docker compose -f docker-compose.test.yml down
test-all:
uv run pytest tests/ -v --cov=src --cov-report=html
test-watch:
uv run pytest-watch tests/unit/
Troubleshooting
Permission Denied Errors
Problem: Files owned by root, cannot write from vscode user.
Solution in Dockerfile:
RUN chown -R vscode:vscode /workspace /home/vscode
Check in container:
whoami
ls -la /workspace
ls -la /home/vscode
Extensions Not Installing
Problem: VS Code extensions fail to install in container.
Solution: Use full extension IDs with version pins:
{
"extensions": [
"ms-python.python@2024.0.0",
"ms-python.vscode-pylance@2024.0.0"
]
}
Or rebuild container:
# In VS Code: Dev Containers: Rebuild Container
Environment Variables Not Loading
Problem: .env files created but variables not available in container.
Solution: Verify files exist and rebuild:
# Check in container
printenv | grep YOUR_VAR
cat /home/vscode/.env
# Rebuild container
Ensure runArgs correctly references env files in devcontainer.json.
Docker Socket Permission Issues
Problem: Cannot access Docker socket from container.
Solution in Dockerfile:
RUN groupadd docker || true && \
usermod -aG docker vscode
In devcontainer.json:
{
"postCreateCommand": "newgrp docker"
}
UV Cache Issues
Problem: Dependency resolution slow or caching issues.
Solution:
# Clear uv cache in container
uv pip cache prune
# In Dockerfile, use specific versions
RUN uv sync --frozen # Use uv.lock if available
Volume Mount Issues on Windows
Problem: Volume mounts not working on Windows with WSL2.
Solution: Ensure Docker Desktop WSL2 integration enabled:
# In devcontainer.json, use Windows paths correctly
"mounts": [
"source=${localEnv:USERPROFILE}\\.ssh,target=/home/vscode/.ssh,type=bind,readonly"
]
Validation Guidance
Pre-Build Validation
Check before building container:
# Validate JSON syntax
python -m json.tool .devcontainer/devcontainer.json
# Check file references
ls -la Dockerfile pyproject.toml .env.example
# Validate Makefile
make --dry-run initialize
Post-Build Validation
After container builds:
# Verify user and permissions
docker exec <container> whoami
docker exec <container> ls -la /workspace
# Test uv installation
docker exec <container> uv --version
# Verify zsh and plugins
docker exec <container> zsh -c "echo $ZSH_VERSION"
# Check VS Code extensions installed
docker exec <container> code --list-extensions 2>/dev/null || echo "VS Code not in container"
Development Environment Check
.PHONY: validate
validate: validate-files validate-container validate-tools
.PHONY: validate-files
validate-files:
@echo "Validating devcontainer configuration..."
@python -m json.tool .devcontainer/devcontainer.json > /dev/null
@test -f Dockerfile && echo "✓ Dockerfile found" || exit 1
@test -f pyproject.toml && echo "✓ pyproject.toml found" || exit 1
@test -f .env.example && echo "✓ .env.example found" || exit 1
.PHONY: validate-container
validate-container:
@echo "Validating container setup..."
@whoami | grep -q vscode && echo "✓ Running as vscode user" || exit 1
@test -d /workspace && echo "✓ Workspace mounted" || exit 1
@test -d /home/vscode && echo "✓ Home directory mounted" || exit 1
.PHONY: validate-tools
validate-tools:
@echo "Validating development tools..."
@uv --version && echo "✓ uv installed" || exit 1
@python --version && echo "✓ Python available" || exit 1
@zsh --version && echo "✓ zsh available" || exit 1
@git --version && echo "✓ git available" || exit 1
.PHONY: check-env
check-env:
@echo "Environment variables:"
@printenv | grep -E '^(PYTHON|DEBUG|LOG_LEVEL|PROJECT_NAME)' || echo "No project vars found"
@test -f .env && echo "✓ .env file loaded" || echo "⚠ .env file missing"
Best Practices Summary
- Always use non-root user (
vscodeby default) in containers - Use dynamic volume naming with
${localWorkspaceFolderBasename}-homefor persistence - Prefer uv for Python dependency management in devcontainers
- Multi-stage builds separate development and production concerns
- Keep Dockerfile minimal - offload setup to Makefile targets
- Environment as configuration - use
.envfiles for settings - Document all tools - list in VS Code extensions and Dockerfile
- Test container builds - validate before committing devcontainer configs
- SSH access when needed - mount
.sshas readonly for git operations - DinD with caution - only enable Docker socket when truly needed