Despliegue Blue-Green en un entorno local con Docker
Introducción
El despliegue Blue-Green es una estrategia de despliegue diseñada para minimizar el tiempo de inactividad y los riesgos durante las actualizaciones de las aplicaciones. Este enfoque consiste en ejecutar dos entornos separados, Blue y Green, en los que uno sirve al tráfico (el entorno activo) mientras el otro está inactivo o en proceso de actualización. Una vez validadas las actualizaciones en el entorno inactivo, el tráfico se conmuta sin problemas, garantizando la estabilidad y minimizando las interrupciones.
En esta guía, demostramos cómo implementar Blue-Green Deployment usando Docker en un entorno local con Laravel como aplicación de ejemplo. Este enfoque está diseñado con fines educativos, ofreciendo una forma práctica de comprender los fundamentos del despliegue Blue-Green.
¿Por qué Docker para el despliegue Blue-Green?
Docker proporciona una forma eficaz de aislar y simular entornos para el despliegue Blue-Green en una configuración local. Con los contenedores Docker, puede crear entornos “blue” y “green” independientes, probar los cambios de forma segura y conmutar el tráfico entre ellos.
Esta guía aprovecha Docker para:
- Simular el comportamiento del despliegue en el mundo real.
- Visualice el proceso de implantación blue-green de forma accesible.
- Replica las comprobaciones de estado y el enrutamiento del tráfico, de forma similar a las configuraciones de plataformas como AWS ECS o Kubernetes.
Aunque hay otras formas de probar los despliegues localmente, este método se centra en la simplicidad y la claridad, lo que permite a los desarrolladores comprender la estrategia sin la complejidad de gestionar la infraestructura de la nube.
¿Por qué realizar pruebas a nivel local?
Probar el despliegue Blue-Green localmente ofrece varias ventajas:
- Depuración segura: Identifique problemas en los scripts de despliegue sin afectar a la producción.
- Replicar flujos de trabajo de producción: Validar transiciones de entornos, integraciones de API y tareas programadas.
- Pruebas sin tiempos de inactividad: Cambie el tráfico de un contenedor a otro sin problemas, garantizando una experiencia de usuario fluida.
- Aprendizaje práctico: Conozca a fondo las estrategias de despliegue observando sus efectos en tiempo real.
Esta configuración local le permite perfeccionar con confianza las estrategias de implantación antes de escalarlas a entornos de producción.
Configuración de demostración
Para esta demostración se utilizan los siguientes componentes:
- Nginx: Actúa como proxy inverso para enrutar el tráfico al entorno activo.
- Comprobaciones de salud o health-checks: Supervisa la disponibilidad de los contenedores y decide cuándo cambiar el tráfico.
- Laravel: Ejemplo de aplicación para visualizar transiciones de entorno.
El papel vital de los health-checks en el despliegue Blue-Green
Uno de los componentes clave de esta configuración es el uso de comprobaciones de estado. Estas comprobaciones supervisan continuamente el estado de un contenedor para determinar si está preparado para gestionar tráfico. Las comprobaciones de estado son esenciales en el despliegue Blue-Green, ya que garantizan la estabilidad y minimizan los riesgos durante la transición entre entornos.
En esta configuración local, las comprobaciones de estado desempeñan las siguientes funciones:
- Simular el comportamiento de producción: Las plataformas como AWS ECS y Kubernetes dependen en gran medida de las comprobaciones de estado para determinar si un servicio está operativo. Al definir las comprobaciones de estado en nuestro archivo
docker-compose.yml, replicamos este comportamiento en un entorno local, proporcionando una experiencia cercana al mundo real. - Automatización de las decisiones de despliegue: El script de despliegue utiliza comprobaciones de estado para decidir si el tráfico puede dirigirse al nuevo entorno. Si un contenedor no supera las comprobaciones de estado, el script evita cambiar el tráfico y activa una reversión para mantener la disponibilidad de la aplicación.
- Garantizar la disponibilidad de la aplicación: Antes de conmutar el tráfico a un nuevo entorno, las comprobaciones de estado validan que el nuevo contenedor esté plenamente operativo. De este modo se evitan los tiempos de inactividad causados por despliegues incompletos o errores de aplicación.
Configuración del chequeo
healthcheck:
test: ['CMD-SHELL', 'curl -f http://localhost || exit 1']
interval: 30s
timeout: 10s
retries: 3
start_period: 5s
- test: Ejecuta un comando para comprobar si el contenedor responde. En este caso, envía una petición HTTP a la aplicación y espera una respuesta satisfactoria.
- interval: Establece la frecuencia de ejecución del chequeo de salud.
- timeout: Define cuánto tiempo puede durar la comprobación de estado antes de considerarse un fallo.
- retries: Especifica el número de fallos consecutivos antes de que el contenedor se marque como no saludable.
- start_period: Permite un periodo de gracia para que el contenedor se inicialice completamente antes de que comiencen las comprobaciones de salud.
Representación visual del despliegue azul y verde
He aquí un diagrama simplificado del proceso de implantación azul-verde:
+-------------------+ +--------------------+
| | | |
| Active (Blue) | <--- Traffic --->| Idle (Green) |
| | | |
+-------------------+ +--------------------+
↑ ↑
| |
Updates & Tests Updates & Tests
| |
↓ ↓
+-------------------+ +--------------------+
| New Green | | New Blue |
| Deployment | | Deployment |
+-------------------+ +--------------------+
| |
Health Checks Passed Health Checks Passed
↓ ↓
+-------------------+ +--------------------+
| Idle (Blue) | <--- Traffic ---| Active (Green) |
+-------------------+ +--------------------+
Por qué son fundamentales los health-checks
Los health-checks son la espina dorsal de esta estrategia de despliegue azul y verde porque:
- Evite el tiempo de inactividad: Solo los contenedores que superan las comprobaciones de estado reciben tráfico, lo que garantiza que los usuarios siempre se dirijan a un entorno funcional.
- Habilitar rollbacks automatizados: Si un contenedor falla en sus comprobaciones de salud, el script revierte al entorno previamente activo.
- Fomentan la confianza: Al validar la preparación del nuevo entorno, las comprobaciones de estado reducen el riesgo de implantar actualizaciones defectuosas.
Componentes y archivos
He aquí un resumen de los archivos utilizados en esta configuración:
- Script de despliegue: Automatiza la creación, conmutación y reversión de entornos Azul-Verde.
abin/install.sh
#!/bin/bash
set -e # Stop script execution on error
NGINX_CONF_PATH="./docker/nginx/active_backend.conf"
NGINX_CONTAINER="app"
ENV_FILE=".env"
build_containers() {
echo "📦 Building Docker containers..."
docker compose build
echo "✅ Docker containers built successfully."
}
prepare_nginx_config() {
if [ ! -d "./docker/nginx" ]; then
echo "📂 Nginx directory not found. Creating it..."
mkdir -p ./docker/nginx
echo "✅ Nginx directory created."
fi
}
update_nginx_config() {
local active_color=$1
echo "🔄 Updating Nginx configuration to route traffic to '$active_color' containers..."
cat > "$NGINX_CONF_PATH" <<EOL
upstream app_backend {
server $active_color:9000 max_fails=3 fail_timeout=30s;
}
EOL
echo "📋 Copying Nginx configuration to the container..."
docker cp "$NGINX_CONF_PATH" "$NGINX_CONTAINER:/etc/nginx/conf.d/active_backend.conf"
echo "🔁 Reloading Nginx to apply the new configuration..."
docker exec "$NGINX_CONTAINER" nginx -s reload >/dev/null 2>&1
echo "✅ Nginx configuration updated and reloaded successfully."
}
wait_for_health() {
local container_prefix=$1
local retries=5
local unhealthy_found
echo "⏳ Waiting for containers with prefix '$container_prefix' to become healthy..."
while (( retries > 0 )); do
unhealthy_found=false
for container_name in $(docker ps --filter "name=$container_prefix" --format "{{.Names}}"); do
health_status=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}}' "$container_name" || echo "unknown")
if [[ "$health_status" != "healthy" ]]; then
unhealthy_found=true
echo "🚧 Container '$container_name' is not ready. Current status: $health_status."
fi
done
if ! $unhealthy_found; then
echo "✅ All containers with prefix '$container_prefix' are healthy."
return 0
fi
echo "⏳ Retrying... ($retries retries left)"
((retries--))
sleep 5
done
echo "❌ Error: Some containers with prefix '$container_prefix' are not healthy. Aborting deployment."
rollback
exit 0
}
rollback() {
echo "🛑 Rolling back deployment. Ensuring the active environment remains intact."
if [ -n "$PREVIOUS_COLOR" ]; then
echo "🔄 Restoring CONTAINER_COLOR=$PREVIOUS_COLOR in .env."
sed -i.bak "s/^CONTAINER_COLOR=.*/CONTAINER_COLOR=$PREVIOUS_COLOR/" "$ENV_FILE"
rm -f "$ENV_FILE.bak"
echo "✅ Restored CONTAINER_COLOR=$PREVIOUS_COLOR in .env."
else
echo "🚧 No previous CONTAINER_COLOR found to restore."
fi
if docker ps --filter "name=green" --format "{{.Names}}" | grep -q "green"; then
echo "✅ Active environment 'green' remains intact."
echo "🛑 Stopping and removing 'blue' containers..."
docker compose stop "blue" >/dev/null 2>&1 || true
docker compose rm -f "blue" >/dev/null 2>&1 || true
elif docker ps --filter "name=blue" --format "{{.Names}}" | grep -q "blue"; then
echo "✅ Active environment 'blue' remains intact."
echo "🛑 Stopping and removing 'green' containers..."
docker compose stop "green" >/dev/null 2>&1 || true
docker compose rm -f "green" >/dev/null 2>&1 || true
else
echo "❌ No active environment detected after rollback. Manual intervention might be needed."
fi
echo "🔄 Rollback completed."
}
update_env_file() {
local active_color=$1
# check if .env file exists
if [ ! -f "$ENV_FILE" ]; then
echo "❌ .env file not found. Creating a new one..."
echo "CONTAINER_COLOR=$active_color" > "$ENV_FILE"
echo "✅ Created .env file with CONTAINER_COLOR=$active_color."
return
fi
# backup previous CONTAINER_COLOR value
if grep -q "^CONTAINER_COLOR=" "$ENV_FILE"; then
PREVIOUS_COLOR=$(grep "^CONTAINER_COLOR=" "$ENV_FILE" | cut -d '=' -f 2)
echo "♻️ Backing up previous CONTAINER_COLOR=$PREVIOUS_COLOR."
else
PREVIOUS_COLOR=""
fi
# update CONTAINER_COLOR value in .env
if grep -q "^CONTAINER_COLOR=" "$ENV_FILE"; then
sed -i.bak "s/^CONTAINER_COLOR=.*/CONTAINER_COLOR=$active_color/" "$ENV_FILE"
echo "🔄 Updated CONTAINER_COLOR=$active_color in .env"
else
echo "CONTAINER_COLOR=$active_color" >> "$ENV_FILE"
echo "🖋️ Added CONTAINER_COLOR=$active_color to .env"
fi
# remove backup file
if [ -f "$ENV_FILE.bak" ]; then
rm "$ENV_FILE.bak"
fi
}
install_dependencies() {
local container=$1
echo "📥 Installing dependencies in container '$container'..."
# Install Laravel dependencies
docker exec -u root -it "$container" bash -c "composer install --no-dev --optimize-autoloader"
docker exec -u root -it "$container" bash -c "mkdir -p database && touch database/database.sqlite"
# Permissions setup
docker exec -u root -it "$container" bash -c "chown www-data:www-data -R ./storage ./bootstrap ./database"
docker exec -u root -it "$container" bash -c "chmod -R 775 ./storage ./bootstrap/cache"
# Clear caches and run migrations
docker exec -u root -it "$container" bash -c "php artisan cache:clear"
docker exec -u root -it "$container" bash -c "php artisan config:clear"
docker exec -u root -it "$container" bash -c "php artisan route:clear"
docker exec -u root -it "$container" bash -c "php artisan view:clear"
docker exec -u root -it "$container" bash -c "php artisan migrate --force"
echo "✅ Dependencies installed and database initialized successfully in container '$container'."
}
deploy() {
local active=$1
local new=$2
# Update .env before deploying
update_env_file "$new"
echo "🚀 Starting deployment. Current active environment: '$active'. Deploying to '$new'..."
docker compose --profile "$new" up -d
wait_for_health "$new"
install_dependencies "$new"
update_nginx_config "$new"
echo "🗑️ Removing old environment: '$active'..."
echo "🛑 Stopping '$active' containers..."
docker compose stop $active >/dev/null 2>&1 || true
echo "🗑️ Removing '$active' containers..."
docker compose rm -f $active >/dev/null 2>&1 || true
update_env_file "$new"
echo "✅ Deployment to '$new' completed successfully."
}
get_active_container() {
if [ -f "$ENV_FILE" ] && grep -q "CONTAINER_COLOR" "$ENV_FILE"; then
grep "CONTAINER_COLOR" "$ENV_FILE" | cut -d '=' -f 2
else
echo ""
fi
}
# Main script logic
prepare_nginx_config
build_containers
ACTIVE_COLOR=$(get_active_container)
if [ -z "$ACTIVE_COLOR" ]; then
# if no active container found, deploy 'blue'
echo "🟦 Initial setup. Bringing up 'blue' containers..."
docker compose --profile blue up -d
wait_for_health "blue"
install_dependencies "blue"
update_nginx_config "blue"
update_env_file "blue"
elif [ "$ACTIVE_COLOR" == "green" ]; then
# if the active is 'green', deploy 'blue'
PREVIOUS_COLOR="green"
deploy "green" "blue"
elif [ "$ACTIVE_COLOR" == "blue" ]; then
# if the active is 'blue', deploy 'green'
PREVIOUS_COLOR="blue"
deploy "blue" "green"
else
# if the active is neither 'green' nor 'blue', reset to 'blue'
echo "🚧 Unexpected CONTAINER_COLOR value. Resetting to 'blue'..."
PREVIOUS_COLOR=""
docker compose --profile blue up -d
wait_for_health "blue"
install_dependencies "blue"
update_nginx_config "blue"
update_env_file "blue"
fi
echo "🎉 Deployment successful!"
- Configuración de Nginx: Maneja el enrutamiento de tráfico al entorno activo.
nginx/default.conf
server {
listen 80;
index index.php index.html;
client_max_body_size 20M;
root /var/www/html/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ .php$ {
fastcgi_pass app_backend;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /.ht {
deny all;
}
}
- Configuración PHP-FPM: Gestiona los procesos PHP para Laravel.
php/www.conf
listen = 9000
user = www-data
group = www-data
[www]
pm = dynamic
pm.max_children = 20
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15
- Dockerfile: Configura el entorno de Laravel con las dependencias necesarias.
Dockerfile
FROM php:8.2.0-fpm
WORKDIR /var/www/html
RUN apt-get update && apt-get install -y \
curl \
dos2unix \
git \
libonig-dev \
libpng-dev \
libxml2-dev \
libzip-dev \
unzip \
zip \
libfcgi0ldbl \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
&& mkdir -p /var/www/html/storage /var/www/html/bootstrap/cache && \
chown -R :www-data ./bootstrap/cache && \
mkdir -p storage && \
cd storage/ && \
mkdir -p logs && \
mkdir -p app && \
mkdir -p framework/sessions && \
mkdir -p framework/views && \
mkdir -p framework/cache && \
chmod -R 775 framework logs app && \
chown -R :www-data ./framework ./logs ./app && \
git config --global --add safe.directory '*'
COPY ./scripts/start.sh /usr/local/bin/start.sh
RUN chmod +x /usr/local/bin/start.sh
CMD ["/usr/local/bin/start.sh"]
- Docker Compose: Define los contenedores Blue-Green, Nginx y las configuraciones de comprobación de salud.
docker-compose.yml
services:
blue:
container_name: blue
env_file:
- .env
profiles:
- blue
build:
context: ./docker
dockerfile: Dockerfile
volumes:
- ./:/var/www/html
- ./docker/supervisor/supervisord.conf:/etc/supervisor/supervisord.conf
- ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf
healthcheck:
test:
[
'CMD-SHELL',
'SCRIPT_FILENAME=/var/www/html/public/index.php REQUEST_METHOD=GET cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1'
]
interval: 30s
timeout: 10s
retries: 3
start_period: 5s
green:
container_name: green
profiles:
- green
env_file:
- .env
build:
context: ./docker
dockerfile: Dockerfile
volumes:
- ./:/var/www/html
- ./docker/supervisor/supervisord.conf:/etc/supervisor/supervisord.conf
- ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf
healthcheck:
test:
[
'CMD-SHELL',
'SCRIPT_FILENAME=/var/www/html/public/index.php REQUEST_METHOD=GET cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1'
]
interval: 30s
timeout: 10s
retries: 3
start_period: 5s
app:
image: nginx:alpine
container_name: app
profiles:
- blue
- green
ports:
- '${PORT-80}:80'
volumes:
- ./:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost']
interval: 30s
timeout: 10s
retries: 3
start_period: 5s
Health-checks en acción
Cómo funciona
- Al desplegar actualizaciones:
- Se inicia un nuevo entorno (Azul o Verde).
- Los health-checks validan la preparación del contenedor.
- Si pasan:
- El tráfico se dirige al nuevo entorno.
- El antiguo entorno queda inactivo, listo para futuras actualizaciones.
- Si fallan:
- Se desencadena un rollback, y el entorno activo permanece inalterado.
Ejemplo de reversión automatizada
El script de despliegue garantiza que se produzca una reversión cuando fallen las comprobaciones de estado. Por ejemplo:
- El azul está activo y el verde no pasa los health-checks.
- El tráfico sigue dirigido a Azul.
- El contenedor Verde defectuoso se elimina, preservando la estabilidad de la aplicación.
Ejemplos:
- 🎥 Deployment demo: Visualiza la transición de Azul a Verde.
- 🎥 Rollback demo: Demuestra cómo las reversiones mantienen la estabilidad cuando falla una actualización.
Conclusión
Esta guía muestra cómo simular y comprender el despliegue Blue-Green con Docker en un entorno local. Al aprovechar las comprobaciones de estado, la conmutación de tráfico y las reversiones automatizadas, puede minimizar el tiempo de inactividad y garantizar la estabilidad durante los despliegues. Aunque este enfoque es simplificado, los conceptos pueden extenderse fácilmente a sistemas de producción.
Utilice esta configuración para probar y perfeccionar las estrategias de despliegue con confianza antes de escalar a producción.
Código completo en este repositorio