Cómo montar Laravel en Docker: Guía práctica y comandos esenciales
Aprende a dockerizar tu aplicación Laravel paso a paso, solucionar problemas comunes de conexión y gestionar tus contenedores
Cómo montar Laravel en Docker
Si alguna vez has intentado mover un proyecto Laravel de tu entorno local a producción y te has encontrado con el clásico problema de "en mi máquina sí funciona", Docker es la solución que estabas buscando.
En este artículo, te voy a explicar cómo estructurar tu proyecto Laravel con Docker de forma profesional, cómo resolver los problemas más comunes (como el temido Connection refused) y te dejaré una lista interactiva (Cheat Sheet) de los comandos indispensables para tu día a día.
Estructura del Proyecto
Antes de entrar en detalle, así es como se organiza nuestro proyecto:
proyecto/
├── .dockerignore ← Excluye archivos innecesarios del build
├── .env ← Única fuente de credenciales (¡NO commitear!)
├── .env.example ← Plantilla documentada (SÍ commitear)
├── docker-compose.yml ← Orquestador de todos los servicios
├── dockerfiles/
│ ├── entrypoint.sh ← Script de arranque automático
│ ├── nginx.dockerfile
│ └── php.dockerfile
├── nginx/
│ └── default.conf ← Configuración del servidor web
└── src/
└── ... ← Tu proyecto Laravel
** Detalle importante:** El fichero
.dockerignoreevita que carpetas comovendor/,node_modules/o archivos.envse envíen al contexto de build de Docker, acelerando la construcción y evitando filtrar información sensible.
La Arquitectura: ¿Qué necesitamos?
Para ejecutar Laravel en Docker de forma robusta, dividimos nuestra aplicación en 4 contenedores (servicios) principales:
Servidor Web (Nginx): El encargado de recibir las peticiones HTTP y pasarlas a PHP. Configurado con cabeceras de seguridad y
server_tokens off.Aplicación (PHP-FPM): Donde reside el código fuente de Laravel y se ejecuta la lógica. Corre con un usuario no-root (
laravel).Base de Datos (MySQL): El almacenamiento persistente con un healthcheck que garantiza que esté lista antes de que PHP arranque.
Gestor de BD (phpMyAdmin) [Solo desarrollo]: Administración visual de la base de datos, activada solo con el perfil
dev.
Todo esto lo orquestamos a través de un archivo maestro llamado docker-compose.yml.
Variables de Entorno: Una Única Fuente de Verdad
Una de las claves de seguridad más importantes es nunca hardcodear credenciales directamente en los ficheros de Docker. En su lugar, usamos un fichero .env centralizado en la raíz del proyecto.
1. Crea tu .env a partir de la plantilla
cp .env.example .env
2. Edita las credenciales en .env
# .env (raíz del proyecto — NO commitear)
APP_DEBUG=false
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=tu_contraseña_segura
MYSQL_ROOT_PASSWORD=otra_contraseña_segura
3. El docker-compose.yml las referencia con ${VARIABLE}
services:
php:
build:
context: .
dockerfile: dockerfiles/php.dockerfile
restart: unless-stopped
environment:
- DB_CONNECTION=mysql
- DB_HOST=mysql # Usamos el nombre del servicio Docker, ¡no 127.0.0.1!
- DB_PORT=3306
- DB_DATABASE=${DB_DATABASE:-laravel}
- DB_USERNAME=${DB_USERNAME:-laravel}
- DB_PASSWORD=${DB_PASSWORD}
- APP_DEBUG=${APP_DEBUG:-false}
depends_on:
mysql:
condition: service_healthy # Espera a que MySQL esté REALMENTE listo
** ¿Por qué no usamos
src/.env?** Docker inyecta estas variables de entorno en el contenedor, sobrescribiendo las del.envde Laravel. Así se separa la configuración del código y se facilitan los despliegues automatizados en herramientas como Coolify o AWS.
Healthchecks: Arranque Inteligente
Uno de los problemas más comunes es que PHP intente conectar a MySQL cuando la base de datos aún no está lista. Para evitarlo, configuramos un healthcheck en MySQL:
mysql:
image: mysql:8.0.32
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
Con depends_on: mysql: condition: service_healthy, Docker Compose esperará a que MySQL responda al ping antes de arrancar los contenedores de PHP y Nginx. ¡Adiós a los Connection refused en el arranque!
🛠️ Automatizando Migraciones y Seeders
El archivo entrypoint.sh se ejecuta cada vez que arranca el contenedor PHP. Automatiza las migraciones y ejecuta los seeders de forma inteligente:
#!/bin/sh
set -e
cd /var/www/html
echo "Database is ready. Running migrations..."
php artisan migrate --force
# Solo ejecutar seeders si existen ficheros Y no se han ejecutado antes
if [ ! -f /var/www/html/storage/.seeded ]; then
SEEDER_COUNT=$(find /var/www/html/database/seeders -name '*.php' 2>/dev/null | wc -l)
if [ "$SEEDER_COUNT" -gt 0 ]; then
echo "Running seeders ($SEEDER_COUNT found)..."
php artisan db:seed --force
touch /var/www/html/storage/.seeded
else
echo "No seeders found, skipping."
fi
else
echo "Seeders already executed, skipping."
fi
exec "$@"
¿Qué hace de especial este script?
set -e: Si cualquier comando falla, el contenedor se detiene inmediatamente (fail-fast).Seeders condicionales: Primero comprueba que existan ficheros
.phpendatabase/seeders. Si los hay y no se han ejecutado antes, se ejecutan y se crea un fichero marcador.seeded. En reinicios posteriores, se omite automáticamente.
** Importante:** Cualquier cambio en
entrypoint.sho en elphp.dockerfilerequiere que reconstruyas la imagen con el flag--build. ¡Si solo reinicias el contenedor usando restart, no se aplicarán los cambios!
Seguridad en Nginx
Nuestra configuración de Nginx incluye cabeceras de seguridad esenciales:
server {
listen 80;
server_name .localhost;
root /var/www/html/public;
# Ocultar versión de Nginx
server_tokens off;
# Cabeceras de seguridad
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Límites
client_max_body_size 10M;
fastcgi_read_timeout 60;
# Bloquear acceso a ficheros ocultos
location ~ /\.(?!well-known) {
deny all;
}
}
Estas cabeceras protegen contra ataques como clickjacking (X-Frame-Options), MIME-sniffing (X-Content-Type-Options) y ataques XSS (X-XSS-Protection).
phpMyAdmin: Solo en Desarrollo
phpMyAdmin se gestiona con un perfil de Docker Compose, lo que significa que solo se levanta cuando lo necesitas explícitamente:
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
profiles: ["dev"] # Solo se activa con --profile dev
depends_on:
mysql:
condition: service_healthy
ports:
- "127.0.0.1:8081:80"
En producción, phpMyAdmin no se despliega, eliminando una superficie de ataque innecesaria.
Cheat Sheet: Los Comandos que Usamos Todo el Tiempo
A continuación tienes una lista de los comandos más habituales al trabajar con contenedores, y los que te salvarán la vida en tu día a día (¡comprobado en este mismo blog!):
Comandos de Ciclo de Vida
Levantar tu proyecto o apagarlo:
docker compose --profile dev up -d: Levanta todos los contenedores incluyendo phpMyAdmin (modo desarrollo).docker compose up -d: Levanta los contenedores sin phpMyAdmin (modo producción).docker compose up -d --build: Levanta los contenedores forzando la reconstrucción de las imágenes. Úsalo si cambias unDockerfileo copias un nuevoentrypoint.sh.docker compose --profile dev up -d --build: Igual que el anterior, pero incluyendo phpMyAdmin.docker compose down: Detiene y destruye los contenedores y redes, pero conserva los volúmenes (tus bases de datos).docker compose down -v: Detiene y destruye todos los contenedores, redes y destruye los volúmenes (tus bases de datos).docker network inspect <nombre_red>: Útil si Docker te dice que una red sigue en uso y no te permite eliminar un entorno temporal.
Ejecutando código de Laravel dentro de Docker
Para usar php artisan y composer, no lo ejecutes en la terminal base de tu máquina (Windows/Mac), ya que intentará usar el PHP instalado localmente y probablemente fallará (por falta de dependencias u otras versiones de PHP). Ejecútalo dentro del contenedor:
docker compose exec php php artisan migrate: Ejecuta las migraciones manualmente.docker compose exec php php artisan db:seed: Ejecuta los seeders para poblar datos de prueba o roles base.docker compose exec php php artisan config:clear: Limpia la caché de configuración. ¡Obligatorio si tocas tu.envlocal!docker compose exec --user root php composer dump-autoload: Regenera el autoload de Composer como super-usuario. Útil si pasas código creado en Windows a Linux y hay errores de permisos ("Operation not permitted").
Debugging y Logs
Cuando algo falla, como los errores HTTP 500, o un host laravel.localhost que devuelve "Connection Refused", la consola de comandos es tu mejor aliado para rastrear el fallo:
docker compose logs php: Muestra lo que está pasando en el contenedor de PHP. Excelente para ver si el Entrypoint ejecutó las migraciones o si los seeders se saltaron correctamente.docker compose logs -f server: Sigue en tiempo real los logs de acceso (y errores502 Bad Gateway) del servidor Nginx.docker compose ps: Comprueba qué contenedores están encendidos en ese momento. Con healthchecks, verás el estadohealthyen MySQL.docker compose exec php ps aux: Verifica qué procesos internos (por ejemplo:php-fpm) se están ejecutando dentro del contenedor en ese preciso instante.
Reflexión Final
Dockerizar Laravel implica una curva de aprendizaje mínima al principio, pero una vez configuras tus volúmenes adecuadamente y entiendes cómo funciona su red interna, el flujo de trabajo es impecable.
Evitas conflictos de versiones (¡adiós a fallos por ejecutar Laravel 8 en PHP 8.3 local!), aislas tus bases de datos para no "manchar" tu sistema operativo y, lo mejor de todo, logras que el comportamiento local sea exactamente igual al del servidor de producción.
Con las mejoras de seguridad (variables externalizadas, cabeceras HTTP, perfiles para desarrollo) y fiabilidad (healthchecks, seeders condicionales), tu stack está preparado para un entorno profesional real.
¿Y tú, ya usas Docker en tus proyectos de Laravel o sigues usando XAMPP/Laragon? ¡Compárteme tu experiencia en los comentarios!