Laravel en la raíz + React (Vite) en frontend/ consumiendo un endpoint de Laravel
Laravel en la raíz + React (Vite) en frontend/ consumiendo un endpoint de Laravel
Nota: este tutorial omite la instalación de Docker y Docker Compose. Se asume que ya los tienes instalados. Verifica con:
docker --version docker compose version
Estructura objetivo:
Laravel vive directamente en la raíz
~/laravel-docker/.React + Vite vive en
~/laravel-docker/frontend/.
Estructura inicial del proyecto
Crea la carpeta raíz y las subcarpetas necesarias:
mkdir -p ~/laravel-docker
cd ~/laravel-docker
mkdir -p docker-compose/nginxEstructura inicial en el host:
~/laravel-docker/
├── docker-compose/
│ └── nginx/
│ └── default.conf (lo crearemos)
├── Dockerfile (lo crearemos)
└── docker-compose.yml (lo crearemos)Laravel se creará directamente en
~/laravel-docker/(raíz). React vivirá en~/laravel-docker/frontend/.
1. Crear el Dockerfile (PHP-FPM + Composer)
Dentro de ~/laravel-docker/:
nano DockerfileContenido:
# Imagen base: PHP 8.3 con FPM
FROM php:8.3-fpm
# Argumentos para el usuario (se pasan desde docker-compose)
ARG user=laravel
ARG uid=1000
# Dependencias del sistema
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
zip \
unzip \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Extensiones PHP necesarias para Laravel
RUN docker-php-ext-install \
pdo_mysql \
mbstring \
exif \
pcntl \
bcmath \
gd
# Composer global
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Usuario no root
RUN useradd -G www-data,root -u ${uid} -d /home/${user} ${user} \
&& mkdir -p /home/${user}/.composer \
&& chown -R ${user}:${user} /home/${user}
# Directorio de trabajo
WORKDIR /var/www
# Cambiar al usuario creado
USER ${user}
# Comando por defecto
CMD ["php-fpm"]2. Configurar Nginx
nano docker-compose/nginx/default.confContenido:
server {
listen 80;
server_name localhost;
root /var/www/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}Puntos clave:
root /var/www/public;→ Laravel montado en/var/www.fastcgi_pass app:9000;→ nombre del servicio PHP-FPM endocker-compose.yml.
3. Crear docker-compose.yml (solo backend por ahora)
nano docker-compose.ymlContenido:
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
user: ${USER:-laravel}
uid: ${UID:-1000}
image: laravel-app
container_name: laravel-app-1
restart: unless-stopped
working_dir: /var/www
volumes:
- ./:/var/www
networks:
- laravel
nginx:
image: nginx:alpine
container_name: laravel-nginx-1
restart: unless-stopped
ports:
- "8000:80"
volumes:
- ./:/var/www
- ./docker-compose/nginx:/etc/nginx/conf.d
networks:
- laravel
depends_on:
- app
db:
image: mysql:8
container_name: laravel-db-1
restart: unless-stopped
environment:
MYSQL_DATABASE: ${DB_DATABASE:-laravel_db}
MYSQL_USER: ${DB_USERNAME:-laravel_user}
MYSQL_PASSWORD: ${DB_PASSWORD:-secret}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-secret}
volumes:
- db_data:/var/lib/mysql
networks:
- laravel
networks:
laravel:
driver: bridge
volumes:
db_data:Laravel se monta desde
./en/var/www. Todo lo que crees en la raíz será visible dentro del contenedor.
4. Levantar backend y crear Laravel en la raíz
Levanta solo los servicios del backend:
docker compose up -d app nginx dbVerifica que los contenedores estén corriendo:
docker psDeberías ver laravel-app-1, laravel-nginx-1 y laravel-db-1.
Crear el proyecto Laravel
Entra al contenedor:
docker exec -it laravel-app-1 bashDentro, crea Laravel en una carpeta temporal y mueve todo a la raíz (/var/www), que está sincronizada con ~/laravel-docker/:
cd /var/www
composer create-project laravel/laravel application --prefer-dist
cp -RT application/ . && rm -rf application
php artisan key:generateSal del contenedor:
exitAhora en tu host verás la estructura de Laravel directamente en la raíz ~/laravel-docker/:
~/laravel-docker/
├── app/
├── bootstrap/
├── config/
├── database/
├── public/
├── resources/
├── routes/
├── storage/
├── tests/
├── vendor/
├── .env
├── composer.json
├── Dockerfile
├── docker-compose.yml
└── docker-compose/5. Ajustar el .env de Laravel
Edita ~/laravel-docker/.env:
nano .envAjusta las variables de base de datos y URL:
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:... # ya generada por artisan
APP_DEBUG=true
APP_URL=http://localhost:8000
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel_db
DB_USERNAME=laravel_user
DB_PASSWORD=secret
FRONTEND_URL=http://localhost:5173
DB_HOST=dbes el nombre del servicio MySQL en la red interna de Docker. No useslocalhostaquí: dentro del contenedorapp,localhostsería él mismo.
Reinicia el contenedor de la app:
docker compose restart app6. Crear un endpoint API en Laravel
Edita routes/api.php:
nano routes/api.phpReemplaza su contenido por:
<?php
use Illuminate\Support\Facades\Route;
Route::get('/hello', function () {
return response()->json([
'message' => '¡Hola desde Laravel API!',
'timestamp' => now()->toIso8601String(),
'app' => config('app.name'),
]);
});Laravel 11: si
routes/api.phpno existe, créalo y regístralo enbootstrap/app.php:->withRouting( web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', )Laravel 10: ya viene incluido y registrado automáticamente.
Prueba el endpoint:
curl http://localhost:8000/api/helloDeberías recibir algo como:
{"message":"¡Hola desde Laravel API!","timestamp":"2025-...","app":"Laravel"}CORS
Laravel 11 maneja CORS automáticamente con allowed_origins: ['*'] para rutas api/*. Si usas Laravel 10 o quieres restringirlo, edita config/cors.php:
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_origins' => ['http://localhost:5173'],
'allowed_methods' => ['*'],
'allowed_headers' => ['*'],7. Crear el proyecto React + Vite en frontend/
Desde la raíz ~/laravel-docker/, crea el proyecto usando un contenedor Node temporal:
docker run --rm -v "$(pwd)/frontend:/app" -w /app node:20-alpine \
sh -c "npm create vite@latest . -- --template react"Esto creará la carpeta frontend/ con un proyecto Vite + React. La estructura queda así:
~/laravel-docker/
├── app/ (Laravel)
├── bootstrap/ (Laravel)
├── config/ (Laravel)
├── database/ (Laravel)
├── public/ (Laravel)
├── resources/ (Laravel)
├── routes/ (Laravel)
├── storage/ (Laravel)
├── tests/ (Laravel)
├── vendor/ (Laravel)
├── frontend/ (React + Vite) ← nueva
│ ├── src/
│ ├── index.html
│ ├── package.json
│ └── vite.config.js
├── .env
├── composer.json
├── Dockerfile
├── docker-compose.yml
└── docker-compose/8. Configurar Vite para Docker
Edita frontend/vite.config.js:
nano frontend/vite.config.jsContenido:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
watch: {
usePolling: true, // Necesario en Docker/WSL para detectar cambios
},
hmr: {
clientPort: 5173, // Para que el HMR use el puerto correcto del host
},
},
})Crea frontend/.env:
nano frontend/.envVITE_API_URL=http://localhost:8000/apiReact corre en el navegador del host, por lo que la URL de la API debe ser
http://localhost:8000. Si hicieras peticiones desde el propio contenedor (SSR), usaríashttp://nginx/api.
No, no es obligatorio detener y eliminar todos los contenedores existentes para agregar el de React. Docker Compose está diseñado para detectar cambios y actuar solo sobre lo necesario
Qué pasa si solo agregas el servicio frontend a docker-compose.yml
Si no tocas la definición de app, nginx ni db, puedes simplemente:
docker compose up -d
Compose:
Creará y levantará el nuevo contenedor
react-frontend-1.Dejará los contenedores existentes (
laravel-app-1,laravel-nginx-1,laravel-db-1) tal como están, sin detenerlos ni eliminarlos.
Si modificaste algo de los servicios existentes
Por ejemplo, si cambiaste depends_on, redes, volúmenes o variables de entorno de app, nginx o db, Compose recreará automáticamente solo esos contenedores que hayan cambiado. No necesitas hacerlo a mano.
Si quieres forzar una reconstrucción completa
Puedes hacer:
docker compose down docker compose up -d --build
Pero cuidado: docker compose down elimina los contenedores y la red, pero conserva los volúmenes nombrados (como db_data). Si usas down -v, borrarás también los datos de MySQL. Para desarrollo está bien, pero si quieres conservar la base de datos, no uses -v.
9. Agregar el servicio frontend a docker-compose.yml
Edita docker-compose.yml y añade el servicio frontend. El archivo completo queda así:
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
user: ${USER:-laravel}
uid: ${UID:-1000}
image: laravel-app
container_name: laravel-app-1
restart: unless-stopped
working_dir: /var/www
volumes:
- ./:/var/www
networks:
- laravel
nginx:
image: nginx:alpine
container_name: laravel-nginx-1
restart: unless-stopped
ports:
- "8000:80"
volumes:
- ./:/var/www
- ./docker-compose/nginx:/etc/nginx/conf.d
networks:
- laravel
depends_on:
- app
db:
image: mysql:8
container_name: laravel-db-1
restart: unless-stopped
environment:
MYSQL_DATABASE: ${DB_DATABASE:-laravel_db}
MYSQL_USER: ${DB_USERNAME:-laravel_user}
MYSQL_PASSWORD: ${DB_PASSWORD:-secret}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-secret}
volumes:
- db_data:/var/lib/mysql
networks:
- laravel
frontend:
image: node:20-alpine
container_name: react-frontend-1
restart: unless-stopped
working_dir: /app
ports:
- "5173:5173"
volumes:
- ./frontend:/app
- frontend_node_modules:/app/node_modules
command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
networks:
- laravel
depends_on:
- nginx
networks:
laravel:
driver: bridge
volumes:
db_data:
frontend_node_modules:Puntos clave:
frontendmonta solo./frontenden/app. Laravel (raíz) y React (frontend/) quedan completamente separados en el host.El volumen
frontend_node_modulesevita que losnode_modulesdel contenedor choquen con los del host.--host 0.0.0.0es obligatorio para que Vite sea accesible desde fuera del contenedor.
10. Levantar React y verificar todo
docker compose up -dVerifica los 4 contenedores:
docker psDeberías ver:
laravel-app-1 (PHP-FPM)
laravel-nginx-1 (Nginx → Laravel)
laravel-db-1 (MySQL)
react-frontend-1 (React + Vite)Abre en el navegador:
Laravel API: http://localhost:8000/api/hello
React + Vite: http://localhost:5173
11. Consumir el endpoint desde React
Edita frontend/src/App.jsx:
nano frontend/src/App.jsxReemplaza su contenido por:
import { useEffect, useState } from 'react'
import './App.css'
function App() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch(`${import.meta.env.VITE_API_URL}/hello`)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
})
.then((json) => {
setData(json)
setLoading(false)
})
.catch((err) => {
setError(err.message)
setLoading(false)
})
}, [])
return (
<div style={{ fontFamily: 'sans-serif', padding: '2rem' }}>
<h1>React + Vite + Laravel API</h1>
{loading && <p>Cargando datos desde Laravel...</p>}
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
{data && (
<div style={{ background: '#f4f4f4', padding: '1rem', borderRadius: 8 }}>
<p><strong>Mensaje:</strong> {data.message}</p>
<p><strong>App:</strong> {data.app}</p>
<p><strong>Timestamp:</strong> {data.timestamp}</p>
</div>
)}
</div>
)
}
export default AppGuarda y abre http://localhost:5173. Deberías ver el mensaje "¡Hola desde Laravel API!" renderizado en tu aplicación React.
Comandos útiles
| Acción | Comando |
|---|---|
| Levantar todo | docker compose up -d |
| Detener todo | docker compose down |
| Detener y borrar volúmenes | docker compose down -v |
| Ver logs de un servicio | docker compose logs -f frontend |
| Entrar al contenedor de Laravel | docker exec -it laravel-app-1 bash |
| Entrar al contenedor de React | docker exec -it react-frontend-1 sh |
| Ejecutar artisan | docker exec -it laravel-app-1 php artisan migrate |
| Ejecutar npm | docker exec -it react-frontend-1 npm install <pkg> |
| Reconstruir solo la app | docker compose build app |
Solución de problemas comunes
La descarga de imágenes se queda pegada
Presiona Ctrl + C y vuelve a ejecutar docker compose up -d. Las capas ya descargadas se reutilizan.
Error de conexión a la base de datos
Verifica que DB_HOST=db en .env y que las credenciales coincidan con las del servicio db en docker-compose.yml. Confirma con docker ps | grep db.
React no recarga al editar archivos
Asegúrate de que frontend/vite.config.js tenga usePolling: true y host: '0.0.0.0'. En WSL2 a veces es imprescindible.
El puerto 5173 o 8000 ya está ocupado
Cambia el mapeo en docker-compose.yml (por ejemplo "5174:5173" o "8001:80") y actualiza VITE_API_URL / APP_URL en consecuencia.
Error CORS al llamar a la API desde React
Revisa config/cors.php (o el manejo por defecto de Laravel 11). Asegúrate de que allowed_origins incluya http://localhost:5173 o usa * en desarrollo.
node_modules del host choca con los del contenedor
Usamos el volumen nombrado frontend_node_modules precisamente para evitarlo. No borres ese volumen ni montes ./frontend/node_modules desde el host.
Composer no se encuentra dentro del contenedor
Verifica que el Dockerfile tenga la línea COPY --from=composer:latest /usr/bin/composer /usr/bin/composer y reconstruye con docker compose build app.
Conclusión
Has configurado un entorno de desarrollo completo con Docker Compose donde:
Laravel vive en la raíz
~/laravel-docker/(tal como en el tutorial original).React + Vite vive en
~/laravel-docker/frontend/, completamente separado del backend.PHP-FPM 8.3 con todas las extensiones necesarias para Laravel.
Composer para gestión de dependencias PHP.
Nginx sirviendo Laravel en el puerto
8000.MySQL 8 con volumen persistente.
React + Vite servido en el puerto
5173con HMR y bind mount.Comunicación cross-container entre React (navegador) y la API Laravel mediante
VITE_API_URL.CORS configurado para desarrollo.
Ahora puedes editar tanto el backend como el frontend desde tu editor local y ver los cambios reflejados automáticamente en los contenedores, todo en un entorno aislado, reproducible y listo para producción con pequeños ajustes
Comentarios
Publicar un comentario