Belajar deploy projek Laravel dan React menggunakan Docker adalah skill penting untuk developer fullstack. Artikel ini membahas cara praktis setup Docker untuk aplikasi Laravel backend dengan React frontend, termasuk solusi untuk masalah authentication yang sering terjadi seperti CORS error dan session tidak bekerja.
Halo, saya Angga Risky Setiawan, founder dari BuildWithAngga.
Kalau kamu sudah bisa bikin aplikasi Laravel dan React di local, langkah selanjutnya adalah deploy ke server. Dan Docker adalah cara paling modern untuk melakukannya.
Kenapa Harus Belajar Docker?
Alasan pertama adalah consistency. Aplikasi yang jalan di laptop kamu akan jalan persis sama di server production. Tidak ada lagi "di laptop saya jalan" syndrome.
Docker memberikan isolation. Setiap service punya environment sendiri. PHP versi 8.2 untuk Laravel, Node versi 20 untuk React, MySQL 8.0 untuk database. Semua terisolasi dan tidak saling ganggu.
Scalability juga jadi mudah. Kalau traffic naik, tinggal tambah container. Tidak perlu setup server baru dari nol.
Yang paling penting, Docker sudah jadi standar industri. Hampir semua company modern menggunakan Docker untuk deployment. Skill ini akan sangat berguna untuk karir kamu.
Problem Umum Deploy Tanpa Docker
Tanpa Docker, deploy aplikasi fullstack itu ribet.
Konflik versi sering terjadi. Server pakai PHP 7.4 tapi Laravel butuh PHP 8.1. Node di server versi lama. MySQL versi berbeda. Semua ini menyebabkan error yang sulit di-debug.
Konfigurasi server berbeda-beda. Setiap VPS punya setup yang berbeda. Apa yang work di DigitalOcean belum tentu work di AWS. Dokumentasi deployment jadi panjang dan error-prone.
Sulit reproduce bugs dari production. Karena environment berbeda, bug yang muncul di production tidak bisa di-reproduce di local.
Problem Authentication yang Pasti Kamu Alami
Ini yang paling menyebalkan. Kamu deploy Laravel dan React, tapi authentication tidak work.
Error "Session store not set on request" muncul terus. Padahal di local jalan dengan baik.
CORS errors di browser console. Kamu sudah setting CORS di Laravel, tapi tetap error.
Cookie tidak terkirim ke backend. User sudah login, tapi setiap request dianggap unauthenticated.
CSRF token invalid. Laravel menolak request karena token tidak match.
Semua ini terjadi karena satu alasan: frontend dan backend di origin yang berbeda. Browser punya security policy ketat untuk cross-origin requests.
Solusi yang Akan Kita Pelajari
Di artikel ini, kamu akan belajar setup yang menghilangkan semua masalah di atas.
Kita akan menggunakan Docker Compose untuk orchestrate semua services. Laravel, React, MySQL, Redis, semuanya dalam satu konfigurasi.
Nginx reverse proxy akan menyatukan frontend dan backend dalam satu origin. Ini kunci untuk menghilangkan CORS dan session issues.
Arsitekturnya seperti ini:
User Browser
↓
Nginx (port 80) ─── Single Entry Point
├── /api/* → Laravel Backend
├── /sanctum/* → Laravel Backend
├── /storage/* → Laravel Backend
└── /* → React Frontend
Dengan setup ini, browser melihat semuanya sebagai satu origin. Tidak ada CORS. Cookies flow natural. Authentication just works.
Yang Kamu Butuhkan
Sebelum lanjut, pastikan kamu sudah punya:
Projek Laravel yang sudah jadi dengan Sanctum untuk authentication. Projek React yang sudah jadi dengan Axios untuk HTTP requests.
Docker Desktop terinstall di komputer kamu. Kalau belum, download dari docker.com dan install.
Basic knowledge terminal atau command line. Kamu akan menjalankan beberapa perintah Docker.
Siap? Di bagian selanjutnya, kita akan setup Docker Compose untuk menyatukan semua services.
Setup Docker Compose - Menyatukan Semua Services
Docker Compose memudahkan menjalankan multiple containers sekaligus untuk aplikasi fullstack. Bagian ini membahas cara setup Docker Compose untuk Laravel backend, React frontend, MySQL database, dan Redis cache dalam satu konfigurasi yang terintegrasi.
Docker Compose adalah tool untuk mendefinisikan dan menjalankan multi-container aplikasi. Dengan satu file konfigurasi, kamu bisa start semua services yang dibutuhkan.
Struktur Folder Project
Sebelum mulai, pastikan struktur folder project kamu seperti ini:
project/
├── docker-compose.yml # Orchestration config
├── Dockerfile # Laravel container
├── nginx.conf # Backend Nginx config
├── frontend/
│ ├── Dockerfile # React container
│ ├── nginx.conf # Frontend Nginx + Proxy
│ └── ... (React source)
└── src/ # Laravel source code
Laravel ada di folder src. React ada di folder frontend. File Docker dan konfigurasi di root project.
Docker Compose Configuration
Buat file docker-compose.yml di root project:
services:
# Frontend - Main entry point
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: app-frontend
restart: unless-stopped
ports:
- "80:80"
networks:
- app-network
depends_on:
- app
# Laravel Application
app:
build:
context: .
dockerfile: Dockerfile
container_name: app-backend
restart: unless-stopped
working_dir: /var/www/html
volumes:
- ./src:/var/www/html
networks:
- app-network
depends_on:
- database
- redis
# Backend Nginx (serve PHP)
webserver:
image: nginx:alpine
container_name: app-webserver
restart: unless-stopped
volumes:
- ./src:/var/www/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
networks:
- app-network
depends_on:
- app
# MySQL Database
database:
image: mysql:8.0
container_name: app-database
restart: unless-stopped
environment:
MYSQL_DATABASE: laravel_db
MYSQL_ROOT_PASSWORD: root
MYSQL_PASSWORD: secret
MYSQL_USER: laravel
volumes:
- dbdata:/var/lib/mysql
networks:
- app-network
# Redis Cache
redis:
image: redis:alpine
container_name: app-redis
restart: unless-stopped
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
dbdata:
driver: local
Penjelasan Setiap Service
Mari kita breakdown satu per satu.
Frontend adalah container untuk React app. Ini yang expose port 80 ke dunia luar. Di dalamnya ada Nginx yang serve React build dan sekaligus proxy request ke backend.
App adalah container Laravel dengan PHP-FPM. Tidak expose port ke luar. Hanya bisa diakses oleh container lain dalam network yang sama.
Webserver adalah Nginx untuk serve Laravel. Nginx ini connect ke PHP-FPM di container app. Juga tidak expose port ke luar.
Database adalah MySQL 8.0. Data disimpan di volume dbdata supaya tidak hilang saat container restart.
Redis untuk cache dan bisa juga untuk session storage. Ringan dan cepat.
Kenapa Frontend yang Expose Port 80?
Ini pertanyaan penting.
Dengan setup ini, semua request dari user masuk ke frontend container dulu. Nginx di frontend akan memutuskan ke mana request harus diarahkan.
Request ke /api akan di-proxy ke backend. Request lainnya akan di-serve oleh React. Dari sudut pandang browser, semuanya datang dari origin yang sama: port 80.
Ini yang menghilangkan masalah CORS. Browser tidak tahu bahwa /api sebenarnya di-handle oleh container berbeda. Yang browser tahu, semua dari domain yang sama.
Network dan Communication
Perhatikan bahwa semua services ada di app-network yang sama. Ini memungkinkan container berkomunikasi satu sama lain menggunakan nama service sebagai hostname.
Frontend bisa akses webserver dengan URL http://webserver. App bisa akses database dengan hostname database. Redis bisa diakses dengan hostname redis.
Tidak perlu IP address. Docker DNS akan resolve nama service ke IP yang benar.
depends_on
depends_on memastikan urutan startup yang benar. Frontend baru start setelah app ready. App baru start setelah database dan redis ready.
Ini mencegah error karena dependency belum siap.
Di bagian selanjutnya, kita akan setup konfigurasi Nginx yang merupakan kunci dari same-origin setup ini.
Konfigurasi Nginx - Kunci Same-Origin Setup
Nginx reverse proxy adalah kunci untuk menghilangkan masalah CORS dan authentication di aplikasi fullstack. Bagian ini membahas konfigurasi Nginx untuk proxy API requests ke Laravel backend sambil serve React frontend, semua dalam satu origin.
Nginx adalah web server yang sangat powerful untuk reverse proxy. Di setup kita, ada dua Nginx: satu di frontend container dan satu di backend container.
Frontend Nginx Configuration
Ini adalah konfigurasi paling penting. Buat file frontend/nginx.conf:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# Proxy /api ke Laravel backend
location /api {
proxy_pass <http://webserver>;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# PENTING untuk cookies dan session
proxy_set_header Cookie $http_cookie;
proxy_pass_header Set-Cookie;
}
# Proxy /sanctum untuk CSRF cookie
location /sanctum {
proxy_pass <http://webserver>;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Cookie $http_cookie;
proxy_pass_header Set-Cookie;
}
# Proxy /storage untuk file uploads
location /storage {
proxy_pass <http://webserver>;
proxy_set_header Host $host;
}
# React SPA routing
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Mari breakdown setiap bagian.
Location /api - Proxy ke Backend
Semua request yang dimulai dengan /api akan diteruskan ke backend Laravel.
proxy_pass <http://webserver> mengarahkan request ke service webserver di Docker network. Ingat, webserver adalah nama service di docker-compose.yml.
Header X-Real-IP dan X-Forwarded-For penting supaya Laravel tahu IP asli user, bukan IP container.
Header untuk Cookies dan Session
Dua baris ini adalah yang paling kritikal:
proxy_set_header Cookie $http_cookie;
proxy_pass_header Set-Cookie;
Baris pertama memastikan cookies dari browser diteruskan ke backend. Tanpa ini, Laravel tidak akan menerima session cookie.
Baris kedua memastikan response header Set-Cookie dari Laravel diteruskan ke browser. Tanpa ini, browser tidak akan menyimpan session cookie.
Kalau authentication kamu tidak work, kemungkinan besar karena dua baris ini tidak ada.
Location /sanctum - CSRF Cookie
Laravel Sanctum butuh endpoint /sanctum/csrf-cookie untuk mengambil CSRF token sebelum login.
Konfigurasinya sama dengan /api. Harus include header untuk cookies supaya CSRF token bisa di-set di browser.
Location /storage - File Uploads
Kalau aplikasi kamu punya file uploads yang disimpan di Laravel storage, endpoint /storage harus di-proxy juga.
Ini memungkinkan React mengakses file seperti /storage/images/photo.jpg yang sebenarnya ada di Laravel backend.
Location / - React SPA Routing
location / {
try_files $uri $uri/ /index.html;
}
Ini konfigurasi standar untuk Single Page Application. React Router handle semua routing di client side.
Ketika user akses /dashboard atau /profile, Nginx akan coba cari file tersebut. Kalau tidak ada, return index.html. React Router yang kemudian render halaman yang sesuai.
Tanpa konfigurasi ini, refresh halaman di route selain / akan menghasilkan 404.
Backend Nginx Configuration
Sekarang konfigurasi untuk backend. Buat file nginx.conf di root project:
server {
listen 80;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \\.php$ {
fastcgi_pass app:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\\.(?!well-known).* {
deny all;
}
}
Ini konfigurasi standar Laravel. Nginx listen di port 80 dan forward PHP requests ke PHP-FPM di container app port 9000.
root /var/www/html/public mengarah ke folder public Laravel. Semua request masuk melalui index.php.
Verifikasi Konfigurasi
Setelah setup, cara verify konfigurasi nginx sudah benar:
# Test nginx config syntax
docker exec -it app-frontend nginx -t
# Reload nginx kalau ada perubahan
docker exec -it app-frontend nginx -s reload
Kalau ada error, nginx akan memberitahu baris mana yang bermasalah.
Di bagian selanjutnya, kita akan setup Laravel Sanctum supaya session-based authentication bekerja dengan baik.
Konfigurasi Laravel Sanctum untuk Session Auth
Laravel Sanctum menyediakan session-based authentication untuk SPA yang membutuhkan konfigurasi tepat. Bagian ini membahas setup environment variables, middleware, dan session driver agar authentication antara React dan Laravel berjalan lancar di production.
Nginx sudah siap. Sekarang kita perlu memastikan Laravel dikonfigurasi dengan benar untuk menerima dan memproses session authentication.
Environment Variables
Edit file .env di folder Laravel kamu. Beberapa konfigurasi ini wajib untuk session auth di Docker:
APP_URL=http://yourdomain.com
# Session Configuration
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_DOMAIN=yourdomain.com
# Sanctum Configuration
SANCTUM_STATEFUL_DOMAINS=yourdomain.com
# Database - sesuaikan dengan docker-compose
DB_CONNECTION=mysql
DB_HOST=database
DB_PORT=3306
DB_DATABASE=laravel_db
DB_USERNAME=laravel
DB_PASSWORD=secret
# Redis
CACHE_DRIVER=redis
REDIS_HOST=redis
Mari bahas yang penting.
SESSION_DRIVER=database
Ini wajib untuk Docker deployment. Kenapa bukan file?
Karena di Docker, container bisa di-restart atau di-recreate kapan saja. Kalau session disimpan di file, semua session hilang saat container restart. User harus login ulang.
Dengan menyimpan session di database, data persistent. Container boleh restart, session tetap ada.
SANCTUM_STATEFUL_DOMAINS
Ini memberitahu Sanctum domain mana yang boleh menggunakan session-based authentication. Isi dengan domain production kamu tanpa protocol.
Kalau domain kamu app.example.com, isi dengan app.example.com. Untuk development, bisa tambahkan localhost.
SANCTUM_STATEFUL_DOMAINS=yourdomain.com,localhost
DB_HOST=database
Perhatikan bahwa DB_HOST bukan localhost atau 127.0.0.1. Tapi database yang merupakan nama service di docker-compose.
Docker akan resolve nama ini ke IP container MySQL. Sama untuk REDIS_HOST=redis.
Pastikan Sessions Table Ada
Session driver database butuh table untuk menyimpan data. Jalankan command ini:
php artisan session:table
php artisan migrate
Atau pastikan migration ini sudah ada di project kamu:
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
Tanpa table ini, kamu akan dapat error "Session store not set on request". Ini error paling umum dan solusinya sesederhana menjalankan migration.
laravel 12 - Enable Stateful API
Kalau kamu pakai laravel 12, ada satu langkah tambahan yang wajib.
Buka file bootstrap/app.php dan pastikan ada konfigurasi ini:
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->statefulApi(); // WAJIB untuk session auth
})
->create();
Method statefulApi() mengaktifkan session dan cookie handling untuk API routes. Tanpa ini, Sanctum tidak akan work untuk SPA authentication.
Di Laravel 10 ke bawah, konfigurasi ini ada di app/Http/Kernel.php dengan menambahkan middleware group.
CORS Configuration
Edit file config/cors.php:
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true, // WAJIB true
];
Yang paling penting adalah supports_credentials harus true. Ini memungkinkan cookies dikirim dalam cross-origin requests.
Meskipun kita sudah setup same-origin, setting ini tetap diperlukan untuk memastikan cookies berjalan dengan baik.
Checklist Laravel
Sebelum lanjut, pastikan semua sudah di-check:
- [ ] SESSION_DRIVER=database di .env
- [ ] Sessions table sudah di-migrate
- [ ] statefulApi() ada di bootstrap/app.php (laravel 12)
- [ ] supports_credentials = true di config/cors.php
- [ ] SANCTUM_STATEFUL_DOMAINS sesuai domain production
- [ ] DB_HOST dan REDIS_HOST pakai nama service Docker
Kalau ada yang terlewat, authentication tidak akan work. Double check sebelum deploy.
Di bagian terakhir, kita akan setup React dengan Axios dan melakukan deployment.
Setup React Axios dan Deployment
Frontend React dengan Axios perlu dikonfigurasi dengan benar untuk berkomunikasi dengan Laravel backend. Bagian ini membahas setup Axios untuk handle cookies dan CSRF token, Dockerfile untuk production build, serta langkah-langkah deployment menggunakan Docker Compose.
Backend sudah siap. Sekarang kita pastikan frontend React bisa berkomunikasi dengan baik dan deploy semuanya.
Environment Variables React
Buat file .env.production di folder frontend:
VITE_API_BASE_URL=/api
Cukup sederhana. Karena kita pakai same-origin setup, API URL cukup /api saja. Tidak perlu full URL dengan domain.
Ini yang membuat setup kita clean. React tidak perlu tahu di mana backend berada. Semua request ke /api akan otomatis di-proxy oleh Nginx ke Laravel.
Axios Configuration
Buat file untuk konfigurasi Axios, misalnya src/lib/axios.ts:
import axios from 'axios';
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
withCredentials: true, // WAJIB untuk cookies
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
// Auto-attach CSRF token ke setiap request
apiClient.interceptors.request.use((config) => {
const xsrfToken = document.cookie
.match(/XSRF-TOKEN=([^;]+)/)?.[1];
if (xsrfToken) {
config.headers['X-XSRF-TOKEN'] = decodeURIComponent(xsrfToken);
}
return config;
});
export default apiClient;
Yang paling penting adalah withCredentials: true. Ini memberitahu Axios untuk mengirim cookies di setiap request. Tanpa ini, session cookie tidak akan terkirim dan user dianggap tidak login.
Interceptor di bawah otomatis mengambil CSRF token dari cookie dan menambahkannya ke header. Laravel butuh ini untuk validasi request.
Fetch CSRF Token Sebelum Login
Sebelum melakukan login atau register, kamu harus mengambil CSRF token dulu:
import axios from 'axios';
import apiClient from './lib/axios';
// Panggil ini sebelum login/register
export const getCsrfToken = async () => {
await axios.get('/sanctum/csrf-cookie', {
withCredentials: true
});
};
export const login = async (email: string, password: string) => {
await getCsrfToken(); // Ambil CSRF dulu
return apiClient.post('/login', { email, password });
};
export const register = async (data: RegisterData) => {
await getCsrfToken(); // Ambil CSRF dulu
return apiClient.post('/register', data);
};
Flow-nya: ambil CSRF cookie dulu, baru kirim request login. Kalau urutan ini terbalik, Laravel akan menolak request dengan error "CSRF token mismatch".
Frontend Dockerfile
Buat file Dockerfile di folder frontend:
# Build stage
FROM node:20-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Ini multi-stage build. Stage pertama build React app. Stage kedua copy hasil build ke Nginx container yang ringan.
Hasil akhirnya adalah container kecil yang hanya berisi static files dan Nginx. Tidak ada Node.js di production.
Laravel Dockerfile
Buat file Dockerfile di root project untuk Laravel:
FROM php:8.2-fpm
# Install dependencies
RUN apt-get update && apt-get install -y \\
git curl zip unzip libpng-dev libonig-dev libxml2-dev \\
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY ./src .
RUN composer install --optimize-autoloader --no-dev
# Set permissions
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
EXPOSE 9000
CMD ["php-fpm"]
Container ini berisi PHP-FPM dengan extensions yang dibutuhkan Laravel. Composer install dependencies dan set permission untuk storage.
Deployment Commands
Sekarang saatnya deploy. Jalankan commands ini di server:
# Build semua images
docker-compose build
# Jalankan semua containers di background
docker-compose up -d
# Jalankan migrations
docker exec -it app-backend php artisan migrate --force
# Generate app key (kalau belum)
docker exec -it app-backend php artisan key:generate
# Clear semua cache
docker exec -it app-backend php artisan config:clear
docker exec -it app-backend php artisan cache:clear
docker exec -it app-backend php artisan route:clear
# Cek status containers
docker ps
Kalau semua berjalan lancar, docker ps akan menunjukkan 5 containers running.
Testing Checklist
Setelah deploy, verifikasi semuanya bekerja:
- Akses
http://yourdomain.com- harus muncul React app - Buka DevTools → Network tab
- Coba login dan perhatikan:
- Request ke
/sanctum/csrf-cookieberhasil - Cookie
XSRF-TOKENter-set di browser - Request ke
/api/loginmembawa cookies - Tidak ada CORS error di Console
- Request ke
- Refresh halaman setelah login - harus tetap login
- Coba akses protected route - harus bisa
Kalau semua checklist pass, selamat! Deployment kamu berhasil.
Troubleshooting Quick Reference
Error: "Session store not set on request"
- Jalankan
php artisan migrate - Cek SESSION_DRIVER=database
Error: CORS atau cookies tidak terkirim
- Pastikan
withCredentials: truedi Axios - Cek nginx proxy headers
Error: 502 Bad Gateway
- Cek containers running:
docker ps - Cek logs:
docker logs app-webserver
Error: CSRF token mismatch
- Panggil
/sanctum/csrf-cookiesebelum login - Cek interceptor Axios sudah benar
Penutup
Deploy aplikasi fullstack Laravel dan React dengan Docker memang butuh setup yang detail. Tapi hasilnya worth it.
Kamu sekarang punya deployment yang consistent, scalable, dan bebas dari masalah authentication yang menyebalkan. Same-origin setup dengan Nginx proxy membuat cookies dan session bekerja natural tanpa konfigurasi CORS yang rumit.
Kunci suksesnya ada di tiga hal: Nginx proxy dengan header yang benar untuk cookies, Laravel Sanctum dengan statefulApi middleware, dan Axios dengan withCredentials true.
Untuk belajar lebih dalam tentang Laravel, React, dan deployment, explore kelas-kelas di BuildWithAngga. Banyak kelas fullstack yang mengajarkan dari development sampai production-ready deployment.
Selamat deploy dan semoga aplikasi kamu sukses!
Angga Risky Setiawan
Founder, BuildWithAngga