Laravel Production Deployment Setup

Apr 30, 2025

Laravel Production Deployment Setup

# Introduction

In this guide, we’ll walk through the deployment of a dockerized production server architecture for a Laravel application, complete with services such as Postgres, Nginx, queue workers, Mailpit.

# Prerequisites

Before proceeding, ensure you have the following:

  • A working Laravel 11 application
  • Application production server
  • Application staging server (optional)
  • Production database server
  • Staging database server (optional)
  • Reverse proxy server

# Architecture

Nginx Reverse Proxy Setup

Here’s a look at the server architecture to support the production deployment of a Laravel 11 application.

The reverse proxy server is listenning on ports 80 and 443 for incoming HTTP requests. The incoming traffic is then pointed to the corresponding application instance running on either the production or staging server. Each application instance then forwards their database requests to the respective database server; production app server will talk to the production database server and staging app server will direct its database calls to the staging database server.

# Folder Structure

The snippet below shows the folder structure and files needed for this setup. They belong to the same parent directory (monorepo) to faciliate easier Git tracking.

.
├── app
├── docker-compose-app-prod.yml
├── docker-compose-db-prod.yml
├── docker-compose-reverse-proxy.yml
├── docker-compose-app-dev.yml
├── Dockerfile
└── nginx
    ├── nginx.conf
    ├── nginx-reverse-proxy.conf
    └── nginx-mailpit.conf

# Reverse Proxy Server

# What is a reverse proxy?

A reverse proxy is a server that sits in front of web servers and forwards client (e.g. web browser) requests to those web servers. Reverse proxies are typically implemented to help increase security, performance, and reliability.

# Benefits of a reverse proxy

  • Loading balancing - A single origin server can become overwhelmed if there are a large number of users sending requests to application/website running on that single server. To combat this point of vulnerability, the application can be deployed on a pool of servers. A reverse proxy is placed in front the pool of servers and it provides a load balancing solution to distribute the incoming traffic evenly.

  • Protection from attacks - With a reverse proxy in place, the application/website never reveals the IP address of their origin server(s). As such, attackers would not be able to directly attack the origin servers but rather only be able to target the reverse proxy.

  • SSL encryption - SSL encryption and decryption can be done on the reverse proxy which can be computationally expensive if performed on the application/website origin server.

# Implementing reverse proxy using Nginx and Docker

Below is the docker-compose-re​verse-proxy.yml file.

services:
  nginx:
    image: nginx:stable-alpine
    container_name: reverse_proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx-reverse-proxy.conf:/etc/nginx/templates/default.conf.template
      - ./nginx/ssl:/etc/nginx/ssl
      - ./nginx/logs:/var/log/nginx
    restart: unless-stopped

Below is the nginx-reverse-proxy.conf file.

server {
    listen 80;
    server_name xxx.xx.xxx.xx;  # IP of reverse proxy server

    location / {
        proxy_pass http://xxx.xx.xxx.xx:8000;  # IP of app server IP and port number
        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;
    }
}

# Deploying reverse proxy on Linux server

  • Use the assigned server credentials and SSH into the machine.
  • Ensure Docker and Git are both installed on the machine
  • Clone the project repository from it’s hosted source origin
  • In the project directory, run the following command to build the image and containers:
  sudo docker compose -f docker-compose-reverse-proxy.yml up -d --build

# Production Application Server

In this guide, the production application server will be running a dockerizedLaravel 11 web application. Below is the docker-compose-app-prod.yml file.

services:
  app:
    container_name: laravel
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    env_file:
      - .env
    volumes:
      - storage:/usr/share/nginx/html/storage:rw
      - public:/usr/share/nginx/html/public:rw
    networks:
      - laravel-network
  queue-worker:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    command: php artisan queue:work
    environment:
      IS_WORKER: "true"
    env_file:
      - .env
    volumes:
      - storage:/usr/share/nginx/html/storage:rw
      - public:/usr/share/nginx/html/public:rw

  nginx:
    image: nginx:1-alpine
    ports:
      - "${APP_PORT}:80"
    volumes:
      - ./nginx-mailpit.conf:/etc/nginx/templates/default.conf.template
      - storage:/usr/share/nginx/html/storage:rw
      - public:/usr/share/nginx/html/public:ro
    networks:
      - laravel-network

  mailpit:
    image: axllent/mailpit
    container_name: mailpit
    ports:
      - "8025:8025"
      - "1025:1025"
    environment:
      MP_WEBROOT: /mailpit/
    restart: unless-stopped
    networks:
      - laravel-network

volumes:
  storage:
  public:

networks:
  laravel-network:
    driver: bridge

Below is the nginx-mailpit.conf file.

