DevOps

Construcción de un pipeline CI/CD tolerante a fallos para aplicaciones PHP con Docker y Kubernetes

Ruslan Ismailov Publicado 18 min de lectura
C

Introducción: CI/CD para proyectos PHP en 2026

En 2026, CI/CD no es una práctica opcional, sino un requisito fundamental para cualquier desarrollo PHP serio. El despliegue manual, las migraciones olvidadas y el clásico "funcionaba en mi máquina" son síntomas de equipos que aún no han construido un pipeline de automatización sólido.

Para proyectos PHP, especialmente los basados en Laravel o Symfony, la entrega continua de código conlleva riesgos adicionales: artefactos de Composer, configuraciones de OPcache, migraciones de base de datos, colas de tareas. Sin una automatización clara, cada despliegue es una prueba de estrés manual para el equipo.

En este artículo construiremos un pipeline CI/CD completo y tolerante a fallos para una aplicación PHP usando Docker, Kubernetes, GitLab CI y GitHub Actions. Analizaremos cada etapa: desde el linting del código hasta Canary Releases y el rollback automático.

Visión general de herramientas: GitHub Actions vs GitLab CI para PHP/Docker

Elegir una plataforma CI/CD es una decisión estratégica. Veamos los dos líderes aplicados a PHP y Docker:

  • GitLab CI/CD — integrado en GitLab, se configura mediante .gitlab-ci.yml. Soporta Container Registry integrado, Auto DevOps y un sistema avanzado de entornos. Ideal para infraestructura self-hosted y equipos corporativos. Integración nativa con Kubernetes a través del GitLab Agent.

  • GitHub Actions — se configura mediante YAML en el directorio .github/workflows/. Enorme ecosistema de actions listas para usar, plan gratuito para repositorios públicos. Ideal para proyectos open-source y equipos que ya utilizan GitHub.

Para PHP/Docker, ambas herramientas funcionan igual de bien. GitLab CI gana en despliegues self-hosted y registry integrado. GitHub Actions destaca por la velocidad de inicio y el ecosistema de componentes listos para usar.

En este artículo mostraremos ejemplos para ambos, con énfasis en GitLab CI como solución más completa para proyectos PHP enterprise.

Contenedorización de la aplicación PHP: Dockerfile óptimo

La base de cualquier pipeline CI/CD para PHP es un Dockerfile bien diseñado. Usaremos compilación multi-stage, Alpine Linux y OPcache para un rendimiento óptimo.

# Dockerfile
# ==========================================
# Stage 1: Composer dependencies
# ==========================================
FROM composer:2.7 AS composer

WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
    --no-dev \
    --no-interaction \
    --prefer-dist \
    --optimize-autoloader \
    --no-scripts

COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative

# ==========================================
# Stage 2: Assets build (si hay frontend)
# ==========================================
FROM node:20-alpine AS assets

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# ==========================================
# Stage 3: Production PHP image
# ==========================================
FROM php:8.3-fpm-alpine AS production

# Dependencias del sistema
RUN apk add --no-cache \
    nginx \
    supervisor \
    libpq \
    libzip \
    && apk add --no-cache --virtual .build-deps \
    libpq-dev \
    libzip-dev \
    autoconf \
    gcc \
    g++ \
    make \
    && docker-php-ext-install \
    pdo_pgsql \
    opcache \
    zip \
    pcntl \
    bcmath \
    && pecl install redis \
    && docker-php-ext-enable redis \
    && apk del .build-deps

# Configuración de OPcache para producción
RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/opcache.ini \
    && echo "opcache.memory_consumption=256" >> /usr/local/etc/php/conf.d/opcache.ini \
    && echo "opcache.interned_strings_buffer=16" >> /usr/local/etc/php/conf.d/opcache.ini \
    && echo "opcache.max_accelerated_files=20000" >> /usr/local/etc/php/conf.d/opcache.ini \
    && echo "opcache.revalidate_freq=0" >> /usr/local/etc/php/conf.d/opcache.ini \
    && echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/opcache.ini

WORKDIR /var/www/html

# Copiamos dependencias de las etapas anteriores
COPY --from=composer /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
COPY . .

# Permisos de acceso
RUN chown -R www-data:www-data storage bootstrap/cache \
    && chmod -R 775 storage bootstrap/cache

