docker Dockerfile multi-stage para Laravel en producción: la guía completa 2026 OmarDevSpeed

09 Sep 2026 · 8 min · DOCKER

Dockerfile multi-stage para Laravel en producción: la guía completa 2026

Cómo construir imágenes Docker pequeñas, seguras y listas para producción con PHP-FPM, Nginx y multi-stage builds.

BLUF: Un Dockerfile multi-stage para Laravel separa dependencias Composer, build de assets con Node/Vite y la imagen final de runtime. El resultado es una imagen de producción más pequeña, sin toolchain de desarrollo y con superficie de ataque reducida. En equipos LATAM que despliegan a AWS ECS, Kubernetes o VPS, esta técnica suele bajar el tamaño de imagen entre 40% y 70% frente a un Dockerfile monolítico.

¿Qué es un Dockerfile multi-stage y por qué lo necesitas?

Un Dockerfile multi-stage declara varias instrucciones FROM en el mismo archivo. Cada FROM inicia un stage con su propio sistema de archivos. Solo copias al stage final lo que realmente necesita correr la app: código PHP, vendor de producción, assets compilados y la configuración mínima de PHP-FPM u Octane.

En Laravel el problema clásico es mezclar en una sola imagen: PHP, Composer, Node, npm, cachés de build y herramientas de debug. Eso infla gigabytes, alarga el pull en deploy y deja binarios innecesarios en producción. El multi-stage resuelve exactamente eso: usas imágenes pesadas solo para construir, y una imagen slim para ejecutar.

Si hoy tu pipeline hace composer install y npm run build dentro del mismo contenedor que sirve tráfico, estás pagando en disco, tiempo de arranque y riesgo de seguridad. El patrón multi-stage es el estándar de facto en 2026 para backends Laravel serios.

Ventajas del build multi-stage en producción

  1. Tamaño: dejas fuera node_modules, caches de Composer y compiladores.
  2. Seguridad: menos paquetes = menos CVEs potenciales en la superficie runtime.
  3. Velocidad de deploy: pulls más cortos en ECS/EKS y en VPS con poco ancho de banda.
  4. Separación de concerns: el stage de Composer no necesita Node; el de Vite no necesita PHP.
  5. Reproducibilidad: cada stage puede pinearse a un digest concreto (php:8.3-fpm-bookworm, node:20-alpine).
  6. Caché de capas: Docker reutiliza layers de composer.lock o package-lock.json si no cambian.

En MZZO y en proyectos telco SVA hemos visto imágenes pasar de ~1.2 GB a ~380–450 MB solo con multi-stage y composer install --no-dev. Eso importa cuando orquestas decenas de réplicas.

Estructura recomendada paso a paso (con bloques de código reales)

La estructura que recomiendo para Laravel 11 + Vite es de tres stages. Puedes añadir un cuarto para tests en CI, pero en producción bastan tres.

Stage 1 — Dependencias de composer

FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
    --no-dev \
    --no-interaction \
    --no-scripts \
    --prefer-dist \
    --ignore-platform-reqs
COPY . .
RUN composer dump-autoload --optimize --no-dev

Aquí el objetivo es materializar vendor/ sin scripts que asuman .env completo. Los scripts (package:discover) se ejecutan después, en el entrypoint o en un stage con PHP real.

Stage 2 — Assets con Node/Vite

FROM node:20-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY vite.config.js tailwind.config.js postcss.config.js ./
COPY resources ./resources
COPY public ./public
RUN npm run build

Este stage produce public/build (manifest + JS/CSS). No debe quedar Node en la imagen final.

Stage 3 — Imagen final de producción

