Blog

Infraestructura 10 min de lectura

Docker para PHP en producción: una guía práctica de configuración para aplicaciones empresariales

Docker hace que una aplicación PHP se ejecute de la misma manera en una computadora portátil, en pruebas y en producción, y hace que los lanzamientos y reversiones sean rutinarios en lugar de estresantes. También agrega partes móviles y una configuración descuidada es peor que un servidor simple. Esta guía muestra una línea base lista para producción con configuración real y cuándo no debería molestarse.

Imagen de portada: Docker para PHP en producción: una guía práctica de configuración para aplicaciones empresariales

En resumen

  • Docker vale la pena cuando desea entornos idénticos, reversiones rápidas y una incorporación sencilla. Para un único sitio de folleto pequeño sobre alojamiento compartido, normalmente no es necesario.
  • Cree una imagen inmutable en CI: compilación en varias etapas, dependencias de producción únicamente, usuario no root, OPcache activado, errores registrados y nunca mostrados.
  • Libere por etiqueta de imagen (el hash de confirmación de Git), verifique el estado después del inicio y vuelva automáticamente a la etiqueta anterior si la verificación falla.
  • Los contenedores son desechables, los datos no: mantenga la base de datos y las cargas en volúmenes o servicios administrados y haga una copia de seguridad fuera de Docker.
  • Los errores de producción más comunes: secretos en la imagen, puertos de bases de datos publicados, logs ilimitados y límites de php-fpm que no coinciden con la memoria que tienes.

Para muchas aplicaciones empresariales, el coste real del hosting no es el servidor sino las personas. Una configuración que sólo entiende un desarrollador, una implementación realizada manualmente a través de FTP y un entorno de prueba que se comporta de manera diferente al de producción son todos costosos a largo plazo. Los contenedores abordan esto empaquetando la aplicación con su versión y extensiones exactas PHP. Ejecutamos nuestros propios sitios PHP y sistemas cliente en una pila basada en Docker, y esta guía describe la base desde la que partimos.

Cuándo Docker vale la pena y cuándo no

  • Vale la pena: varios servicios (web, PHP, base de datos, cola), más de un desarrollador, la necesidad de lanzamientos repetibles y reversiones rápidas, o un plan para moverse entre proveedores.
  • No es necesario: un sitio de contenido simple que rara vez cambia en hosting administrado. La complejidad añadida necesita una recompensa.
  • No reemplaza las buenas operaciones: aún necesita parches, copias de seguridad y monitoreo. Consulte nuestra guía de respaldo y monitoreo.

La arquitectura objetivo

Una configuración sólida y sencilla para una aplicación PHP en un servidor:

  • Proxy inverso y TLS en el borde ( Nginx o Caddy en el host, o una CDN como Cloudflare, consulte nuestra guía de configuración Cloudflare).
  • Contenedor web ( Nginx) que sirve archivos estáticos y pasa solicitudes PHP.
  • Contenedor PHP-FPM con su código, integrado en la imagen.
  • Base de datos en su propio contenedor con un volumen persistente, o mejor un servicio de base de datos administrado para datos críticos.
  • Volúmenes solo para datos que deben sobrevivir: archivos de bases de datos y cargas de usuarios.

Un Dockerfile de varias etapas

Una compilación de varias etapas instala dependencias en una etapa y copia solo el resultado en una imagen de tiempo de ejecución delgada. La imagen final no tiene Composer, compiladores ni herramientas de prueba, lo que la hace más pequeña y les da menos uso a los atacantes:

# ---- stage 1: PHP dependencies ----
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction --optimize-autoloader

# ---- stage 2: runtime ----
FROM php:8.3-fpm-alpine

RUN apk add --no-cache icu-libs libzip \
 && apk add --no-cache --virtual .build icu-dev libzip-dev $PHPIZE_DEPS \
 && docker-php-ext-install -j"$(nproc)" pdo_mysql intl zip opcache \
 && apk del .build

COPY docker/php.ini  /usr/local/etc/php/conf.d/zz-app.ini
COPY docker/www.conf /usr/local/etc/php-fpm.d/zz-www.conf

WORKDIR /var/www/app
COPY --from=vendor /app/vendor ./vendor
COPY --chown=www-data:www-data . .

USER www-data
EXPOSE 9000

Elija la versión PHP deliberadamente y verifique que todavía sea compatible. Nuestra guía de actualización PHP 8 explica cómo planificarlo. Fije versiones de imágenes base y actualícelas periódicamente, ya que una imagen que nunca reconstruye acumula vulnerabilidades conocidas. Mantenga un .dockerignore que excluya .git, .env, pruebas y datos locales, para que nada confidencial termine en la imagen.