# Configuración de Nginx
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf

EXPOSE 80

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

Principios clave de este Dockerfile:

  • Compilación multi-stage — la imagen final no contiene Composer, Node.js ni dependencias de compilación

  • Alpine Linux — imagen base mínima (~5 MB frente a ~900 MB de Debian)

  • OPcache con validate_timestamps=0 — máximo rendimiento en producción

  • Capas en cachécomposer.json y composer.lock se copian por separado antes que el resto del código

Etapa CI: linting, pruebas y compilación de la imagen

PHP_CodeSniffer y PHPStan

El análisis estático es la primera barrera del pipeline. Configure phpcs.xml y phpstan.neon en la raíz del proyecto:

# phpstan.neon
parameters:
    level: 8
    paths:
        - app
        - tests
    excludePaths:
        - vendor
    checkMissingIterableValueType: false

Configuración de PHPUnit

<!-- phpunit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage>
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value=":memory:"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
    </php>
</phpunit>

Pipeline completo en .gitlab-ci.yml

A continuación, una configuración de GitLab CI lista para producción para PHP/Laravel con despliegue en Kubernetes:

# .gitlab-ci.yml
variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
  KUBERNETES_NAMESPACE: production

stages:
  - validate
  - test
  - build
  - migrate
  - deploy
  - verify

# ==========================================
# Plantillas
# ==========================================
.php_template: &php_template
  image: php:8.3-cli-alpine
  before_script:
    - apk add --no-cache git unzip libzip-dev
    - docker-php-ext-install zip
    - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer
    - composer install --no-interaction --prefer-dist --optimize-autoloader

# ==========================================
# Stage: validate
# ==========================================
phpcs:
  <<: *php_template
  stage: validate
  script:
    - vendor/bin/phpcs --standard=PSR12 app/ --report=checkstyle --report-file=phpcs-report.xml
  artifacts:
    reports:
      codequality: phpcs-report.xml
    when: always
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

phpstan:
  <<: *php_template
  stage: validate
  script:
    - vendor/bin/phpstan analyse --memory-limit=512M --error-format=gitlab > phpstan-report.json || true
  artifacts:
    reports:
      codequality: phpstan-report.json
    when: always
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

# ==========================================
# Stage: test
# ==========================================
unit_tests:
  <<: *php_template
  stage: test
  services:
    - name: postgres:16-alpine
      alias: postgres
  variables:
    POSTGRES_DB: test_db
    POSTGRES_USER: test_user
    POSTGRES_PASSWORD: test_password
    DB_HOST: postgres
    DB_DATABASE: test_db
    DB_USERNAME: test_user
    DB_PASSWORD: test_password
  before_script:
    - apk add --no-cache git unzip libzip-dev libpq-dev
    - docker-php-ext-install zip pdo_pgsql
    - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer
    - composer install --no-interaction --prefer-dist
    - cp .env.testing .env
    - php artisan key:generate
    - php artisan migrate --force
  script:
    - vendor/bin/phpunit --coverage-text --coverage-cobertura=coverage.xml
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
    when: always
  coverage: '/^\s*Lines:\s*\d+\.?\d*%/'

# ==========================================
# Stage: build
# ==========================================
build_image:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build
        --cache-from $IMAGE_LATEST
        --build-arg BUILDKIT_INLINE_CACHE=1
        --tag $IMAGE_TAG
        --tag $IMAGE_LATEST
        .
    - docker push $IMAGE_TAG
    - docker push $IMAGE_LATEST
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_COMMIT_BRANCH =~ /^release\/.*/

# ==========================================
# Stage: migrate
# ==========================================
run_migrations:
  stage: migrate
  image:
    name: bitnami/kubectl:latest
    entrypoint: [""] 
  script:
    - kubectl config use-context $KUBE_CONTEXT
    - |
      kubectl run migration-$CI_COMMIT_SHORT_SHA \
        --image=$IMAGE_TAG \
        --restart=Never \
        --namespace=$KUBERNETES_NAMESPACE \
        --env="APP_ENV=production" \
        --command -- php artisan migrate --force
    - kubectl wait --for=condition=complete \
        job/migration-$CI_COMMIT_SHORT_SHA \
        --timeout=300s \
        --namespace=$KUBERNETES_NAMESPACE || \
        (kubectl logs -l job-name=migration-$CI_COMMIT_SHORT_SHA --namespace=$KUBERNETES_NAMESPACE && exit 1)
    - kubectl delete pod migration-$CI_COMMIT_SHORT_SHA --namespace=$KUBERNETES_NAMESPACE
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  needs:
    - build_image

