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:

bash
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:

bash
mkdir -p ~/laravel-docker
cd ~/laravel-docker
mkdir -p docker-compose/nginx

Estructura inicial en el host:

text
~/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/:

bash
nano Dockerfile

Contenido:

dockerfile
# 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

bash
nano docker-compose/nginx/default.conf

Contenido:

nginx
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 en docker-compose.yml.


3. Crear docker-compose.yml (solo backend por ahora)

bash
nano docker-compose.yml

Contenido:

yaml
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:

bash
docker compose up -d app nginx db

Verifica que los contenedores estén corriendo:

bash
docker ps

Deberías ver laravel-app-1, laravel-nginx-1 y laravel-db-1.

Crear el proyecto Laravel

Entra al contenedor:

bash
docker exec -it laravel-app-1 bash

Dentro, crea Laravel en una carpeta temporal y mueve todo a la raíz (/var/www), que está sincronizada con ~/laravel-docker/:

bash
cd /var/www
composer create-project laravel/laravel application --prefer-dist
cp -RT application/ . && rm -rf application
php artisan key:generate

Sal del contenedor:

bash
exit

Ahora en tu host verás la estructura de Laravel directamente en la raíz ~/laravel-docker/:

text
~/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:

bash
nano .env

Ajusta las variables de base de datos y URL:

env
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=db es el nombre del servicio MySQL en la red interna de Docker. No uses localhost aquí: dentro del contenedor app, localhost sería él mismo.

Reinicia el contenedor de la app:

bash
docker compose restart app

6. Crear un endpoint API en Laravel

Edita routes/api.php:

bash
nano routes/api.php

Reemplaza su contenido por:

php
<?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.php no existe, créalo y regístralo en bootstrap/app.php:

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:

bash
curl http://localhost:8000/api/hello

Deberías recibir algo como:

json
{"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:

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:

bash
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í:

text
~/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:

bash
nano frontend/vite.config.js

Contenido:

js
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:

bash
nano frontend/.env
env
VITE_API_URL=http://localhost:8000/api

React 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ías http://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:

bash
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:

bash
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í:

yaml
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:

  • frontend monta solo ./frontend en /app. Laravel (raíz) y React (frontend/) quedan completamente separados en el host.

  • El volumen frontend_node_modules evita que los node_modules del contenedor choquen con los del host.

  • --host 0.0.0.0 es obligatorio para que Vite sea accesible desde fuera del contenedor.


10. Levantar React y verificar todo

bash
docker compose up -d

Verifica los 4 contenedores:

bash
docker ps

Deberías ver:

text
laravel-app-1        (PHP-FPM)
laravel-nginx-1      (Nginx → Laravel)
laravel-db-1         (MySQL)
react-frontend-1     (React + Vite)

Abre en el navegador:


11. Consumir el endpoint desde React

Edita frontend/src/App.jsx:

bash
nano frontend/src/App.jsx

Reemplaza su contenido por:

jsx
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 App

Guarda y abre http://localhost:5173. Deberías ver el mensaje "¡Hola desde Laravel API!" renderizado en tu aplicación React.


Comandos útiles

AcciónComando
Levantar tododocker compose up -d
Detener tododocker compose down
Detener y borrar volúmenesdocker compose down -v
Ver logs de un serviciodocker compose logs -f frontend
Entrar al contenedor de Laraveldocker exec -it laravel-app-1 bash
Entrar al contenedor de Reactdocker exec -it react-frontend-1 sh
Ejecutar artisandocker exec -it laravel-app-1 php artisan migrate
Ejecutar npmdocker exec -it react-frontend-1 npm install <pkg>
Reconstruir solo la appdocker 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 5173 con 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

Entradas más populares de este blog

12. Hola Mundo en Docker.

2.1-Instalación de Docker (nuevo método oficial), configuración de un proyecto Laravel con Nginx y MySQL en contenedores

11¿Qué es Docker? y ¿Por qué debo saberlo?