Configuración PHP de producción y php-fpm

# docker/php.ini
expose_php = 0
display_errors = 0
log_errors = 1
memory_limit = 256M
upload_max_filesize = 10M
post_max_size = 12M

opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0   ; the code in an image never changes while it runs

# docker/www.conf
[www]
pm = dynamic
pm.max_children = 12
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
request_terminate_timeout = 30s
catch_workers_output = yes
decorate_workers_output = no

Configurar opcache.validate_timestamps = 0 proporciona el mejor rendimiento para una imagen inmutable, pero solo funciona si nunca editas archivos dentro de un contenedor en ejecución: en su lugar, publicas una nueva imagen. Dimensione el grupo a partir de números reales: mida la memoria promedio de un trabajador PHP bajo carga, decida cuánta memoria PHP puede usar en el servidor y divida. Si un trabajador usa 60 MB y usted puede darle a PHP 1 GB, pm.max_children tiene aproximadamente 16. Configurarlo mucho más alto solo hace que el servidor intercambie o que el kernel elimine los procesos durante un pico de tráfico. Demasiado bajo y cola de solicitudes. Consulte el registro de php-fpm para ver las advertencias de "max_children alcanzado".

Un archivo de redacción con los valores predeterminados correctos

x-logging: &default-logging
  driver: json-file
  options: { max-size: "10m", max-file: "5" }     # logs cannot fill the disk

services:
  web:
    image: ghcr.io/your-org/app-web:${TAG}
    ports: ["127.0.0.1:8080:80"]                  # only the local reverse proxy can reach it
    depends_on: [php]
    restart: unless-stopped
    logging: *default-logging
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1/health.php"]
      interval: 15s
      timeout: 3s
      retries: 3

  php:
    image: ghcr.io/your-org/app:${TAG}
    env_file: .env                                 # secrets stay on the server, not in the image
    restart: unless-stopped
    read_only: true
    tmpfs: [/tmp]
    mem_limit: 512m
    volumes: [uploads:/var/www/app/storage/uploads]
    logging: *default-logging

  db:
    image: mysql:8.4
    env_file: .env.db
    restart: unless-stopped
    volumes: [db_data:/var/lib/mysql]              # no "ports:" - the database is not public
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
      interval: 15s
      retries: 5
    logging: *default-logging

volumes:
  uploads:
  db_data:

La imagen web es una pequeña imagen Nginx con su carpeta public/ y la configuración del sitio copiadas, construida en el mismo proceso que la imagen PHP. Puntos clave: vincular los puertos publicados a 127.0.0.1, nunca publicar la base de datos, establecer políticas de reinicio, limitar la memoria para que un contenedor no pueda desactivar el host y limitar el tamaño del registro. Recuerde que los puertos publicados por Docker pueden eludir las reglas del firewall del host, como explicamos en la guía Cloudflare. Nunca monte el socket Docker en un contenedor de aplicaciones, ya que le da a ese contenedor control sobre el host.

Lanzamientos y reversiones

La mejor característica de una configuración basada en imágenes es que cada lanzamiento es una etiqueta y una reversión es "iniciar la etiqueta anterior". Etiquete imágenes con el hash de confirmación de Git, nunca confíe en latest y haga que el script de lanzamiento verifique su estado:

#!/usr/bin/env bash
# /srv/app/release.sh <git-sha>
set -euo pipefail
TAG="${1:?usage: release.sh <git-sha>}"
cd /srv/app
PREV="$(cat .current_tag 2>/dev/null || true)"

export TAG
docker compose pull php web
docker compose up -d --no-deps php web

for i in $(seq 1 30); do
  if curl -fsS http://127.0.0.1:8080/health.php > /dev/null; then
    echo "$TAG" > .current_tag
    echo "healthy: $TAG"; exit 0
  fi
  sleep 2
done

echo "unhealthy after release, rolling back to ${PREV:-none}" >&2
if [ -n "$PREV" ]; then TAG="$PREV" docker compose up -d --no-deps php web; fi
exit 1

Los cambios en la base de datos necesitan cuidado: escriba las migraciones para que la versión anterior del código aún funcione con el nuevo esquema (agregue columnas primero, elimínelas en una versión posterior). Eso es lo que hace que una reversión rápida sea segura. Realice una copia de seguridad de la base de datos antes de cualquier lanzamiento que cambie el esquema.

Construcción y envío desde CI

Cree la imagen una vez en CI e implemente exactamente ese artefacto. Un flujo de trabajo mínimo GitHub Actions:

name: build-and-release
on:
  push:
    branches: [main]
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/your-org/app:${{ github.sha }}
      - name: Release over SSH
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: |
          install -m 600 /dev/null key && echo "$DEPLOY_KEY" > key
          ssh -i key -o StrictHostKeyChecking=accept-new deploy@your-server "/srv/app/release.sh ${{ github.sha }}"

Ejecute sus pruebas y análisis estáticos en la misma canalización antes de crear la imagen y use una clave de implementación que solo pueda ejecutar el script de lanzamiento. Agregue un análisis de vulnerabilidad de imágenes si su perfil de riesgo lo exige.

Registros, datos y seguridad.

  • Los registros van a stdout y stderr, Docker los rota y, para cualquier cosa importante, se envían a un lugar central para que sobrevivan al contenedor.
  • Los datos viven fuera del contenedor. Un volumen no es una copia de seguridad. Utilice el enfoque de nuestra guía de copia de seguridad: cifrada, externa, con restauración probada.
  • Los secretos nunca se plasman en imágenes. Páselos en tiempo de ejecución a través de archivos env o un administrador de secretos y manténgalos fuera de Git.
  • Ejecútelo como usuario no root, utilice un sistema de archivos de solo lectura cuando sea posible y elimine lo que no necesite.
  • Mantenga las imágenes frescas. Reconstruya periódicamente para detectar correcciones de seguridad en la imagen base y PHP.
  • Aplique los controles a nivel de aplicación de nuestra lista de verificación de seguridad PHP, ya que los contenedores no reparan el código inseguro.

Una lista de verificación de preparación para la producción

  1. Imagen de varias etapas solo con dependencias de producción, imagen base fijada, usuario no root.
  2. OPcache habilitado, los errores registrados no se muestran, tamaño php-fpm de la memoria medida.
  3. Redacte archivos con políticas de reinicio, comprobaciones de estado, límites de memoria, rotación de registros y sin puerto de base de datos pública.
  4. Lanzamientos mediante etiqueta commit-hash con verificación de estado y reversión automáticas.
  5. Copias de seguridad y restauración probada para la base de datos y las cargas.
  6. Monitoreo y alertas sobre salud, disco, errores y certificados.

¿Cómo podemos ayudar?

Colocamos en contenedores aplicaciones PHP, configuramos canales y lanzamientos de CI y los conectamos a pagos, CRM y mensajería. Vea nuestro servicio de integraciones o reserve una llamada y revisaremos su proceso actual de alojamiento y lanzamiento.

Preguntas frecuentes

¿ Docker es bueno para ejecutar PHP en producción?

Sí, cuando se usa bien. Ofrece entornos idénticos, lanzamientos repetibles y reversiones rápidas. Necesita una configuración adecuada: imágenes inmutables, controles de estado, límites de recursos, rotación de registros, gestión de secretos y copias de seguridad de datos fuera del contenedor.

¿Debería la base de datos ejecutarse en Docker en producción?

Puede serlo para sistemas pequeños y medianos si utiliza un volumen persistente, nunca se expone públicamente y tiene copias de seguridad probadas. Para datos críticos, muchos equipos prefieren un servicio de base de datos administrada, que maneja la conmutación por error, las actualizaciones y las copias de seguridad por usted.

¿Cómo puedo revertir una versión Docker ?

Etiqueta cada imagen con el hash de confirmación de Git y conserva la etiqueta anterior. Para retroceder, inicie los contenedores con la etiqueta anterior. Escriba migraciones de bases de datos para que el código anterior siga funcionando con el esquema más nuevo.

¿Cuántos trabajadores php-fpm debo configurar?

Mida la memoria promedio de un trabajador bajo carga, decida cuánta memoria PHP puede usar y divida. Establecer un valor demasiado alto provoca intercambios, mientras que un valor demasiado bajo provoca solicitudes en cola. Mire el registro de php-fpm para ver las advertencias de max_children.

¿Cuál es el mayor error de seguridad Docker con las aplicaciones PHP ?

Poner secretos en la imagen o Git y publicar puertos de bases de datos en Internet. También evite ejecutar como root, montar el socket Docker en contenedores y nunca actualizar la imagen base.

Llamada de descubrimiento gratuita

Traiga una pila desordenada o una integración estancada - Integrono trazará el primer paso más inteligente.

Obtiene una visión senior sobre trampas de presupuesto, dependencias y si conviene comprar, ensamblar o construir - antes de que empiece el trabajo de pago. El CEO sigue al tanto.