docker-compose.yml:
services:
nginx:
image: nginx:stable-alpine
container_name: nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
- ./nginx/certificates:/etc/nginx/certificates
- /etc/letsencrypt/options-ssl-nginx.conf:/etc/letsencrypt/options-ssl-nginx.conf
- /etc/letsencrypt/ssl-dhparams.pem:/etc/letsencrypt/ssl-dhparams.pem
depends_on:
- frontend
- backend
- socketio
networks:
- sail
backend:
build:
context: backend/docker/8.3
dockerfile: Dockerfile
args:
WWWGROUP: '${WWWGROUP}'
container_name: backend
restart: unless-stopped
image: sail-8.3/app
extra_hosts:
- 'host.docker.internal:host-gateway'
ports:
- '${APP_PORT:-8000}:8000'
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
environment:
LARAVEL_SAIL: 1
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
IGNITION_LOCAL_SITES_PATH: '${PWD}'
volumes:
- './backend:/var/www/html'
networks:
- sail
depends_on:
- mysql
- redis
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: frontend
restart: unless-stopped
ports:
- "3000:3000"
environment:
NODE_ENV: "production"
networks:
- sail
depends_on:
- backend
socketio:
build:
context: ./socketio
dockerfile: Dockerfile
container_name: socketio
restart: unless-stopped
ports:
- "4000:4000"
environment:
PORT: 4000
FRONTEND_URL: '${FRONTEND_URL}'
BACKEND_URL: '${BACKEND_URL}'
BACKEND_NET_URL: '${BACKEND_NET_URL}'
REDIS_URL: '${REDIS_URL}'
networks:
- sail
depends_on:
- backend
- redis
mysql:
image: 'mysql/mysql-server:8.0'
ports:
- '${FORWARD_DB_PORT:-3306}:3306'
environment:
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
MYSQL_ROOT_HOST: '%'
MYSQL_DATABASE: '${DB_DATABASE}'
MYSQL_USER: '${DB_USERNAME}'
MYSQL_PASSWORD: '${DB_PASSWORD}'
volumes:
- 'sail-mysql:/var/lib/mysql'
- './backend/docker/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
networks:
- sail
healthcheck:
test:
- CMD
- mysqladmin
- ping
- '-p${DB_PASSWORD}'
retries: 3
timeout: 5s
redis:
image: 'redis:alpine'
ports:
- '${FORWARD_REDIS_PORT:-6379}:6379'
volumes:
- 'sail-redis:/data'
command: ['redis-server', '--replicaof', 'no', 'one']
networks:
- sail
healthcheck:
test:
- CMD
- redis-cli
- ping
retries: 3
timeout: 5s
networks:
sail:
driver: bridge
volumes:
sail-mysql:
driver: local
sail-redis:
driver: local
This file is just an extended version of docker-compose.yml file you will find in your regular Laravel Sail application's root folder. backend, mysql, redis service definitions are mostly generated from Laravel Sail along with the networks and volumes parts. I have additionally defined services for my frontend and socketio services. And I added an nginx service to reverse-proxy requests made to 80 or 443 ports of my localhost to each service running on ports 3000 (frontend), 4000 (socketio), 8000 (backend). I will share my nginx.conf shortly.
The docker-compose.dev.yml file looks the same as docker-compose.yml except definitions for frontend and socketio services point to respective service folder's Dockerfile.dev file to prevent projects from being built when developing.
The docker-compose.prod.yml does also look similiar to docker-compose.yml file. The only difference is that I removed XDEBUG_MODE, XDEBUG_CONFIG, IGNITION_LOCAL_SITES_PATH enviroment variable definitions in backend service since we don't need any debugging in production. Another thing is I removed all ports definitions in all services except the nginx service to prevent requests made to 3000, 4000, 8000 , 3306, 6379 ports. Users can only reach to my domain and not domain:3000 or other ports as a minor security measurement. (Note: I used expose instead of ports to definition for that purpose in each service.)
And for the last step, nginx.conf:
limit_req_zone \$binary_remote_addr zone=mylimit:20m rate=20r/s;
server {
listen 80;
server_name golden-dirt.dev;
# Redirect all HTTP requests to HTTPS
return 301 http://$host$request_uri;
}
server {
listen 443 ssl;
server_name golden-dirt.dev;
ssl_certificate /etc/nginx/certificates/golden-dirt.dev.crt;
ssl_certificate_key /etc/nginx/certificates/golden-dirt.dev.key;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Enable rate limiting
limit_req zone=mylimit burst=30 nodelay;
# Backend (Laravel) Storage
location /storage/ {
proxy_pass http://backend:8000;
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;
}
# Frontend (Next.js)
location / {
proxy_pass http://frontend:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
# Disable buffering for streaming support
proxy_buffering off;
proxy_set_header X-Accel-Buffering no;
}
# Backend (Laravel)
location /api/ {
proxy_pass http://backend:8000;
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;
}
# Socket.io
location /socket.io/ {
proxy_pass http://socketio:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
This nginx.conf simply reverse-proxies the requests made to server_name golden-dirt.dev address to each defined service as instructed. It also adds SSL certificates that I acquired by my domain provider but you can create self-signed SSL certificates using certbot for both development and production. And lastly, there is a simple request throttling definition at beginnig of the file: limit_req_zone \$binary_remote_addr zone=mylimit:20m rate=20r/s; when users make too many requests in defined parameters, they receive 503 service unavaialble response to prevent DDoS attacks. (Don't know how effective this is but it seems to be doing a great job, though I had to increase parameters since some of the requests that my website do to acquire static assets like images and so on was blocked by this measurement, so I had to increase the limits a bit.)