Containerization and Deployment
Moving async applications to production involves more than just “docker build && docker run”. Async apps have specific needs around resource limits, signal handling, and graceful shutdowns that can make or break your deployment.
Docker Configuration
Build containers that work well with async applications:
# Multi-stage Dockerfile for async Python application
FROM python:3.11-slim as builder
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1
The builder stage handles dependency installation. We use environment variables to optimize Python for containers - no buffering, no .pyc files, and no pip cache.
Install system dependencies and create a virtual environment:
# Install dependencies
RUN apt-get update && apt-get install -y build-essential \
&& rm -rf /var/lib/apt/lists/*
# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
Using a virtual environment in containers might seem redundant, but it provides better isolation and makes the final image cleaner.
The production stage creates a minimal runtime environment:
# Production stage
FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH"
# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
Multi-stage builds keep the final image small by excluding build tools and intermediate files.
Security and application setup:
# Create non-root user
RUN useradd --create-home --shell /bin/bash app
USER app
WORKDIR /home/app
# Copy application code
COPY --chown=app:app . .
# Expose port
EXPOSE 8000
Running as a non-root user is crucial for security. The --chown
flag ensures the app user owns the files.
Health checks and startup:
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Start application
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Application Configuration
Configure your async application for containerized environments:
import os
import asyncio
import signal
from contextlib import asynccontextmanager
from fastapi import FastAPI
class AppConfig:
def __init__(self):
self.host = os.getenv("HOST", "0.0.0.0")
self.port = int(os.getenv("PORT", 8000))
self.workers = int(os.getenv("WORKERS", 1))
self.max_connections = int(os.getenv("MAX_CONNECTIONS", 1000))
self.keepalive_timeout = int(os.getenv("KEEPALIVE_TIMEOUT", 5))
config = AppConfig()
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
print("Starting async application...")
# Initialize resources
await initialize_database_pool()
await initialize_redis_connection()
yield
# Shutdown
print("Shutting down async application...")
await cleanup_database_pool()
await cleanup_redis_connection()
app = FastAPI(lifespan=lifespan)
@app.get("/health")
async def health_check():
return {"status": "healthy", "timestamp": asyncio.get_event_loop().time()}
async def initialize_database_pool():
# Initialize database connection pool
pass
async def cleanup_database_pool():
# Clean up database connections
pass
async def initialize_redis_connection():
# Initialize Redis connection
pass
async def cleanup_redis_connection():
# Clean up Redis connection
pass
Kubernetes Deployment
Deploy with Kubernetes for scalability:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: async-app
labels:
app: async-app
spec:
replicas: 3
selector:
matchLabels:
app: async-app
template:
metadata:
labels:
app: async-app
spec:
containers:
- name: async-app
image: async-app:latest
ports:
- containerPort: 8000
env:
- name: MAX_CONNECTIONS
value: "1000"
- name: WORKERS
value: "1"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: async-app-service
spec:
selector:
app: async-app
ports:
- protocol: TCP
port: 80
targetPort: 8000
type: LoadBalancer
Environment Variables
Configure your application through environment variables:
import os
class Settings:
# Database
DATABASE_URL: str = os.getenv("DATABASE_URL", "postgresql://localhost/mydb")
DATABASE_POOL_SIZE: int = int(os.getenv("DATABASE_POOL_SIZE", "10"))
# Redis
REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379")
REDIS_MAX_CONNECTIONS: int = int(os.getenv("REDIS_MAX_CONNECTIONS", "10"))
# Application
DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
# Performance
MAX_WORKERS: int = int(os.getenv("MAX_WORKERS", "4"))
REQUEST_TIMEOUT: int = int(os.getenv("REQUEST_TIMEOUT", "30"))
settings = Settings()
Production Checklist
Before deploying to production:
Security:
- Run as non-root user
- Use secrets management for sensitive data
- Enable HTTPS/TLS
- Implement rate limiting
Performance:
- Set appropriate resource limits
- Configure connection pooling
- Enable compression
Monitoring:
- Health check endpoints
- Structured logging
- Metrics collection
- Error tracking
Reliability:
- Graceful shutdown handling
- Circuit breakers for external services
- Retry mechanisms with backoff
Summary
Key deployment considerations for async applications:
- Use multi-stage Docker builds for smaller images
- Configure proper resource limits and health checks
- Implement graceful shutdown handling
- Use structured logging for better observability
- Set up monitoring and alerting
- Follow security best practices
Proper containerization and deployment ensure your async applications run reliably in production environments.
In Part 22, we’ll explore security best practices for async applications.