# ==========================================
# Stage: deploy (Rolling Update)
# ==========================================
deploy_production:
  stage: deploy
  image:
    name: bitnami/kubectl:latest
    entrypoint: [""]
  environment:
    name: production
    url: https://app.example.com
  script:
    - kubectl config use-context $KUBE_CONTEXT
    - kubectl set image deployment/php-app
        php-app=$IMAGE_TAG
        --namespace=$KUBERNETES_NAMESPACE
    - kubectl rollout status deployment/php-app
        --namespace=$KUBERNETES_NAMESPACE
        --timeout=300s
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  needs:
    - run_migrations

# ==========================================
# Stage: verify (smoke tests)
# ==========================================
smoke_tests:
  stage: verify
  image: curlimages/curl:latest
  script:
    - sleep 10
    - curl -f -s -o /dev/null https://app.example.com/health || (echo "Health check failed" && exit 1)
    - echo "Deployment verified successfully"
  needs:
    - deploy_production
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

GitHub Actions: workflow equivalente

# .github/workflows/deploy.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          tools: composer:v2, phpcs, phpstan
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: PHP CodeSniffer
        run: vendor/bin/phpcs --standard=PSR12 app/
      - name: PHPStan
        run: vendor/bin/phpstan analyse --memory-limit=512M

  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: test_db
          POSTGRES_USER: test_user
          POSTGRES_PASSWORD: test_password
        ports:
          - 5432:5432
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: pdo_pgsql, zip, redis
          coverage: xdebug
      - run: composer install --no-interaction --prefer-dist
      - run: cp .env.testing .env && php artisan key:generate
      - run: php artisan migrate --force
      - run: vendor/bin/phpunit --coverage-clover coverage.xml
      - uses: codecov/codecov-action@v4
        with:
          file: coverage.xml

  build-and-push:
    needs: [validate, test]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=sha-
            type=raw,value=latest
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: azure/setup-kubectl@v4
      - name: Configure kubectl
        run: |
          echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig.yaml
          export KUBECONFIG=kubeconfig.yaml
      - name: Deploy to Kubernetes
        run: |
          export KUBECONFIG=kubeconfig.yaml
          kubectl set image deployment/php-app \
            php-app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} \
            --namespace=production
          kubectl rollout status deployment/php-app \
            --namespace=production --timeout=300s

Estrategias de despliegue en Kubernetes

Rolling Update — estrategia estándar

Rolling Update es la estrategia más utilizada para aplicaciones PHP. Kubernetes reemplaza gradualmente los pods antiguos por los nuevos:

# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-app
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: php-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # +1 pod nuevo durante la actualización
      maxUnavailable: 0  # 0 pods no disponibles durante la actualización
  template:
    metadata:
      labels:
        app: php-app
        version: "{{ .Values.image.tag }}"
    spec:
      containers:
        - name: php-app
          image: registry.example.com/php-app:{{ .Values.image.tag }}
          ports:
            - containerPort: 80
          readinessProbe:
            httpGet:
              path: /health
              port: 80
            initialDelaySeconds: 10
            periodSeconds: 5
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /health
              port: 80
            initialDelaySeconds: 30
            periodSeconds: 10
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          envFrom:
            - secretRef:
                name: php-app-secrets
            - configMapRef:
                name: php-app-config

Blue-Green Deployment

Blue-Green permite mantener dos entornos idénticos y cambiar el tráfico de forma instantánea. Para PHP esto es especialmente útil en migraciones de gran envergadura:

# Despliegue de la versión green
kubectl apply -f kubernetes/deployment-green.yaml

# Esperamos a que green esté listo
kubectl rollout status deployment/php-app-green --timeout=300s

# Cambiamos el Service a green
kubectl patch service php-app-service \
  -p '{"spec":{"selector":{"version":"green"}}}'

