Deployment & DevOps for APIs
Ship your APIs to production with confidence using Docker, CI/CD, blue-green deployments, and modern deployment strategies
From Code to Production
Building an API is only half the story. Deploying it reliably, maintaining it at scale, and releasing updates without downtime requires DevOps practices. In this lesson, you'll learn to containerize your API with Docker, automate deployments with CI/CD pipelines, implement blue-green and canary deployment strategies, manage configuration across environments, and deploy serverless APIs using AWS Lambda. These are the skills that separate hobby projects from production systems serving millions of users.
In This Lesson
Dockerizing APIs
Docker containers package your API with all its dependencies, ensuring it runs identically everywhere, from your laptop to production servers. This eliminates "works on my machine" problems and makes deployments predictable and repeatable.
Why Dockerize Your API?
Same environment in development, staging, and production. No dependency conflicts or version mismatches.
Each API runs in its own container with isolated dependencies. Multiple Python versions on the same host.
Deploy anywhere, AWS, GCP, Azure, or your own servers. Switch cloud providers without code changes.
Spin up multiple containers instantly. Horizontal scaling becomes trivial with orchestration tools.
Creating a Production-Ready Dockerfile
A well-crafted Dockerfile uses multi-stage builds to minimize image size, leverages caching for faster builds, and follows security best practices.
# Dockerfile for FastAPI application
# Stage 1: Build stage
FROM python:3.11-slim as builder
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first (leverage Docker layer caching)
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir --user -r requirements.txt
# Stage 2: Runtime stage
FROM python:3.11-slim
WORKDIR /app
# Create non-root user for security
RUN useradd -m -u 1000 apiuser && chown -R apiuser:apiuser /app
# Copy Python dependencies from builder
COPY --from=builder /root/.local /home/apiuser/.local
# Copy application code
COPY --chown=apiuser:apiuser ./app ./app
# Switch to non-root user
USER apiuser
# Add local bin to PATH
ENV PATH=/home/apiuser/.local/bin:$PATH
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
# Run with uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]Docker Compose for Local Development
Docker Compose orchestrates multiple containers (API + database + Redis) for local development that mirrors production architecture.
# docker-compose.yml
services:
api:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/apidb
- REDIS_URL=redis://redis:6379
- ENV=development
volumes:
- ./app:/app/app # Hot reload in development
depends_on:
- db
- redis
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
db:
image: postgres:15-alpine
environment:
- POSTGRES_PASSWORD=password
- POSTGRES_DB=apidb
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:Usage: docker-compose up starts all services.docker-compose down stops them. Code changes reload automatically.
Optimizing Build Context with .dockerignore
# .dockerignore - exclude unnecessary files from Docker build __pycache__ *.pyc *.pyo *.pyd .Python *.so *.egg *.egg-info/ dist/ build/ # Virtual environments venv/ env/ .venv/ # IDE .vscode/ .idea/ *.swp # Git .git/ .gitignore # Testing .pytest_cache/ .coverage htmlcov/ # Documentation docs/ *.md # Environment files (security!) .env .env.local *.pem *.key
Excluding these files reduces build context size from ~500MB to ~10MB, speeding up builds and preventing secrets from leaking into images.
CI/CD Pipelines
Continuous Integration and Continuous Deployment (CI/CD) automate testing, building, and deploying your API. Every push to main triggers tests, builds a Docker image, and deploys to production, no manual steps, no human error.
GitHub Actions Pipeline
GitHub Actions runs workflows triggered by git events (push, pull request, release). This pipeline tests code, builds a Docker image, pushes to a registry, and deploys.
# .github/workflows/deploy.yml
name: Deploy API
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
DOCKER_IMAGE: ghcr.io/your-org/api
AWS_REGION: us-east-1
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run tests
run: pytest --cov=app --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v5
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.DOCKER_IMAGE }}:latest
${{ env.DOCKER_IMAGE }}:${{ github.sha }}
cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:latest
cache-to: type=inline
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy to ECS
run: |
aws ecs update-service \
--cluster api-cluster \
--service api-service \
--force-new-deployment \
--desired-count 2
- name: Wait for deployment
run: |
aws ecs wait services-stable \
--cluster api-cluster \
--services api-service
- name: Notify deployment
if: success()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{
"text": "API deployed successfully! 🚀\nCommit: ${{ github.sha }}"
}GitLab CI/CD Pipeline
GitLab CI offers similar capabilities with a different YAML syntax. This pipeline includes security scanning and deployment to Kubernetes.
# .gitlab-ci.yml
stages:
- test
- build
- security
- deploy
variables:
DOCKER_IMAGE: registry.gitlab.com/your-org/api
test:
stage: test
image: python:3.11-slim
cache:
paths:
- .pip-cache/
before_script:
- pip install --cache-dir=.pip-cache -r requirements.txt pytest pytest-cov
script:
- pytest --cov=app --cov-report=term --cov-report=html
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
coverage: '/TOTAL.*\s+(\d+%)$/'
build:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA -t $DOCKER_IMAGE:latest .
- docker push $DOCKER_IMAGE:$CI_COMMIT_SHA
- docker push $DOCKER_IMAGE:latest
only:
- main
security_scan:
stage: security
image: aquasec/trivy:latest
script:
- trivy image --severity HIGH,CRITICAL $DOCKER_IMAGE:$CI_COMMIT_SHA
only:
- main
deploy_production:
stage: deploy
image: bitnami/kubectl:latest
before_script:
- kubectl config set-cluster k8s --server="$KUBE_URL" --insecure-skip-tls-verify=true
- kubectl config set-credentials admin --token="$KUBE_TOKEN"
- kubectl config set-context default --cluster=k8s --user=admin
- kubectl config use-context default
script:
- kubectl set image deployment/api-deployment api=$DOCKER_IMAGE:$CI_COMMIT_SHA -n production
- kubectl rollout status deployment/api-deployment -n production
environment:
name: production
url: https://api.example.com
only:
- main
when: manual # Require manual approval for productionThis pipeline adds security scanning with Trivy and requires manual approval before production deployment, a good practice for critical systems.
Blue-Green Deployments
Blue-green deployment eliminates downtime by running two identical production environments. Deploy the new version to the "green" environment, test it, then switch traffic instantly. If issues arise, rollback is instant, just switch back to "blue".
How Blue-Green Deployment Works
All traffic routes to the blue environment running v1.0
Deploy v1.1 to green environment. Blue still serves traffic.
Run smoke tests against green. Verify health checks pass.
Update load balancer to route all traffic to green. Instant cutover.
Blue-Green with AWS ECS
# appspec.yml for AWS CodeDeploy Blue-Green
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "arn:aws:ecs:region:account:task-definition/api:5"
LoadBalancerInfo:
ContainerName: "api"
ContainerPort: 8000
PlatformVersion: "LATEST"
NetworkConfiguration:
AwsvpcConfiguration:
Subnets:
- "subnet-12345"
- "subnet-67890"
SecurityGroups:
- "sg-abc123"
AssignPublicIp: "ENABLED"
Hooks:
- BeforeInstall: "BeforeInstallHookFunction"
- AfterInstall: "AfterInstallHookFunction"
- AfterAllowTestTraffic: "AfterAllowTestTrafficHookFunction"
- BeforeAllowTraffic: "BeforeAllowTrafficHookFunction"
- AfterAllowTraffic: "AfterAllowTrafficHookFunction"
# Deployment configuration
DeploymentConfiguration:
Type: "LINEAR"
LinearPercentage: 100 # All at once
Interval: 0 # Immediate switchCodeDeploy automatically creates a green task set, runs health checks, switches the target group, and keeps blue running for rollback. Rollback is one command.
Blue-Green with Kubernetes
# kubernetes/blue-green-deployment.sh
#!/bin/bash
# Deploy green version
kubectl apply -f deployment-green.yaml
# Wait for green to be ready
kubectl wait --for=condition=available --timeout=300s deployment/api-green
# Run smoke tests against green
kubectl run smoke-test --image=curlimages/curl --rm -it --restart=Never \
-- curl -f http://api-green-service/health || exit 1
# Switch service to green
kubectl patch service api-service -p '{"spec":{"selector":{"version":"green"}}}'
echo "Traffic switched to green. Blue deployment still running for rollback."
echo "To rollback: kubectl patch service api-service -p '{"spec":{"selector":{"version":"blue"}}}'"
# Optional: After confidence window, delete blue
# kubectl delete deployment api-blueCanary Releases & Feature Flags
Canary releases gradually roll out new versions to a small percentage of users before full deployment. Feature flags allow toggling features on/off without code changes. Together, they enable safe, incremental releases.
Canary Deployment Strategy
Instead of switching all traffic at once, route a small percentage (5-10%) to the new version. Monitor error rates, latency, and business metrics. Gradually increase the percentage if metrics look good.
- 5% traffic → monitor for 10 minutes
- 25% traffic → monitor for 30 minutes
- 50% traffic → monitor for 1 hour
- 100% traffic → new version fully deployed
Canary with Kubernetes & Istio
# istio-canary-virtualservice.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: api-canary
spec:
hosts:
- api.example.com
http:
- match:
- headers:
cookie:
regex: "^(.*?;)?(canary=true)(;.*)?$"
route:
- destination:
host: api-service
subset: v2
- route:
- destination:
host: api-service
subset: v1
weight: 90
- destination:
host: api-service
subset: v2
weight: 10
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: api-destination
spec:
host: api-service
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2This routes 10% of traffic to v2, while users with a canary=true cookie always get v2 (useful for internal testing).
Feature Flags for Gradual Rollouts
Feature flags decouple deployment from release. Deploy code with new features disabled, then enable them for specific users or percentages via configuration.
# app/feature_flags.py
import hashlib
from functools import lru_cache
import os
from typing import Optional
class FeatureFlags:
"""Manage feature flags with percentage rollouts"""
def __init__(self):
self.flags = {
"new_recommendation_engine": {
"enabled": os.getenv("FF_NEW_RECS", "false").lower() == "true",
"rollout_percentage": int(os.getenv("FF_NEW_RECS_PCT", "0")),
},
"v2_api_endpoints": {
"enabled": os.getenv("FF_V2_API", "false").lower() == "true",
"rollout_percentage": int(os.getenv("FF_V2_API_PCT", "0")),
},
}
def is_enabled(self, flag_name: str, user_id: Optional[str] = None) -> bool:
"""Check if feature is enabled for user"""
if flag_name not in self.flags:
return False
flag = self.flags[flag_name]
# Feature globally disabled
if not flag["enabled"]:
return False
# Full rollout
if flag["rollout_percentage"] >= 100:
return True
# No rollout
if flag["rollout_percentage"] == 0:
return False
# Percentage-based rollout (consistent per user)
if user_id:
user_hash = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
return (user_hash % 100) < flag["rollout_percentage"]
return False
@lru_cache()
def get_feature_flags():
return FeatureFlags()
# Usage in API endpoint
from app.feature_flags import get_feature_flags
@app.get("/recommendations/{user_id}")
async def get_recommendations(user_id: str):
flags = get_feature_flags()
if flags.is_enabled("new_recommendation_engine", user_id):
# New ML-based recommendations
return new_recommendation_engine(user_id)
else:
# Old rule-based recommendations
return legacy_recommendations(user_id)Using LaunchDarkly for Feature Flags
# app/feature_flags_launchdarkly.py
import os
import ldclient
from ldclient.config import Config
# Initialize LaunchDarkly client
ldclient.set_config(Config(os.getenv("LAUNCHDARKLY_SDK_KEY")))
ld_client = ldclient.get()
@app.get("/recommendations/{user_id}")
async def get_recommendations(user_id: str):
user = {
"key": user_id,
"email": f"{user_id}@example.com",
"custom": {
"plan": "premium", # Use for targeting
}
}
# Check feature flag with LaunchDarkly
use_new_engine = ld_client.variation(
"new-recommendation-engine",
user,
default=False
)
if use_new_engine:
return new_recommendation_engine(user_id)
else:
return legacy_recommendations(user_id)
# Close client on shutdown
@app.on_event("shutdown")
async def shutdown():
ld_client.close()LaunchDarkly provides a UI to control flags, target specific users, run A/B tests, and track metrics, no code changes required.
Environment Configuration Management
Managing configuration across environments (dev, staging, production) without hardcoding secrets is critical. Use environment variables for configuration and secret managers for sensitive data.
12-Factor App: Config in Environment
Store configuration in environment variables, not code. Separate config from code means the same Docker image can run in any environment.
DB_URL = "postgres://..."DB_URL = os.getenv("DATABASE_URL")settings.database_urlType-Safe Configuration with Pydantic
# app/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from functools import lru_cache
class Settings(BaseSettings):
"""Application configuration loaded from environment"""
# Application
app_name: str = "My API"
debug: bool = False
environment: str = "production"
# Database
database_url: str
db_pool_size: int = 10
db_max_overflow: int = 20
# Redis
redis_url: str = "redis://localhost:6379"
# Security
secret_key: str
jwt_algorithm: str = "HS256"
access_token_expire_minutes: int = 30
# AWS
aws_region: str = "us-east-1"
aws_access_key_id: str | None = None
aws_secret_access_key: str | None = None
# External APIs
stripe_api_key: str | None = None
sendgrid_api_key: str | None = None
# Observability
sentry_dsn: str | None = None
log_level: str = "INFO"
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)
@lru_cache()
def get_settings() -> Settings:
return Settings()
# Usage
from app.config import get_settings
settings = get_settings()
print(settings.database_url) # Type-safe, validatedAWS Secrets Manager Integration
Store sensitive secrets (API keys, database passwords) in AWS Secrets Manager instead of environment variables. Rotate them automatically.
# app/secrets.py
import boto3
import json
from functools import lru_cache
@lru_cache()
def get_secret(secret_name: str, region: str = "us-east-1") -> dict:
"""Retrieve secret from AWS Secrets Manager"""
client = boto3.client("secretsmanager", region_name=region)
try:
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response["SecretString"])
except Exception as e:
raise RuntimeError(f"Failed to retrieve secret {secret_name}: {e}")
# Usage in config
from pydantic import model_validator
class Settings(BaseSettings):
database_url: str | None = None
@model_validator(mode="after")
def load_production_secrets(self) -> "Settings":
"""Load secrets from AWS Secrets Manager in production"""
if self.environment == "production":
db_secrets = get_secret("production/database")
self.database_url = db_secrets["url"]
api_secrets = get_secret("production/api-keys")
self.stripe_api_key = api_secrets["stripe"]
self.sendgrid_api_key = api_secrets["sendgrid"]
return selfEnvironment-Specific .env Files
DEBUG=true ENVIRONMENT=development DATABASE_URL=sqlite:///dev.db REDIS_URL=redis://localhost:6379 SECRET_KEY=dev-secret-key LOG_LEVEL=DEBUG
DEBUG=false ENVIRONMENT=production DATABASE_URL=<from Secrets Manager> REDIS_URL=<from Secrets Manager> SECRET_KEY=<from Secrets Manager> LOG_LEVEL=INFO SENTRY_DSN=https://...
Key Takeaways
- Docker containers ensure consistency across development, staging, and production. Multi-stage builds reduce image size by 80%+.
- CI/CD pipelines automate testing and deployment, eliminating manual errors. GitHub Actions and GitLab CI integrate seamlessly with cloud providers.
- Blue-green deployments enable zero-downtime releases. Deploy to green, test, switch traffic instantly. Rollback is one command.
- Canary releases reduce risk by gradually routing traffic (5% → 25% → 100%) while monitoring metrics. Automatic rollback protects users.
- Feature flags decouple deployment from release. Ship code with features disabled, enable for specific users or percentages via configuration.
- Environment configuration belongs in environment variables, not code. Pydantic Settings provides type-safe config. AWS Secrets Manager stores sensitive data.
- Modern deployment strategies enable continuous delivery with confidence. Blue-green for instant rollbacks, canary for gradual rollouts, feature flags for controlled releases.
Ready to Deploy?
You now have the knowledge to ship your API to production with confidence. Start with Docker and a simple CI/CD pipeline. Add blue-green deployments as your user base grows. Experiment with feature flags to control releases. The best deployment strategy is the one you can maintain and iterate on. Ship often, monitor everything, and improve continuously. Next, explore serverless deployment options for event-driven workloads.