API Deployment & DevOps
API Deployment & DevOps
Deploying APIs to production requires careful planning, automation, and monitoring. In this lesson, we'll explore modern DevOps practices for API deployment, including containerization, CI/CD pipelines, environment configuration, and zero-downtime deployments.
Understanding API Deployment Challenges
API deployments face unique challenges compared to traditional web applications:
- Zero Downtime: APIs must remain available during deployments as clients depend on them 24/7
- Backward Compatibility: Breaking changes can disrupt existing integrations
- Environment Parity: Development, staging, and production must be consistent
- Database Migrations: Schema changes must be coordinated with code deployments
- Configuration Management: Secrets and environment-specific settings must be secure
Containerization with Docker
Docker enables consistent deployments across all environments. Here's a production-ready Dockerfile for a Laravel API:
<!-- Multi-stage build for optimized images -->
FROM php:8.2-fpm-alpine AS base
# Install system dependencies
RUN apk add --no-cache \
nginx \
postgresql-dev \
redis \
git \
zip \
unzip \
curl
# Install PHP extensions
RUN docker-php-ext-install pdo pdo_pgsql opcache
# Install Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
# Copy composer files
COPY composer.json composer.lock ./
# Install dependencies (skip dev in production)
RUN composer install --no-dev --optimize-autoloader --no-scripts
# Copy application code
COPY . .
# Set permissions
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html/storage
# Generate optimized autoloader
RUN composer dump-autoload --optimize
# Cache configuration
RUN php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
EXPOSE 9000
CMD ["php-fpm"]
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/var/www/html
environment:
- APP_ENV=local
- APP_DEBUG=true
depends_on:
- database
- redis
database:
image: postgres:15-alpine
environment:
POSTGRES_DB: api_db
POSTGRES_USER: api_user
POSTGRES_PASSWORD: secret
volumes:
- db_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- .:/var/www/html
ports:
- "8000:80"
depends_on:
- app
volumes:
db_data:
Environment Configuration Management
Never commit secrets to version control. Use environment variables and secret management systems:
APP_NAME="My API"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=https://api.example.com
LOG_CHANNEL=stack
LOG_LEVEL=error
DB_CONNECTION=pgsql
DB_HOST=database.example.com
DB_PORT=5432
DB_DATABASE=api_production
DB_USERNAME=
DB_PASSWORD=
REDIS_HOST=redis.example.com
REDIS_PASSWORD=
REDIS_PORT=6379
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
JWT_SECRET=
JWT_TTL=60
MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
SENTRY_LARAVEL_DSN=
CI/CD Pipeline with GitHub Actions
Automate testing, building, and deployment with CI/CD pipelines:
name: Deploy API
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
extensions: pdo, pdo_pgsql, redis
coverage: xdebug
- name: Install Dependencies
run: composer install --prefer-dist --no-progress
- name: Copy .env
run: cp .env.ci .env
- name: Generate Application Key
run: php artisan key:generate
- name: Run Migrations
run: php artisan migrate --force
- name: Run Tests
run: php artisan test --coverage --min=80
- name: Run Static Analysis
run: ./vendor/bin/phpstan analyse
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to Production
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
cd /var/www/api
docker-compose pull
docker-compose up -d --no-deps --build app
docker-compose exec -T app php artisan migrate --force
docker-compose exec -T app php artisan config:cache
docker-compose exec -T app php artisan route:cache
docker-compose exec -T app php artisan queue:restart
Zero-Downtime Deployment Strategies
Implement deployment strategies that keep your API available during updates:
1. Blue-Green Deployment
#!/bin/bash
# Blue-Green deployment for API
CURRENT=$(docker ps --filter "name=api-blue" -q)
if [ -z "$CURRENT" ]; then
NEW_COLOR="blue"
OLD_COLOR="green"
else
NEW_COLOR="green"
OLD_COLOR="blue"
fi
echo "Deploying to $NEW_COLOR environment..."
# Start new environment
docker-compose -f docker-compose.$NEW_COLOR.yml up -d
# Wait for health check
echo "Waiting for health check..."
for i in {1..30}; do
if curl -f http://localhost:8001/health > /dev/null 2>&1; then
echo "Health check passed!"
break
fi
sleep 2
done
# Switch traffic
echo "Switching traffic to $NEW_COLOR..."
# Update load balancer or reverse proxy configuration
nginx -s reload
# Stop old environment
echo "Stopping $OLD_COLOR environment..."
docker-compose -f docker-compose.$OLD_COLOR.yml down
echo "Deployment complete!"
2. Rolling Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-deployment
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Add 1 extra pod during update
maxUnavailable: 1 # Allow 1 pod to be unavailable
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: ghcr.io/myorg/api:latest
ports:
- containerPort: 9000
livenessProbe:
httpGet:
path: /health
port: 9000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 9000
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Database Migration Strategies
Handle database changes safely during deployments:
- Always make migrations backward-compatible
- Use multi-phase migrations for breaking changes
- Test migrations on production-like data
- Keep migrations fast (use background jobs for large data changes)
- Always have a rollback plan
<?php
// Phase 1: Add new column (nullable, backward-compatible)
Schema::table('users', function (Blueprint $table) {
$table->string('new_email')->nullable();
});
// Deploy code that writes to both old and new columns
// Phase 2: Backfill data (run as background job)
User::chunk(1000, function ($users) {
foreach ($users as $user) {
$user->new_email = $user->email;
$user->save();
}
});
// Phase 3: Make new column non-nullable
Schema::table('users', function (Blueprint $table) {
$table->string('new_email')->nullable(false)->change();
});
// Deploy code that only uses new column
// Phase 4: Drop old column
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('email');
$table->renameColumn('new_email', 'email');
});
Monitoring and Health Checks
Implement comprehensive health checks for automated monitoring:
<?php
// Liveness probe - is the app running?
Route::get('/health', function () {
return response()->json([
'status' => 'healthy',
'timestamp' => now()->toIso8601String(),
]);
});
// Readiness probe - can the app serve traffic?
Route::get('/ready', function () {
$checks = [
'database' => false,
'redis' => false,
'storage' => false,
];
try {
DB::connection()->getPdo();
$checks['database'] = true;
} catch (\Exception $e) {
Log::error('Database check failed: ' . $e->getMessage());
}
try {
Redis::ping();
$checks['redis'] = true;
} catch (\Exception $e) {
Log::error('Redis check failed: ' . $e->getMessage());
}
try {
Storage::disk('local')->exists('test');
$checks['storage'] = true;
} catch (\Exception $e) {
Log::error('Storage check failed: ' . $e->getMessage());
}
$allHealthy = array_reduce($checks, fn($carry, $check) => $carry && $check, true);
return response()->json([
'status' => $allHealthy ? 'ready' : 'not_ready',
'checks' => $checks,
'timestamp' => now()->toIso8601String(),
], $allHealthy ? 200 : 503);
});
// Detailed status endpoint (authenticated)
Route::middleware(['auth:api', 'admin'])->get('/status', function () {
return response()->json([
'app' => [
'name' => config('app.name'),
'env' => config('app.env'),
'version' => config('app.version'),
],
'system' => [
'php_version' => PHP_VERSION,
'laravel_version' => app()->version(),
'memory_usage' => memory_get_usage(true) / 1024 / 1024 . ' MB',
'peak_memory' => memory_get_peak_usage(true) / 1024 / 1024 . ' MB',
],
'cache' => [
'driver' => config('cache.default'),
'redis_connection' => Redis::connection()->ping(),
],
'database' => [
'connection' => DB::connection()->getName(),
'version' => DB::select('SELECT version()')[0]->version,
],
'queue' => [
'connection' => config('queue.default'),
'failed_jobs' => DB::table('failed_jobs')->count(),
],
]);
});
Deployment Checklist
- ✓ All tests passing (unit, integration, E2E)
- ✓ Code review approved
- ✓ Security scan completed (no critical vulnerabilities)
- ✓ Database migrations tested on staging
- ✓ Backward compatibility verified
- ✓ Documentation updated
- ✓ Monitoring alerts configured
- ✓ Rollback plan documented
- ✓ Stakeholders notified
- ✓ Health checks passing
- ✓ Smoke tests executed
- ✓ Error rates within normal range
- ✓ Response times acceptable
- ✓ Database queries optimized
- ✓ Cache warmed up
- ✓ Background jobs running
- ✓ Logs monitored for errors
- ✓ Client integrations verified
- ✓ Deployment documented
Rollback Procedures
#!/bin/bash
echo "Starting rollback..."
# Get previous version
PREVIOUS_VERSION=$(git describe --abbrev=0 --tags $(git rev-list --tags --skip=1 --max-count=1))
echo "Rolling back to version: $PREVIOUS_VERSION"
# Checkout previous version
git checkout $PREVIOUS_VERSION
# Rebuild and deploy
docker-compose build
docker-compose up -d
# Rollback migrations (if needed)
# php artisan migrate:rollback --step=1
# Clear caches
docker-compose exec app php artisan cache:clear
docker-compose exec app php artisan config:cache
docker-compose exec app php artisan route:cache
echo "Rollback complete!"
echo "Please verify the application is working correctly."
Summary
API deployment and DevOps require:
- Containerization for consistency across environments
- Automated CI/CD pipelines for testing and deployment
- Secure configuration management with no hardcoded secrets
- Zero-downtime deployment strategies (blue-green, rolling updates)
- Safe database migration patterns with rollback plans
- Comprehensive health checks and monitoring
- Documented deployment and rollback procedures
In the next lesson, we'll explore API design patterns including repository pattern, DTOs, and action classes for cleaner, more maintainable API code.