# Verificamos y revertimos si es necesario
kubectl patch service php-app-service \
  -p '{"spec":{"selector":{"version":"blue"}}}'

Canary Releases

Los Canary Releases permiten dirigir un pequeño porcentaje del tráfico (5-10%) a la nueva versión e incrementarlo de forma gradual:

# deployment-canary.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-app-canary
  namespace: production
spec:
  replicas: 1  # 1 de cada 10 pods = 10% del tráfico
  selector:
    matchLabels:
      app: php-app
      track: canary
  template:
    metadata:
      labels:
        app: php-app
        track: canary
    spec:
      containers:
        - name: php-app
          image: registry.example.com/php-app:new-version

Gestión de secretos: Kubernetes Secrets y HashiCorp Vault

Nunca almacene secretos en el repositorio Git. Existen dos enfoques principales:

Kubernetes Secrets (nivel básico)

# Creación de un secreto desde el archivo .env
kubectl create secret generic php-app-secrets \
  --from-literal=APP_KEY=base64:tu_clave \
  --from-literal=DB_PASSWORD=contraseña_secreta \
  --from-literal=REDIS_PASSWORD=contraseña_redis \
  --namespace=production

# O mediante manifiesto (los valores deben estar codificados en base64)
apiVersion: v1
kind: Secret
metadata:
  name: php-app-secrets
  namespace: production
type: Opaque
data:
  APP_KEY: YmFzZTY0OmtleQ==
  DB_PASSWORD: c2VjcmV0

HashiCorp Vault (nivel producción)

Para entornos de producción, use Vault con Vault Agent Injector:

# kubernetes/vault-annotations.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-app
spec:
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "php-app"
        vault.hashicorp.com/agent-inject-secret-env: "secret/data/php-app/production"
        vault.hashicorp.com/agent-inject-template-env: |
          {{- with secret "secret/data/php-app/production" -}}
          export APP_KEY={{ .Data.data.app_key }}
          export DB_PASSWORD={{ .Data.data.db_password }}
          {{- end }}

En GitLab CI, los secretos se pasan mediante variables de entorno con enmascaramiento: Settings → CI/CD → Variables → Masked. En GitHub Actions use Settings → Secrets and variables → Actions.

Migraciones automáticas de base de datos

Las migraciones son un momento crítico del despliegue. Existen dos patrones:

Init Container (recomendado para Kubernetes)

# Agregue initContainers en deployment.yaml
spec:
  initContainers:
    - name: run-migrations
      image: registry.example.com/php-app:{{ .Values.image.tag }}
      command: ["php", "artisan", "migrate", "--force"]
      envFrom:
        - secretRef:
            name: php-app-secrets
        - configMapRef:
            name: php-app-config

El Init Container se ejecutará antes que los pods principales y finalizará. Solo tras su éxito, Kubernetes iniciará el contenedor principal. Esto garantiza que las migraciones se ejecuten antes del arranque de la aplicación.

Principios de migraciones seguras

  • Migraciones retrocompatibles: el nuevo código debe funcionar con el esquema antiguo de la BD (patrón expand-contract)

  • Nunca elimine columnas en la misma versión en que las quita del código

  • Use transacciones en las migraciones para garantizar atomicidad

  • Pruebe el rollback: cada migración debe tener un método down()

Rollback del despliegue: estrategias y automatización

Rollback manual con kubectl

# Ver el historial de despliegues
kubectl rollout history deployment/php-app --namespace=production

# Revertir a la versión anterior
kubectl rollout undo deployment/php-app --namespace=production

# Revertir a una revisión específica
kubectl rollout undo deployment/php-app \
  --to-revision=3 \
  --namespace=production

# Verificar el estado tras el rollback
kubectl rollout status deployment/php-app --namespace=production

Rollback automático en GitLab CI