FROM php:8.3-fpm-bookworm AS app
RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq-dev libzip-dev unzip curl \
 && docker-php-ext-install pdo_mysql zip opcache \
 && rm -rf /var/lib/apt/lists/*
WORKDIR /var/www/html
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
COPY . .
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
RUN chown -R www-data:www-data storage bootstrap/cache \
 && chmod -R ug+rwx storage bootstrap/cache
USER www-data
CMD ["php-fpm"]

En front pones nginx (o Caddy) como reverse proxy hacia PHP-FPM. Si usas Octane, cambia el CMD a php artisan octane:start.

Variables de entorno y secretos en Docker

Nunca copies .env con secretos al contexto de build ni lo dejes en la imagen. Inyecta variables en runtime:

  • APP_KEY, DB_PASSWORD, REDIS_PASSWORD vía secrets de Docker/Kubernetes o Parameter Store/SSM.
  • Usa env_file solo en compose local; en producción preferimos secret mounts.
  • php artisan config:cache en el entrypoint después de tener env reales.
  • No hagas ARG de passwords: quedan en historial de capas.

Ejemplo de entrypoint seguro:

#!/bin/sh
set -e
php artisan config:cache
php artisan route:cache
php artisan view:cache
exec php-fpm -F

Errores comunes y cómo evitarlos

  • Copiar vendor del host: genera incompatibilidad de extensiones. Siempre genera vendor en stage Composer.
  • Olvidar public/build: la app carga sin CSS/JS. Copia explícitamente desde el stage Node.
  • Correr como root: define USER www-data y permisos correctos en storage/.
  • --no-scripts sin dump-autoload: falutan providers. Haz dump-autoload o corre package:discover en entrypoint.
  • Una sola capa gigante: ordena COPY de lockfiles primero para aprovechar caché.
  • Healthchecks mal hechos: verifica /up (Laravel 11) o un endpoint real, no solo el proceso PHP.

Preguntas frecuentes sobre Docker multi-stage con Laravel

¿Cuánto reduce el tamaño de imagen el multi-stage?

En proyectos Laravel reales con Vite, el multi-stage suele reducir entre 40% y 70% el tamaño frente a una imagen que incluye Node + Composer + PHP. De ~1 GB a ~350–500 MB es un rango típico si además usas composer install --no-dev y una base php-fpm slim/bookworm.

¿Funciona con Laravel Octane?

Sí. El multi-stage es independiente del runtime. En el stage final instalas las extensiones que Octane/Swoole/RoadRunner necesitan y cambias el comando de arranque. Lo importante: sigue sin meter Node ni Composer en la imagen que escucha tráfico.

¿Cómo debuggear un contenedor de producción?

No abras SSH permanente. Usa docker exec puntual, logs centralizados (CloudWatch, Loki) y un stage o tag debug aparte con herramientas extra. En producción preferimos APP_DEBUG=false, opcache.validate_timestamps=0 y traces vía OpenTelemetry.

Sobre el autor

Omar Curvelo es Subgerente de TI en MZZO (Chile) y creador de OmarDevSpeed. Trabaja en integraciones SVA, Laravel, Docker y AWS para equipos en LATAM. Conéctate en LinkedIn.

Sigue leyendo

Checklist operativo antes de promover la imagen

Antes de etiquetar prod, valida: healthcheck verde, php artisan about sin secretos en output, permisos storage y bootstrap/cache, que el manifest de Vite exista, y que las migraciones corran en un job de release separado del rolling update. Documenta el digest de la imagen en el ticket de cambio. Si usas ECR, habilita scan on push y bloquea critical CVEs conocidos en la base PHP. Mantén un tag inmutable por commit SHA además de latest. En rollbacks, vuelve al digest anterior; no reconstruyas "a ciegas" desde main si el incidente es de datos. Separar build y migrate evita que un fail de schema tumbe todos los pods al mismo tiempo. Este procedimiento lo aplicamos en despliegues Laravel detrás de nginx y también aplica si el front es Octane.

En redes corporativas chilenas el pull desde Docker Hub a veces es inestable: preferimos mirror o cache pull-through. Fija versiones menores (8.3.x) y revisa changelogs de extensiones zip y pcntl cuando subas minor de PHP. El multi-stage no reemplaza pruebas: corre la suite en CI usando el mismo Dockerfile con target intermedio si necesitas toolchain.