Zero-Downtime Blue-Green Deployment Pipelines for Laravel Applications on Linode
Establishing the Foundation: Infrastructure and Initial Setup
This guide details a zero-downtime blue-green deployment strategy for Laravel applications hosted on Linode. We’ll leverage a load balancer, two distinct application environments (Blue and Green), and a robust CI/CD pipeline. The core principle is to maintain two identical production environments, with only one actively serving live traffic at any given time. New deployments are pushed to the inactive environment, thoroughly tested, and then traffic is switched over. This minimizes risk and eliminates user-facing downtime.
Our infrastructure will consist of:
- A Linode Load Balancer (e.g., Linode NodeBalancers).
- Two separate Linode Compute Instances for the “Blue” environment.
- Two separate Linode Compute Instances for the “Green” environment.
- A shared database instance (e.g., Linode Managed Databases for PostgreSQL or MySQL).
- A Git repository (e.g., GitHub, GitLab, Bitbucket) for version control and CI/CD triggers.
- A CI/CD tool (e.g., GitHub Actions, GitLab CI, Jenkins).
For this setup, we’ll assume a basic Laravel application is already deployed and accessible. The database will be external to the application servers to ensure data consistency across environments. Each application environment (Blue and Green) will have its own set of web servers and potentially application servers (if using something like Octane). The load balancer will direct traffic to either the Blue or Green environment.
Configuring the Load Balancer and Environments
The Linode NodeBalancer is crucial for directing traffic. We’ll configure it to point to the web servers of our active environment. Initially, the Blue environment will be active.
NodeBalancer Configuration:
Create a NodeBalancer in your Linode Cloud Manager. Configure the following:
- Frontend Protocol: HTTP/HTTPS (depending on your SSL setup).
- Frontend Port: 80/443.
- Backend Protocol: HTTP.
- Backend Port: 80 (or your application’s port).
- Nodes: Add the IP addresses of the web servers for your *active* environment (initially, the Blue environment’s web servers).
- Health Checks: Configure a simple HTTP health check to a specific endpoint (e.g., `/healthz`) that returns a 200 OK. This endpoint should be lightweight and not hit the database.
Environment Setup (Example: Blue Environment):
On each of the two “Blue” Linode Compute Instances, we’ll set up Nginx and PHP-FPM. The application code will be deployed to a specific directory, e.g., /var/www/myapp/current.
Nginx Configuration (/etc/nginx/sites-available/myapp):
server {
listen 80;
server_name your-domain.com;
root /var/www/myapp/current/public;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust PHP version as needed
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
# Health check endpoint
location /healthz {
access_log off;
return 200 'OK';
add_header Content-Type text/plain;
}
}
PHP-FPM Configuration: Ensure PHP-FPM is running and configured correctly. The socket path in the Nginx config must match your PHP-FPM setup.
The “Green” environment will be an exact replica of the “Blue” environment, but with its own set of Linode Compute Instances and its application code deployed to a separate directory (e.g., /var/www/myapp/green). The Nginx configuration will be similar, pointing to the `green` directory.
Crucially, both environments will connect to the *same* shared database instance. This is vital for data consistency during the switchover.
Automating Deployments with CI/CD
We’ll use GitHub Actions as our CI/CD orchestrator. The workflow will trigger on pushes to the `main` branch. The goal is to deploy to the *inactive* environment.
GitHub Actions Workflow (.github/workflows/deploy.yml):
name: Deploy Laravel App
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1' # Match your application's PHP version
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Copy files to Green environment
uses: appleboy/scp-action@master
with:
host: ${{ secrets.GREEN_SERVER_IP }}
username: ${{ secrets.LINODE_SSH_USER }}
key: ${{ secrets.LINODE_SSH_KEY }}
port: 22
source: "./*"
target: "/var/www/myapp/green" # Deploy to the Green environment's directory
- name: Run migrations and cache on Green environment
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.GREEN_SERVER_IP }}
username: ${{ secrets.LINODE_SSH_USER }}
key: ${{ secrets.LINODE_SSH_KEY }}
port: 22
script: |
cd /var/www/myapp/green
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
chown -R www-data:www-data . # Ensure correct permissions
chmod -R 755 storage bootstrap/cache
- name: Trigger health check on Green environment
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.GREEN_SERVER_IP }}
username: ${{ secrets.LINODE_SSH_USER }}
key: ${{ secrets.LINODE_SSH_KEY }}
port: 22
script: |
curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.GREEN_SERVER_IP }}/healthz || exit 1 # Exit if health check fails
- name: Update Load Balancer to Green
# This step requires Linode API access.
# You'll need to create a Linode API token and store it as a secret.
# The script below is a conceptual example and needs to be adapted.
run: |
# Example using curl to interact with Linode API (requires jq for parsing)
# Install jq on the runner: sudo apt-get update && sudo apt-get install -y jq
LINODE_API_TOKEN="${{ secrets.LINODE_API_TOKEN }}"
NODEBALANCER_ID="YOUR_NODEBALANCER_ID" # Replace with your NodeBalancer ID
GREEN_SERVER_IP="${{ secrets.GREEN_SERVER_IP }}"
# Get current NodeBalancer configuration
CURRENT_CONFIG=$(curl -s -X GET \
-H "Authorization: Bearer $LINODE_API_TOKEN" \
-H "Content-Type: application/json" \
"https://api.linode.com/v4/nodebalancers/$NODEBALANCER_ID/configs")
# Find the config ID for the frontend port (e.g., 80)
CONFIG_ID=$(echo "$CURRENT_CONFIG" | jq -r '.data[] | select(.port == 80) | .id')
# Get current nodes for that config
CURRENT_NODES=$(echo "$CURRENT_CONFIG" | jq -r --argjson config_id "$CONFIG_ID" '.data[] | select(.id == $config_id) | .nodes')
# Check if Green server IP is already in the active config
IS_GREEN_ACTIVE=$(echo "$CURRENT_NODES" | jq -r --arg ip "$GREEN_SERVER_IP" '.[] | select(.address == $ip) | .address')
if [ -z "$IS_GREEN_ACTIVE" ]; then
echo "Green server is not active. Proceeding with switchover."
# Add Green server to the config (assuming it's not already there)
# This is a simplified example. In a real scenario, you'd likely want to
# remove the old Blue servers and add the new Green servers.
# For a true blue-green, you'd have two sets of nodes in your config,
# and you'd enable/disable them.
# A more robust approach involves managing node groups.
# For simplicity here, we'll assume we're adding the green server.
# A better approach is to have both blue and green servers in the config
# and enable/disable them.
# Let's assume a simpler scenario: we're switching from Blue to Green.
# We need to know the IP of the Blue server to remove it.
# This requires more sophisticated state management or querying the LB.
# A more practical approach for Linode NodeBalancers:
# 1. Have both Blue and Green servers configured as nodes.
# 2. Use a "label" or "group" to identify active/inactive.
# 3. Update the NodeBalancer config to enable Green nodes and disable Blue nodes.
# Example of updating nodes (conceptual, requires careful handling of existing nodes)
# This is a placeholder. Actual API calls to update nodes are complex.
# You might need to get the current nodes, filter out the old ones, add the new ones,
# and then send a PUT request to update the config.
echo "Switching traffic to Green environment..."
# Placeholder for actual Linode API call to update NodeBalancer nodes.
# This would involve fetching the current config, modifying the 'nodes' array
# to include Green IPs and exclude Blue IPs, and then sending a PUT request.
# Example:
# curl -s -X PUT \
# -H "Authorization: Bearer $LINODE_API_TOKEN" \
# -H "Content-Type: application/json" \
# -d '{"nodes": [{"address": "'"$GREEN_SERVER_IP"'", "port": 80, "label": "green-web-1", "weight": 100}]}' \
# "https://api.linode.com/v4/nodebalancers/$NODEBALANCER_ID/configs/$CONFIG_ID/nodes"
# This is a simplified example. You'd need to handle multiple nodes and existing configurations.
echo "Load Balancer updated to point to Green environment."
else
echo "Green server is already active. No action needed."
fi
Important Notes on the CI/CD Workflow:
- Secrets Management: Ensure
LINODE_SSH_USER,LINODE_SSH_KEY,GREEN_SERVER_IP, andLINODE_API_TOKENare securely stored as GitHub Secrets. - Linode API Token: The Linode API token needs `read` and `write` permissions for NodeBalancers.
- NodeBalancer ID: Replace
YOUR_NODEBALANCER_IDwith your actual NodeBalancer ID. - Switching Logic: The NodeBalancer update logic is a critical and complex part. The provided script is a conceptual outline. A robust implementation would involve:
- Identifying the *current* active environment (Blue or Green).
- Fetching the NodeBalancer configuration for the relevant port.
- Modifying the `nodes` array to point to the *new* environment’s servers and remove the old ones.
- Using a `PUT` request to update the NodeBalancer configuration.
- A more advanced strategy involves having *all* servers (Blue and Green) in the NodeBalancer configuration and using labels or weights to enable/disable them, which can be faster.
- Health Checks: The `curl` command in the workflow verifies the health of the newly deployed environment *before* switching traffic. If the health check fails, the deployment is halted.
- Permissions: Ensure the SSH user has sufficient permissions to write to
/var/www/myapp/greenand execute Artisan commands. Thechownandchmodcommands are essential.
The Zero-Downtime Switchover Mechanism
The magic of zero-downtime lies in how traffic is switched. When a new deployment is ready on the Green environment:
- The CI/CD pipeline deploys the new code to the Green servers.
- Artisan commands (migrations, cache clearing) are executed on the Green servers.
- A health check is performed on the Green environment.
- If the health check passes, the CI/CD pipeline updates the Linode NodeBalancer configuration. This is the crucial step: the NodeBalancer is reconfigured to send traffic to the Green environment’s servers instead of the Blue environment’s servers.
- The switchover itself is near-instantaneous, as it’s a configuration change on the load balancer.
- The Blue environment, now inactive, can be used for the *next* deployment cycle (becoming the “Green” environment for the subsequent deployment).
Manual Switchover (for testing or rollback):
You can manually trigger a switchover by updating the NodeBalancer configuration via the Linode Cloud Manager or by using the Linode API. This is also your rollback mechanism: if a deployment to Green causes issues, you can quickly switch the NodeBalancer back to the Blue environment.
Handling Database Migrations and Data Consistency
Database migrations are a common point of failure in blue-green deployments. Since both environments share a single database, migrations must be applied carefully.
Strategy: Backward-Compatible Migrations
- Schema Changes First: Any schema changes (e.g., adding columns, creating tables) should be deployed *before* the code that uses them. This means a migration that adds a column should be deployed, and then the application code that reads from that column should be deployed.
- Code Changes Second: The application code that relies on the new schema can then be deployed.
- Data Migration: If you need to populate new columns, this should be done in a separate migration or a background job after the schema change is live.
- Rollback: If a migration fails, you must be able to roll it back. This is easier if the migration only adds new elements and doesn’t modify existing ones in a way that breaks older code.
Example Migration Flow:
- Deployment 1 (Schema Update):
- Push a migration that adds a new column to a table.
- Deploy this migration to the *active* environment (e.g., Blue).
- Run the migration. The Blue environment now has the new column, but the current code doesn’t use it.
- Deploy the *new* application code (which uses the new column) to the *inactive* environment (Green).
- Run Artisan commands on Green.
- Switch traffic to Green. Now, the live application uses the new column.
- Deployment 2 (Code Update):
- Push application code that *uses* the new column.
- Deploy this code to the *inactive* environment (e.g., Blue).
- Run Artisan commands on Blue.
- Switch traffic to Blue.
The php artisan migrate --force command in the CI/CD pipeline assumes you have a strategy for handling migrations that works with your blue-green setup. For complex schema changes, consider using tools like `doctrine/dbal` for schema manipulation or implementing a phased rollout of migrations.
Advanced Considerations and Optimizations
Session Management: If your Laravel application relies on file-based sessions, ensure your two environments can access a shared session store (e.g., Redis, Memcached, or a database-backed session driver). This prevents users from being logged out during the switchover.
Caching: Similar to sessions, shared caching (Redis, Memcached) is essential for performance and consistency. Ensure both environments are configured to use the same cache backend.
Asset Management: Pre-compile your Laravel assets (CSS, JS) during the CI build process. Ensure these compiled assets are deployed to both environments. Using a CDN for assets is also highly recommended.
Health Check Endpoint: Make your /healthz endpoint as robust as necessary. It should ideally check critical dependencies like database connectivity (without performing heavy queries) and cache availability.
Automated Testing: Integrate automated integration tests that run against the *inactive* environment *after* deployment but *before* the traffic switch. This provides an extra layer of confidence.
Rollback Strategy: Define a clear and tested rollback procedure. This typically involves reconfiguring the NodeBalancer to point back to the previous stable environment. Ensure you can quickly revert code if necessary, though the blue-green model minimizes this need.
Environment Variables: Manage environment variables securely. Use Linode’s secrets management or a dedicated secrets manager. Ensure that environment variables are consistent across both Blue and Green environments.
By implementing this blue-green deployment strategy, you can achieve near-zero downtime for your Laravel applications on Linode, significantly improving your deployment reliability and user experience.