# Agregue la etapa de rollback automático en .gitlab-ci.yml
auto_rollback:
  stage: verify
  image:
    name: bitnami/kubectl:latest
    entrypoint: [""]
  script:
    - kubectl config use-context $KUBE_CONTEXT
    - |
      # Verificamos el health endpoint 3 veces con intervalo de 10 segundos
      for i in 1 2 3; do
        STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://app.example.com/health)
        if [ "$STATUS" != "200" ]; then
          echo "Health check failed (attempt $i), HTTP $STATUS"
          if [ "$i" -eq 3 ]; then
            echo "Rolling back deployment..."
            kubectl rollout undo deployment/php-app --namespace=$KUBERNETES_NAMESPACE
            exit 1
          fi
          sleep 10
        else
          echo "Health check passed"
          exit 0
        fi
      done
  needs:
    - deploy_production
  when: on_success

Monitoreo del pipeline y notificaciones

Un pipeline tolerante a fallos sin monitoreo es una ilusión de fiabilidad. Integre las siguientes herramientas:

Notificaciones en Slack/Telegram desde GitLab CI

# Agregue al final de .gitlab-ci.yml
notify_success:
  stage: .post
  image: curlimages/curl:latest
  script:
    - |
      curl -X POST $SLACK_WEBHOOK_URL \
        -H 'Content-type: application/json' \
        --data '{"text":"✅ Despliegue de *'$CI_PROJECT_NAME'* exitoso. Commit: '$CI_COMMIT_SHORT_SHA' ('$CI_COMMIT_AUTHOR')"}'
  when: on_success
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

notify_failure:
  stage: .post
  image: curlimages/curl:latest
  script:
    - |
      curl -X POST $SLACK_WEBHOOK_URL \
        -H 'Content-type: application/json' \
        --data '{"text":"🚨 ¡Despliegue de *'$CI_PROJECT_NAME'* FALLIDO! Pipeline: '$CI_PIPELINE_URL'"}'
  when: on_failure
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

Métricas del pipeline: qué monitorear

  • Deployment Frequency — con qué frecuencia despliega el equipo (métrica DORA)

  • Lead Time for Changes — tiempo desde el commit hasta producción

  • Change Failure Rate — porcentaje de despliegues que requirieron rollback

  • Mean Time to Recovery (MTTR) — tiempo promedio de recuperación tras un fallo

Para el monitoreo de Kubernetes use la combinación Prometheus + Grafana. Para el rastreo de despliegues, ArgoCD o el dashboard de GitLab Environments integrado.

Endpoint /health para PHP/Laravel

ReadinessProbe y LivenessProbe en Kubernetes requieren un endpoint de health funcional. Agréguelo en Laravel:

<?php
// routes/web.php
Route::get('/health', function () {
    try {
        // Verificamos la conexión con la BD
        DB::select('SELECT 1');
        // Verificamos Redis
        Redis::ping();

        return response()->json([
            'status' => 'ok',
            'timestamp' => now()->toISOString(),
            'version' => config('app.version'),
        ], 200);
    } catch (\Exception $e) {
        return response()->json([
            'status' => 'error',
            'message' => $e->getMessage(),
        ], 503);
    }
})->middleware('throttle:60,1');

Checklist: señales de un pipeline CI/CD listo para producción

  • Todos los secretos están almacenados en Vault o en variables CI/CD (no en el código)

  • El pipeline no despliega si los tests fallan

  • Las imágenes Docker tienen tags específicos (no solo latest)

  • Las migraciones se ejecutan mediante Init Container, no manualmente

  • ReadinessProbe configurado — Kubernetes no envía tráfico a pods no listos

  • El rollback tarda no más de 2 minutos

  • El equipo recibe notificaciones sobre el estado de cada despliegue

  • Cobertura de tests mínima del 70%, el informe se publica en MR/PR

  • La compilación tarda no más de 10 minutos (de lo contrario los desarrolladores ignoran el pipeline)

Conclusión

Construir un pipeline CI/CD tolerante a fallos para PHP con Docker y Kubernetes es una inversión que se amortiza con el primer despliegue fallido que evita. Comience con un pipeline básico: linting → pruebas → compilación → despliegue. Luego itere: agregue gestión de secretos con Vault, configure el rollback automático e implemente Canary Releases.

Principio clave: cada paso del pipeline debe estar automatizado, ser idempotente y observable. Si algo no puede revertirse automáticamente, es un riesgo que debe eliminarse antes del próximo despliegue.

Invierta tiempo en la calidad del pipeline hoy, y su equipo podrá desplegar en viernes sin miedo.

Tecnologías

Etiquetas

Ruslan Ismailov

Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →