Raja's Exocortex

Laravel Production Deployment

Best practices for deploying Laravel apps in production.

Installation Files and Commands

Note: change app and app.example.com as required.

Docker Compose

# Filename: /srv/app.example.com/docker-compose.yaml

name: app-example-com

services:
  app:
    image: rsubr/php-apache-ubuntu:noble
    container_name: app.example.com
    restart: always

    volumes:
      - ./www:/var/www
      - ./www/public:/var/www/html

    healthcheck:
	  test: ["CMD", "php", "/var/www/health.php"]
	  interval: 30s
	  timeout: 1s
	  retries: 3

    depends_on:
      pgdb:
        condition: service_healthy

    labels:
      - com.centurylinklabs.watchtower.enable=true
      - traefik.enable=true
      - traefik.http.routers.app.rule=Host(`app.example.com`)
      - traefik.http.routers.app.tls=true
      - traefik.http.routers.app.tls.certresolver=lets-encrypt


  pgdb:
    image: postgres:16-alpine
    container_name: pgdb-app.example.com
    restart: always
    environment:
      - POSTGRES_DB=app
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=app

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 30s
      timeout: 5s
      retries: 5

    volumes:
      - ./data:/var/lib/postgresql/data

Filename: health.php

<?php
# Filename: health.php
# Health check script for docker

# In docker-compose, use
#   healthcheck:
#     test: ["CMD", "php", "/var/www/health.php"]
#     interval: 10s
#     timeout: 10s
#     retries: 3

$url = "http://127.0.0.1/up";
$headers = @get_headers($url);

if (!$headers) {
  echo "ERROR: running health check";
  exit(2);
}

if (!str_contains($headers[0], '200')) {
  echo "ERROR: Health check failed  $url\n\n";
  echo implode("\n", $headers) , "\n";
  exit(1);
}

echo "SUCCESS\n";
exit(0);

Installing

# Commands to run as root user
sudo composer install --no-dev
sudo cp .env.sample .env
sudo php aritsan key:generate
sudo chown -Rh www-data: bootstrap/cache storage

# Run the below as www-data user
sudo -u www-data php artisan migrate
sudo -u www-data php artisan optimize

To install static versions of php and composer on the docker host, use:

  1. Static PHP
  2. composer.phar

Copy the files to /usr/local/bin/ and make them executable.

Environment .env file changes

# Filename: .env

APP_NAME=TODO_APP_NAME
...
# Most important for production
APP_ENV=production
APP_DEBUG=false
...
APP_TIMEZONE=Asia/Kolkata
APP_URL=https://app.example.com

# Using postgres from a linked docker container
DB_CONNECTION=pgsql
DB_HOST=pgdb
DB_PORT=5432
DB_DATABASE=app
DB_USERNAME=app
DB_PASSWORD=app

# Let docker handle log storage
LOG_CHANNEL=stderr

Deploying Updates in Production

Use a CI/CD pipeline to deploy updates to production using the below deploy.sh script.

Filename: deploy.sh

#!bin/bash
# Filename: deploy.sh
# Run this deploy script as root to install code updates and DB migrations

# Abort script on errors
set -eoux pipefail

# Function to run commands as www-data user
run_as_www_data() {
  su -s /bin/bash www-data -c "$*"
}

run_as_www_data "php artisan down --refresh=15"

git pull
composer install --no-dev

run_as_www_data "php artisan migrate"
run_as_www_data "php artisan optimize"

chown -Rh www-data: bootstrap/cache storage

run_as_www_data "php artisan up"

Auto Deploy Updates Using Webhook

  1. Run apt install webook on the docker host.
  2. Create /etc/webhook.conf with below content:
# Filename: /etc/webhook.conf
# Deploy Laravel app https://git.example.com./example/example-laravel
- id: app-example-deploy
  execute-command: "/srv/app.example.com/www/deploy.sh"
  command-working-directory: "/srv/all.example.com/www"
  include-command-output-in-response: true
  1. Whenever code is pushed to the main branch, configure a webhook for this repo in Gitea to call the webbook URL http://docker-host.example.com:9000/hooks/app-example-deploy. This will run deploy.sh as the root user.

References

  1. Official Laravel Deployment docs
  2. Best practices for setting up a Laravel development laravel-dev-env-setup