server {
     listen       80;
     server_name  localhost;
     root         /usr/share/nginx/html/public;

     access_log /dev/stdout;
     error_log  /dev/stderr error;

     index index.html index.htm index.php;

     location / {
         try_files $uri $uri/ /index.php$is_args$args;
     }

     location /storage/ {
         alias /usr/share/nginx/html/storage/app/public/;
         access_log off;
         expires max;
         add_header Cache-Control "public";
     }

     location ~ \.php$ {
         fastcgi_split_path_info ^(.+\.php)(/.+)$;
         fastcgi_pass app:9000;
         fastcgi_index index.php;
         include fastcgi.conf;
         include fastcgi_params;
         fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
     }

    location /mailpit/ {
        proxy_pass http://mailpit:8025/mailpit/;

        # configure the websocket
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        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;

    }

 }

# Deploying Laravel app on Linux server

  • Use the assigned server credentials and SSH into the machine.
  • Ensure Docker and Git are both installed on the machine
  • Clone the project repository from it’s hosted source origin
  • In the project directory, run the following command to build the image and containers:
  sudo docker compose -f docker-compose-app-prod.yml up -d --build

# Production Database Server

In this guide, the production database server will be hosting a Postgres database running in Docker. Below is the docker-compose-db-prod.yml file.

services:
  postgres:
    image: bitnami/postgresql:16.3.0
    platform: linux/amd64
    ports:
      - 5432:5432
    restart: always
    volumes:
      - db-data:/bitnami/postgresql
    environment:
      - POSTGRESQL_DATABASE=${DB_DATABASE}
      - POSTGRESQL_USERNAME=${DB_USERNAME}
      - POSTGRESQL_PASSWORD=${DB_PASSWORD}
    networks:
      - laravel-network
    healthcheck:
      test:
        ["CMD", "pg_isready", "-U", "${DB_USERNAME}", "-d", "${DB_DATABASE}"]
      interval: 10s
      retries: 5
      timeout: 5s
      start_period: 10s

volumes:
  db-data:

networks:
  laravel-network:
    driver: bridge

# Deploying database container on Linux server

  • Use the assigned server credentials and SSH into the machine.
  • Ensure Docker and Git are both installed on the machine
  • Clone the project repository from it’s hosted source origin
  • In the project directory, run the following command to build the image and containers:
  sudo docker compose -f docker-compose-db-prod.yml up -d --build

# Local development environment

To faciliate an easier local development setup, Docker can also be leveraged. To enable features such as hot reloading, some changes are needed as compared to the code presently in the docker-compose-app-prod.yml file. The local development environment will be setup in the docker-compose-app-dev.yml file as shown below.

services:
  postgres:
    image: bitnami/postgresql:16.3.0
    platform: linux/amd64
    ports:
      - 5432:5432
    restart: always
    volumes:
      - ./db-data:/bitnami/postgresql
    environment:
      - POSTGRESQL_DATABASE=${DB_DATABASE}
      - POSTGRESQL_USERNAME=${DB_USERNAME}
      - POSTGRESQL_PASSWORD=${DB_PASSWORD}
    networks:
      - laravel-network
    healthcheck:
      test:
        ["CMD", "pg_isready", "-U", "${DB_USERNAME}", "-d", "${DB_DATABASE}"]
      interval: 10s
      retries: 5
      timeout: 5s
      start_period: 10s

  app:
    container_name: laravel
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    ports:
      - 9002:9001
    depends_on:
      - postgres
    env_file:
      - .env
    volumes:
      - ./:/usr/share/nginx/html # Mount entire project directory for 'hot-reload'
      - storage:/usr/share/nginx/html/storage:rw
      - public:/usr/share/nginx/html/public:rw
    networks:
      - laravel-network
  queue-worker:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    command: php artisan queue:work
    environment:
      IS_WORKER: "true"
    env_file:
      - .env
    depends_on:
      - postgres
    volumes:
      - ./:/usr/share/nginx/html # Mount entire project directory for 'hot-reload'
      - storage:/usr/share/nginx/html/storage:rw
      - public:/usr/share/nginx/html/public:rw

  nginx:
    image: nginx:1-alpine
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./nginx.conf:/etc/nginx/templates/default.conf.template
      - storage:/usr/share/nginx/html/storage:rw
      - public:/usr/share/nginx/html/public:ro
    networks:
      - laravel-network

  mailpit:
    image: axllent/mailpit
    container_name: mailpit
    ports:
      - "8025:8025"
      - "1025:1025"
    environment:
      MP_WEBROOT: /mailpit/
    restart: unless-stopped
    networks:
      - laravel-network

volumes:
  storage:
  public:
  db-data:

networks:
  laravel-network:
    driver: bridge
LinuxDockerDocker ComposeLaravelPHPPostgresNginxQueue workersSSH