REST API Development

API Deployment & DevOps

18 min Lesson 31 of 35

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
API Deployment Philosophy: Treat your API as a product. Every deployment should be reversible, observable, and documented. Always have a rollback plan.

Containerization with Docker

Docker enables consistent deployments across all environments. Here's a production-ready Dockerfile for a Laravel API:

Dockerfile:
<!-- 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"]
docker-compose.yml for Development:
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:

.env.example Template:
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=
Security Warning: Never use default or example credentials in production. Generate strong, unique secrets for every environment. Use secrets management tools like AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault for sensitive data.

CI/CD Pipeline with GitHub Actions

Automate testing, building, and deployment with CI/CD pipelines:

.github/workflows/deploy.yml:
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

Blue-Green Deployment Script:
#!/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

Kubernetes Rolling Update:
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:

Migration Best Practices:
  • 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
Safe Migration Pattern:
<?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:

routes/api.php:
<?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

Pre-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
Post-Deployment Checklist:
  • ✓ 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

Quick Rollback Script:
#!/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."
Rollback Warning: Database rollbacks can be destructive. If your migration deleted data, rolling back won't restore it. Always backup your database before deployments and test rollback procedures in staging.

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.