From 7ccb8b94fe2460af6b01d27af3f21e1de2dc1747 Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Wed, 15 Apr 2026 09:57:37 +0200 Subject: [PATCH] Initial commit: multi-version PHP+Nginx Docker image for Laravel --- .dockerignore | 13 ++ Dockerfile | 178 +++++++++++++++ README.md | 149 +++++++++++++ config/default.conf | 34 +++ config/nginx.conf | 79 +++++++ config/opcache.ini | 17 ++ config/php.ini | 22 ++ config/supervisor/nginx.conf | 11 + config/supervisor/php-fpm.conf | 9 + config/supervisord.conf | 35 +++ docker-bake.hcl | 123 +++++++++++ docs/examples.md | 210 ++++++++++++++++++ scripts/build.sh | 137 ++++++++++++ scripts/plug-n-pray.sh | 384 +++++++++++++++++++++++++++++++++ scripts/publish.sh | 133 ++++++++++++ scripts/start-container | 145 +++++++++++++ 16 files changed, 1679 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 config/default.conf create mode 100644 config/nginx.conf create mode 100644 config/opcache.ini create mode 100644 config/php.ini create mode 100644 config/supervisor/nginx.conf create mode 100644 config/supervisor/php-fpm.conf create mode 100644 config/supervisord.conf create mode 100644 docker-bake.hcl create mode 100644 docs/examples.md create mode 100755 scripts/build.sh create mode 100755 scripts/plug-n-pray.sh create mode 100755 scripts/publish.sh create mode 100755 scripts/start-container diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..adb7f2f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.github +.gitignore +*.md +!README.md +LICENSE +docker-bake.hcl +scripts/build.sh +scripts/publish.sh +docker-compose*.yml +.env* +.idea +.vscode diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d346124 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,178 @@ +# =========================================================================== +# docker-laravel — Multi-version PHP + Nginx image for Laravel +# +# Build args: +# PHP_VERSION — 7.4 | 8.0 | 8.1 | 8.2 | 8.3 | 8.4 | 8.5 (default: 8.4) +# NODE_MAJOR — Node.js major version (default: 22) +# =========================================================================== +ARG PHP_VERSION=8.4 +FROM php:${PHP_VERSION}-fpm + +ARG PHP_VERSION + +LABEL maintainer="docker-laravel" +LABEL description="Laravel-optimized PHP-FPM + Nginx image" +LABEL php.version="${PHP_VERSION}" + +WORKDIR /var/www/html + +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=UTC + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# --------------------------------------------------------------------------- +# System dependencies (single layer) +# --------------------------------------------------------------------------- +RUN apt-get update && apt-get install -y --no-install-recommends \ + gnupg gosu curl wget ca-certificates zip unzip git \ + supervisor sqlite3 libcap2-bin python3 pkg-config \ + # GD + libfreetype6-dev libjpeg62-turbo-dev libpng-dev libwebp-dev \ + # PHP extension libs + libonig-dev libxml2-dev libzip-dev libicu-dev libcurl4-openssl-dev \ + # PostgreSQL + libpq-dev \ + # ImageMagick + libmagickwand-dev \ + # Nginx + headers-more module + nginx libnginx-mod-http-headers-more-filter \ + # Database clients + default-mysql-client \ + # Ghostscript (PDF rendering) + ghostscript \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# --------------------------------------------------------------------------- +# PHP extensions +# --------------------------------------------------------------------------- +# GD configure flags changed between PHP 7.x and 8.0 +RUN PHP_MAJOR=$(php -r "echo PHP_MAJOR_VERSION;") && \ + if [ "$PHP_MAJOR" -ge 8 ]; then \ + docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp; \ + else \ + docker-php-ext-configure gd --with-freetype-dir=/usr --with-jpeg-dir=/usr --with-webp-dir=/usr; \ + fi && \ + docker-php-ext-install -j$(nproc) \ + pdo_mysql \ + pdo_pgsql \ + mysqli \ + mbstring \ + exif \ + pcntl \ + bcmath \ + gd \ + sockets \ + zip \ + xml \ + soap \ + intl \ + opcache + +# PECL extensions (fail gracefully for bleeding-edge PHP) +RUN pecl install redis && docker-php-ext-enable redis || echo "NOTICE: redis unavailable for PHP ${PHP_VERSION}" +RUN pecl install ev && docker-php-ext-enable ev || echo "NOTICE: ev unavailable for PHP ${PHP_VERSION}" +RUN pecl install igbinary && docker-php-ext-enable igbinary || echo "NOTICE: igbinary unavailable for PHP ${PHP_VERSION}" +RUN pecl install imagick && docker-php-ext-enable imagick || echo "NOTICE: imagick unavailable for PHP ${PHP_VERSION}" + +# --------------------------------------------------------------------------- +# OPcache configuration (JIT enabled automatically for PHP 8.0+) +# --------------------------------------------------------------------------- +COPY config/opcache.ini /usr/local/etc/php/conf.d/opcache.ini +RUN PHP_MAJOR=$(php -r "echo PHP_MAJOR_VERSION;") && \ + if [ "$PHP_MAJOR" -ge 8 ]; then \ + { echo ""; echo "; JIT (PHP 8.0+)"; echo "opcache.jit=1255"; echo "opcache.jit_buffer_size=128M"; } \ + >> /usr/local/etc/php/conf.d/opcache.ini; \ + echo "JIT enabled for PHP $(php -r 'echo PHP_VERSION;')"; \ + else \ + echo "JIT skipped (PHP $(php -r 'echo PHP_VERSION;') < 8.0)"; \ + fi + +# ImageMagick PDF policy (for spatie/pdf-to-image etc.) +RUN IMGK_CONF=$(find /etc/ImageMagick* -name policy.xml 2>/dev/null | head -1) && \ + if [ -n "$IMGK_CONF" ]; then \ + sed -i 's///g' "$IMGK_CONF" && \ + sed -i '/<\/policymap>/i\ ' "$IMGK_CONF"; \ + fi + +# Custom PHP configuration +COPY config/php.ini /usr/local/etc/php/conf.d/99-custom.ini + +# --------------------------------------------------------------------------- +# Composer +# --------------------------------------------------------------------------- +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +# --------------------------------------------------------------------------- +# Node.js + npm +# --------------------------------------------------------------------------- +ARG NODE_MAJOR=22 +RUN mkdir -p /etc/apt/keyrings && \ + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ + | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \ + > /etc/apt/sources.list.d/nodesource.list && \ + apt-get update && apt-get install -y --no-install-recommends nodejs && \ + npm install -g npm@latest && \ + rm -rf /var/lib/apt/lists/* + +# --------------------------------------------------------------------------- +# Image optimization tools (spatie/image-optimizer) + ffmpeg +# --------------------------------------------------------------------------- +RUN apt-get update && apt-get install -y --no-install-recommends \ + optipng pngquant gifsicle webp libavif-bin ffmpeg \ + nano procps net-tools \ + && npm install -g svgo \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# --------------------------------------------------------------------------- +# Users & directories +# --------------------------------------------------------------------------- +RUN usermod -u 1000 www-data && groupmod -g 1000 www-data + +# PsySH config for artisan tinker +RUN mkdir -p /var/www/.config/psysh && chown -R www-data:www-data /var/www/.config + +# --------------------------------------------------------------------------- +# Nginx configuration +# --------------------------------------------------------------------------- +COPY config/nginx.conf /etc/nginx/nginx.conf +COPY config/default.conf /etc/nginx/sites-available/default +RUN rm -f /etc/nginx/sites-enabled/default && \ + ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default && \ + rm -f /etc/nginx/conf.d/default.conf + +# --------------------------------------------------------------------------- +# Supervisor configuration +# conf.d/ — core programs (php-fpm, nginx), override via volume mount +# laravel.d/ — generated at boot from ENABLE_* env vars +# custom.d/ — mount your own .conf files here +# --------------------------------------------------------------------------- +COPY config/supervisord.conf /etc/supervisor/supervisord.conf +COPY config/supervisor/php-fpm.conf /etc/supervisor/conf.d/php-fpm.conf +COPY config/supervisor/nginx.conf /etc/supervisor/conf.d/nginx.conf +RUN mkdir -p /etc/supervisor/conf.d /etc/supervisor/laravel.d /etc/supervisor/custom.d \ + /var/log/supervisor /var/log/nginx /var/log/php + +# --------------------------------------------------------------------------- +# Startup script +# --------------------------------------------------------------------------- +COPY scripts/start-container /usr/local/bin/start-container +RUN chmod +x /usr/local/bin/start-container + +# Composer cache +RUN mkdir -p /.composer && chmod 0777 /.composer + +# --------------------------------------------------------------------------- +# Environment variables for optional Laravel services +# Set to "true" to enable at container start +# --------------------------------------------------------------------------- +ENV ENABLE_QUEUE=false +ENV ENABLE_SCHEDULER=false +ENV ENABLE_HORIZON=false +ENV ENABLE_LARAVEL_PERMS=0 + +EXPOSE 80 + +ENTRYPOINT ["start-container"] + diff --git a/README.md b/README.md new file mode 100644 index 0000000..75d5eae --- /dev/null +++ b/README.md @@ -0,0 +1,149 @@ +# docker-laravel + +Multi-version PHP + Nginx Docker image optimized for Laravel (9 – 13). + +The image provides **PHP-FPM, Nginx, Composer, Node.js, Bun**, and common PHP extensions out of the box. +It does **not** contain any Laravel code — mount your project at `/var/www/html`. + +## Supported PHP Versions + +| Tag | PHP | Laravel | +|-----|-----|---------| +| `php7.4` | 7.4 | Legacy (< 9) | +| `php8.0` | 8.0 | 9 | +| `php8.1` | 8.1 | 9, 10 | +| `php8.2` | 8.2 | 9, 10, 11, 12 | +| `php8.3` | 8.3 | 10, 11, 12, 13 | +| `php8.4` | 8.4 | 11, 12, 13 | +| `php8.5` | 8.5 | 12, 13 | +| `latest` | 8.4 | (alias) | + +## Quick Start + +```bash +# Build a single version +docker build --build-arg PHP_VERSION=8.4 -t docker-laravel:php8.4 . + +# Build all versions +./scripts/build.sh + +# Build only specific PHP versions +./scripts/build.sh 8.4 8.5 + +# Build all versions with buildx bake +docker buildx bake + +# Build only actively-supported PHP versions +docker buildx bake active +``` + +## plug-n-pray 🙏 + +Generate a full Docker Compose boilerplate for any Laravel project: + +```bash +# From your Laravel project root — one-liner: +curl -fsSL https://raw.githubusercontent.com/blax-software/docker-laravel/main/scripts/plug-n-pray.sh | bash + +# With options: +./plug-n-pray.sh --php=8.4 --name=my-app --host=my-app.localhost --horizon + +# Or via artisan (requires blax-software/laravel-workkit): +php artisan workkit:plug-n-pray +php artisan workkit:plug-n-pray --php=8.5 --horizon --no-mysql +``` + +See [docs/examples.md](docs/examples.md) for full usage examples. + +## Usage in docker-compose.yml + +```yaml +services: + app: + image: docker-laravel:php8.4 + volumes: + - ./:/var/www/html + ports: + - "80:80" + environment: + ENABLE_QUEUE: "true" + ENABLE_SCHEDULER: "true" + ENABLE_HORIZON: "false" + ENABLE_LARAVEL_PERMS: "1" +``` + +## Build Args + +| Arg | Default | Description | +|-----|---------|-------------| +| `PHP_VERSION` | `8.4` | PHP version (7.4, 8.0 – 8.5) | +| `NODE_MAJOR` | `22` | Node.js major version | + +## Runtime Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `ENABLE_QUEUE` | `false` | Start `artisan queue:work` via supervisor | +| `ENABLE_SCHEDULER` | `false` | Start `artisan schedule:work` via supervisor | +| `ENABLE_HORIZON` | `false` | Start `artisan horizon` via supervisor | +| `ENABLE_LARAVEL_PERMS` | `0` | Fix `storage/` and `bootstrap/cache/` permissions on boot | + +## What's Included + +- **PHP-FPM** with extensions: pdo_mysql, pdo_pgsql, mysqli, mbstring, exif, pcntl, bcmath, gd (freetype + jpeg + webp), sockets, zip, xml, soap, intl, opcache +- **PECL**: redis, ev, igbinary, imagick (graceful fallback if unavailable for a PHP version) +- **OPcache** with JIT auto-enabled on PHP 8.0+ +- **Nginx** with headers-more module, optimized for Laravel +- **Composer** (latest) +- **Node.js** + npm +- **Image optimizers**: optipng, pngquant, gifsicle, webp, avif, svgo +- **ffmpeg**, **ghostscript**, **MySQL client** + +## Architecture + +``` +start-container (entrypoint) + └─ supervisord + ├─ php-fpm (always) + ├─ nginx (always) + ├─ queue.conf (if ENABLE_QUEUE=true) + ├─ scheduler.conf (if ENABLE_SCHEDULER=true) + └─ horizon.conf (if ENABLE_HORIZON=true) +``` + +Optional supervisor configs are generated at runtime in `/etc/supervisor/laravel.d/`. + +## Customizing Supervisor Programs + +Every supervisor program lives in its own `.conf` file across three include directories: + +| Directory | Purpose | How to customize | +|-----------|---------|------------------| +| `/etc/supervisor/conf.d/` | Core services (php-fpm, nginx) | Mount a replacement file to override | +| `/etc/supervisor/laravel.d/` | Queue, scheduler, horizon (auto-generated from `ENABLE_*` env vars) | Use env vars, or disable them and mount your own | +| `/etc/supervisor/custom.d/` | Empty — for your own programs | Mount a directory or individual files | + +**Examples:** + +```yaml +services: + app: + image: blaxsoftware/laravel:php8.4 + volumes: + - ./:/var/www/html + # Override php-fpm config (e.g. change pool settings) + - ./docker/php-fpm.conf:/etc/supervisor/conf.d/php-fpm.conf + # Add custom programs (e.g. reverb, octane, custom workers) + - ./docker/supervisor/:/etc/supervisor/custom.d/ + environment: + ENABLE_QUEUE: "true" +``` + +To **disable a core service** (e.g. nginx), mount an override with `autostart=false`: + +```ini +; my-nginx-override.conf +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +autostart=false +``` diff --git a/config/default.conf b/config/default.conf new file mode 100644 index 0000000..6d6eb3c --- /dev/null +++ b/config/default.conf @@ -0,0 +1,34 @@ +server { + listen 80; + + index index.php index.html; + root /var/www/html/public; + + # Log to Docker (avoid writing to container FS) + access_log /dev/stdout; + error_log /dev/stderr warn; + + client_max_body_size 500M; + + # Add X-Forwarded-Proto header + 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 X-Forwarded-Host $host; + proxy_set_header Host $host; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ [^/]\.php(/|$) { + fastcgi_split_path_info ^(.+?\.php)(/.*)$; + fastcgi_param HTTP_PROXY ""; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param PHP_SELF $fastcgi_script_name$fastcgi_path_info; + fastcgi_param SERVER_NAME $host; + fastcgi_pass 127.0.0.1:9000; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + } +} diff --git a/config/nginx.conf b/config/nginx.conf new file mode 100644 index 0000000..1769032 --- /dev/null +++ b/config/nginx.conf @@ -0,0 +1,79 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; + +# Send logs to Docker +error_log /dev/stderr warn; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 4096; + multi_accept on; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + server_tokens off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Custom headers + more_set_headers "Server: Laravel Proxy"; + more_set_headers "X-Powered-By: Laravel Proxy"; + + # Security headers (safe behind Traefik) + more_set_headers "X-Frame-Options: SAMEORIGIN"; + more_set_headers "X-Content-Type-Options: nosniff"; + more_set_headers "Referrer-Policy: no-referrer-when-downgrade"; + more_set_headers "X-XSS-Protection: 1; mode=block"; + more_set_headers "Permissions-Policy: geolocation=(), microphone=(), camera=()"; + + # Gzip + gzip on; + gzip_comp_level 5; + gzip_min_length 256; + gzip_vary on; + gzip_types + text/plain text/css text/xml application/json application/javascript + application/x-javascript application/xml application/xml+rss + font/ttf font/otf image/svg+xml; + + # Real IP from Traefik + set_real_ip_from 0.0.0.0/0; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + # Buffers & timeouts for Laravel + client_max_body_size 50M; + client_body_buffer_size 128k; + client_header_timeout 30s; + client_body_timeout 30s; + send_timeout 30s; + + # FastCGI settings for PHP (Laravel) + fastcgi_read_timeout 300; + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; + + # Logging (to Docker) + access_log /dev/stdout; + + # Cache static assets aggressively (ideal for Laravel mix/vite builds) + map $sent_http_content_type $static_expires { + default off; + ~*image/ 30d; + ~*font/ 30d; + ~*text/css 30d; + ~*javascript 30d; + } + + proxy_headers_hash_max_size 1024; + proxy_headers_hash_bucket_size 128; + + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} diff --git a/config/opcache.ini b/config/opcache.ini new file mode 100644 index 0000000..54df7e4 --- /dev/null +++ b/config/opcache.ini @@ -0,0 +1,17 @@ +; OPcache settings for Laravel (PHP 7.4+) +; JIT settings are appended automatically for PHP 8.0+ during build. +opcache.enable=1 +opcache.enable_cli=1 + +; Memory — generous for large codebases +opcache.memory_consumption=256 +opcache.interned_strings_buffer=32 +opcache.max_accelerated_files=20000 + +; File validation — safe for development; disable for production +; (set validate_timestamps=0 and revalidate_freq=0 in production) +opcache.validate_timestamps=1 +opcache.revalidate_freq=2 + +; Optimisation +opcache.optimization_level=0x7FFEBFFF diff --git a/config/php.ini b/config/php.ini new file mode 100644 index 0000000..727699b --- /dev/null +++ b/config/php.ini @@ -0,0 +1,22 @@ +[PHP] +post_max_size = 2G +upload_max_filesize = 2G +memory_limit = 500M +variables_order = EGPCS +fastcgi.logging = Off + +; Error Reporting +display_errors = Off +log_errors = On +error_log = /var/log/php/error.log +error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT + +; OPcache Settings +; opcache.enable = 1 +; opcache.memory_consumption = 128 +; opcache.interned_strings_buffer = 16 +; opcache.max_accelerated_files = 10000 +; opcache.validate_timestamps = 1 +; opcache.revalidate_freq = 2 +; opcache.fast_shutdown = 1 +; opcache.enable_cli = 1 diff --git a/config/supervisor/nginx.conf b/config/supervisor/nginx.conf new file mode 100644 index 0000000..3c7451e --- /dev/null +++ b/config/supervisor/nginx.conf @@ -0,0 +1,11 @@ +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +autostart=true +autorestart=true +priority=10 +startsecs=0 +startretries=10 +stdout_logfile=/proc/1/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/proc/1/fd/2 +stderr_logfile_maxbytes=0 diff --git a/config/supervisor/php-fpm.conf b/config/supervisor/php-fpm.conf new file mode 100644 index 0000000..af750e1 --- /dev/null +++ b/config/supervisor/php-fpm.conf @@ -0,0 +1,9 @@ +[program:php-fpm] +command=/usr/local/sbin/php-fpm --nodaemonize +autostart=true +autorestart=true +priority=5 +stdout_logfile=/proc/1/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/proc/1/fd/2 +stderr_logfile_maxbytes=0 diff --git a/config/supervisord.conf b/config/supervisord.conf new file mode 100644 index 0000000..df049fd --- /dev/null +++ b/config/supervisord.conf @@ -0,0 +1,35 @@ +[supervisord] +nodaemon=true +user=root +logfile=/proc/1/fd/1 +logfile_maxbytes=0 +pidfile=/var/run/supervisord.pid +loglevel=info + +[unix_http_server] +file=/var/run/supervisor.sock +chmod=0700 + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock + +; ========================================================================== +; Include directories (order matters — later files can't override earlier ones, +; but you can mount over individual files in conf.d/) +; +; conf.d/ — Core services (php-fpm, nginx). Override by mounting a +; replacement file, e.g.: +; -v ./my-php-fpm.conf:/etc/supervisor/conf.d/php-fpm.conf +; +; laravel.d/ — Auto-generated at boot from ENABLE_* env vars. +; To use your own instead, set the env var to "false" and +; mount your config into custom.d/ or laravel.d/. +; +; custom.d/ — Mount any extra .conf files here for your own programs. +; -v ./my-programs/:/etc/supervisor/custom.d/ +; ========================================================================== +[include] +files = /etc/supervisor/conf.d/*.conf /etc/supervisor/laravel.d/*.conf /etc/supervisor/custom.d/*.conf diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 0000000..130b898 --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,123 @@ +# =========================================================================== +# docker-laravel — Multi-version build matrix +# +# Usage: +# docker buildx bake # build all versions +# docker buildx bake php-84 # build PHP 8.4 only +# docker buildx bake --set "*.platform=linux/amd64,linux/arm64" +# +# Override registry: +# REGISTRY=ghcr.io/myorg IMAGE_NAME=docker-laravel docker buildx bake +# =========================================================================== + +variable "REGISTRY" { + default = "" +} + +variable "IMAGE_NAME" { + default = "docker-laravel" +} + +variable "NODE_MAJOR" { + default = "22" +} + +function "tag" { + params = [php_version] + result = REGISTRY != "" ? "${REGISTRY}/${IMAGE_NAME}:php${php_version}" : "${IMAGE_NAME}:php${php_version}" +} + +function "latest_tag" { + params = [] + result = REGISTRY != "" ? "${REGISTRY}/${IMAGE_NAME}:latest" : "${IMAGE_NAME}:latest" +} + +# --------------------------------------------------------------------------- +# Individual targets +# --------------------------------------------------------------------------- + +target "php-74" { + context = "." + dockerfile = "Dockerfile" + args = { + PHP_VERSION = "7.4" + NODE_MAJOR = "${NODE_MAJOR}" + } + tags = [tag("7.4")] +} + +target "php-80" { + context = "." + dockerfile = "Dockerfile" + args = { + PHP_VERSION = "8.0" + NODE_MAJOR = "${NODE_MAJOR}" + } + tags = [tag("8.0")] +} + +target "php-81" { + context = "." + dockerfile = "Dockerfile" + args = { + PHP_VERSION = "8.1" + NODE_MAJOR = "${NODE_MAJOR}" + } + tags = [tag("8.1")] +} + +target "php-82" { + context = "." + dockerfile = "Dockerfile" + args = { + PHP_VERSION = "8.2" + NODE_MAJOR = "${NODE_MAJOR}" + } + tags = [tag("8.2")] +} + +target "php-83" { + context = "." + dockerfile = "Dockerfile" + args = { + PHP_VERSION = "8.3" + NODE_MAJOR = "${NODE_MAJOR}" + } + tags = [tag("8.3")] +} + +target "php-84" { + context = "." + dockerfile = "Dockerfile" + args = { + PHP_VERSION = "8.4" + NODE_MAJOR = "${NODE_MAJOR}" + } + tags = [tag("8.4"), latest_tag()] +} + +target "php-85" { + context = "." + dockerfile = "Dockerfile" + args = { + PHP_VERSION = "8.5" + NODE_MAJOR = "${NODE_MAJOR}" + } + tags = [tag("8.5")] +} + +# --------------------------------------------------------------------------- +# Groups +# --------------------------------------------------------------------------- + +group "default" { + targets = ["php-74", "php-80", "php-81", "php-82", "php-83", "php-84", "php-85"] +} + +group "active" { + targets = ["php-82", "php-83", "php-84", "php-85"] +} + +group "legacy" { + targets = ["php-74", "php-80", "php-81"] +} diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..bb5850c --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,210 @@ +# Examples + +## Quick Start with plug-n-pray + +The fastest way to Dockerize any Laravel project: + +```bash +# From your Laravel project root: +curl -fsSL https://raw.githubusercontent.com/blax-software/docker-laravel/main/scripts/plug-n-pray.sh | bash + +# Or with options: +curl -fsSL https://raw.githubusercontent.com/blax-software/docker-laravel/main/scripts/plug-n-pray.sh | bash -s -- \ + --php=8.4 \ + --name=my-app \ + --host=my-app.localhost +``` + +Or if you have `blax-software/laravel-workkit` installed: + +```bash +php artisan workkit:plug-n-pray +``` + +This generates: +- `docker-compose.yml` — Full stack with app, MySQL, Redis, and Traefik +- `.env.docker` — Database/Redis connection values to merge into `.env` +- `docker/supervisor/` — Directory for custom supervisor programs + +Then: +```bash +docker network create web # once per machine +docker compose up -d +``` + +--- + +## Example: Minimal API (no frontend tooling) + +```yaml +services: + app: + image: blaxsoftware/laravel:php8.4 + volumes: + - ./:/var/www/html + ports: + - "80:80" + environment: + ENABLE_QUEUE: "true" + ENABLE_SCHEDULER: "true" +``` + +--- + +## Example: Full Stack with Traefik + +Assumes Traefik is already running on the `web` network. + +```yaml +networks: + web: + external: true + internal: + +volumes: + mysql-data: + redis-data: + +services: + app: + image: blaxsoftware/laravel:php8.4 + volumes: + - ./:/var/www/html + - ./docker/supervisor:/etc/supervisor/custom.d + environment: + ENABLE_QUEUE: "true" + ENABLE_SCHEDULER: "true" + ENABLE_LARAVEL_PERMS: "1" + networks: + - web + - internal + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + labels: + - traefik.enable=true + - traefik.docker.network=web + - traefik.http.routers.my-app.rule=Host(`my-app.localhost`) + - traefik.http.routers.my-app.entrypoints=web + - traefik.http.routers.my-app.service=my-app-http + - traefik.http.services.my-app-http.loadbalancer.server.port=80 + + mysql: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: secret + MYSQL_DATABASE: my_app + volumes: + - mysql-data:/var/lib/mysql + networks: + - internal + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-psecret"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + volumes: + - redis-data:/data + networks: + - internal + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 +``` + +--- + +## Example: With Horizon + +```yaml +services: + app: + image: blaxsoftware/laravel:php8.4 + volumes: + - ./:/var/www/html + environment: + ENABLE_HORIZON: "true" # replaces basic queue worker + ENABLE_SCHEDULER: "true" +``` + +--- + +## Example: Custom Supervisor Programs + +Mount your own `.conf` files into `/etc/supervisor/custom.d/`: + +```yaml +services: + app: + image: blaxsoftware/laravel:php8.4 + volumes: + - ./:/var/www/html + - ./docker/supervisor/reverb.conf:/etc/supervisor/custom.d/reverb.conf +``` + +`docker/supervisor/reverb.conf`: +```ini +[program:reverb] +command=/usr/local/bin/php -d variables_order=EGPCS /var/www/html/artisan reverb:start --host=0.0.0.0 --port=8080 +autostart=true +autorestart=true +user=www-data +priority=25 +startsecs=5 +stdout_logfile=/proc/1/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/proc/1/fd/2 +stderr_logfile_maxbytes=0 +``` + +--- + +## Example: Override PHP-FPM + +```yaml +volumes: + - ./docker/php-fpm.conf:/etc/supervisor/conf.d/php-fpm.conf +``` + +--- + +## Example: Multiple PHP Versions in One Compose + +```yaml +services: + app-legacy: + image: blaxsoftware/laravel:php8.1 + volumes: + - ../legacy-app:/var/www/html + + app-new: + image: blaxsoftware/laravel:php8.4 + volumes: + - ../new-app:/var/www/html +``` + +--- + +## plug-n-pray.sh Options + +| Flag | Default | Description | +|------|---------|-------------| +| `--php=VERSION` | `8.4` | PHP version | +| `--name=NAME` | directory name | Project name (used for container names & Traefik router) | +| `--host=HOST` | `NAME.localhost` | Traefik hostname | +| `--db=NAME` | project name | MySQL database name | +| `--db-pass=PASS` | `secret` | MySQL root password | +| `--image=IMAGE` | `blaxsoftware/laravel` | Docker image | +| `--no-queue` | — | Disable queue worker | +| `--no-scheduler` | — | Disable scheduler | +| `--horizon` | — | Enable Horizon (auto-disables basic queue) | +| `--no-redis` | — | Skip Redis service | +| `--no-mysql` | — | Skip MySQL service | +| `--force` | — | Overwrite existing files | diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..e9be95b --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# =========================================================================== +# build.sh — Build all PHP version images with full Laravel tag matrix +# +# Usage: +# ./build.sh # build all versions +# ./build.sh 8.4 # build only PHP 8.4 +# ./build.sh 8.3 8.4 # build PHP 8.3 + 8.4 +# +# Environment: +# IMAGE_NAME — image name (default: docker-laravel) +# NODE_MAJOR — Node.js major ver (default: 22) +# PLATFORM — e.g. linux/amd64 (default: current platform) +# =========================================================================== +set -euo pipefail + +IMAGE_NAME="${IMAGE_NAME:-docker-laravel}" +NODE_MAJOR="${NODE_MAJOR:-22}" + +# --------------------------------------------------------------------------- +# PHP → Laravel version mapping +# --------------------------------------------------------------------------- +declare -A PHP_LARAVEL_MAP=( + ["7.4"]="" + ["8.0"]="9" + ["8.1"]="9 10" + ["8.2"]="9 10 11 12" + ["8.3"]="10 11 12 13" + ["8.4"]="11 12 13" + ["8.5"]="12 13" +) + +# Recommended (highest) PHP version per Laravel version → gets the bare `laravelN` tag +declare -A LARAVEL_RECOMMENDED_PHP=( + ["9"]="8.1" + ["10"]="8.3" + ["11"]="8.4" + ["12"]="8.5" + ["13"]="8.5" +) + +# Which PHP version gets the `latest` tag +LATEST_PHP="8.4" + +ALL_PHP_VERSIONS=("7.4" "8.0" "8.1" "8.2" "8.3" "8.4" "8.5") + +# --------------------------------------------------------------------------- +# Determine which versions to build +# --------------------------------------------------------------------------- +if [ $# -gt 0 ]; then + BUILD_VERSIONS=("$@") +else + BUILD_VERSIONS=("${ALL_PHP_VERSIONS[@]}") +fi + +# Validate requested versions +for v in "${BUILD_VERSIONS[@]}"; do + if [[ ! -v "PHP_LARAVEL_MAP[$v]" ]]; then + echo "ERROR: Unknown PHP version: $v" + echo "Available: ${ALL_PHP_VERSIONS[*]}" + exit 1 + fi +done + +# --------------------------------------------------------------------------- +# Build +# --------------------------------------------------------------------------- +TOTAL=${#BUILD_VERSIONS[@]} +CURRENT=0 +FAILED=() + +PLATFORM_ARG="" +if [ -n "${PLATFORM:-}" ]; then + PLATFORM_ARG="--platform=${PLATFORM}" +fi + +for PHP_VERSION in "${BUILD_VERSIONS[@]}"; do + CURRENT=$((CURRENT + 1)) + + # Collect all tags for this PHP version + TAGS=() + TAGS+=("${IMAGE_NAME}:php${PHP_VERSION}") + + # Laravel combo tags: laravel12-php8.4, etc. + LARAVEL_VERSIONS="${PHP_LARAVEL_MAP[$PHP_VERSION]}" + for LV in $LARAVEL_VERSIONS; do + TAGS+=("${IMAGE_NAME}:laravel${LV}-php${PHP_VERSION}") + + # Convenience bare tag: laravelN → recommended PHP + if [ "${LARAVEL_RECOMMENDED_PHP[$LV]}" = "$PHP_VERSION" ]; then + TAGS+=("${IMAGE_NAME}:laravel${LV}") + fi + done + + # latest tag + if [ "$PHP_VERSION" = "$LATEST_PHP" ]; then + TAGS+=("${IMAGE_NAME}:latest") + fi + + TAG_ARGS="" + TAG_LIST="" + for T in "${TAGS[@]}"; do + TAG_ARGS="${TAG_ARGS} -t ${T}" + TAG_LIST="${TAG_LIST} ${T}\n" + done + + echo "" + echo "===========================================" + echo " [${CURRENT}/${TOTAL}] Building PHP ${PHP_VERSION}" + echo "===========================================" + echo -e "Tags:\n${TAG_LIST}" + + if docker build ${PLATFORM_ARG} \ + --build-arg PHP_VERSION="${PHP_VERSION}" \ + --build-arg NODE_MAJOR="${NODE_MAJOR}" \ + ${TAG_ARGS} \ + . ; then + echo "[${CURRENT}/${TOTAL}] PHP ${PHP_VERSION} — OK" + else + echo "[${CURRENT}/${TOTAL}] PHP ${PHP_VERSION} — FAILED" + FAILED+=("$PHP_VERSION") + fi +done + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +echo "" +echo "===========================================" +echo " Build complete" +echo "===========================================" +echo "Succeeded: $((TOTAL - ${#FAILED[@]}))/${TOTAL}" + +if [ ${#FAILED[@]} -gt 0 ]; then + echo "Failed: ${FAILED[*]}" + exit 1 +fi diff --git a/scripts/plug-n-pray.sh b/scripts/plug-n-pray.sh new file mode 100755 index 0000000..1e4e23c --- /dev/null +++ b/scripts/plug-n-pray.sh @@ -0,0 +1,384 @@ +#!/usr/bin/env bash +# =========================================================================== +# plug-n-pray.sh — Generate a boilerplate Docker setup for any Laravel project +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/blax-software/docker-laravel/main/scripts/plug-n-pray.sh | bash +# # or locally: +# ./plug-n-pray.sh +# ./plug-n-pray.sh --php=8.4 --name=my-app --host=my-app.localhost +# +# Assumes Traefik is already running on the "web" network. +# +# What it creates: +# docker-compose.yml — App + MySQL + Redis (with Traefik labels) +# .env.docker — Docker-specific env vars +# docker/supervisor/ — Empty custom supervisor dir +# =========================================================================== +set -euo pipefail + +# --------------------------------------------------------------------------- +# Defaults +# --------------------------------------------------------------------------- +PHP_VERSION="8.4" +PROJECT_NAME="" +TRAEFIK_HOST="" +DB_NAME="" +DB_PASSWORD="secret" +IMAGE="blaxsoftware/laravel" +ENABLE_QUEUE="true" +ENABLE_SCHEDULER="true" +ENABLE_HORIZON="false" +ENABLE_REDIS="true" +ENABLE_MYSQL="true" +ENABLE_WEBSOCKET="false" +WEBSOCKET_PORT="6001" +FORCE="false" + +# --------------------------------------------------------------------------- +# Parse args +# --------------------------------------------------------------------------- +for arg in "$@"; do + case $arg in + --php=*) PHP_VERSION="${arg#*=}" ;; + --name=*) PROJECT_NAME="${arg#*=}" ;; + --host=*) TRAEFIK_HOST="${arg#*=}" ;; + --db=*) DB_NAME="${arg#*=}" ;; + --db-pass=*) DB_PASSWORD="${arg#*=}" ;; + --image=*) IMAGE="${arg#*=}" ;; + --no-queue) ENABLE_QUEUE="false" ;; + --no-scheduler) ENABLE_SCHEDULER="false" ;; + --horizon) ENABLE_HORIZON="true" ;; + --no-redis) ENABLE_REDIS="false" ;; + --no-mysql) ENABLE_MYSQL="false" ;; + --websocket) ENABLE_WEBSOCKET="true" ;; + --websocket-port=*) WEBSOCKET_PORT="${arg#*=}" ;; + --force) FORCE="true" ;; + --help|-h) + echo "Usage: plug-n-pray.sh [OPTIONS]" + echo "" + echo "Options:" + echo " --php=VERSION PHP version (default: 8.4)" + echo " --name=NAME Project/compose name (default: directory name)" + echo " --host=HOST Traefik hostname (default: NAME.localhost)" + echo " --db=NAME Database name (default: PROJECT_NAME)" + echo " --db-pass=PASS Database password (default: secret)" + echo " --image=IMAGE Docker image (default: blaxsoftware/laravel)" + echo " --no-queue Disable queue worker" + echo " --no-scheduler Disable scheduler" + echo " --horizon Enable Horizon (disables basic queue)" + echo " --no-redis Skip Redis service" + echo " --no-mysql Skip MySQL service" + echo " --websocket Enable WebSocket server (blax/laravel-websockets)" + echo " --websocket-port=N WebSocket port (default: 6001)" + echo " --force Overwrite existing files" + echo " --help Show this help" + exit 0 + ;; + *) + echo "Unknown option: $arg (try --help)" + exit 1 + ;; + esac +done + +# Derive defaults from current directory +if [ -z "$PROJECT_NAME" ]; then + PROJECT_NAME="$(basename "$(pwd)")" + # Sanitize: lowercase, replace non-alnum with dash + PROJECT_NAME="$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')" +fi + +if [ -z "$TRAEFIK_HOST" ]; then + TRAEFIK_HOST="${PROJECT_NAME}.localhost" +fi + +if [ -z "$DB_NAME" ]; then + DB_NAME="$(echo "$PROJECT_NAME" | sed 's/-/_/g')" +fi + +# If horizon is on, disable basic queue +if [ "$ENABLE_HORIZON" = "true" ]; then + ENABLE_QUEUE="false" +fi + +# --------------------------------------------------------------------------- +# Safety check +# --------------------------------------------------------------------------- +if [ "$FORCE" != "true" ] && [ -f "docker-compose.yml" ]; then + echo "docker-compose.yml already exists. Use --force to overwrite." + exit 1 +fi + +echo "==========================================" +echo " plug-n-pray 🙏" +echo "==========================================" +echo " Project: $PROJECT_NAME" +echo " PHP: $PHP_VERSION" +echo " Image: ${IMAGE}:php${PHP_VERSION}" +echo " Traefik: $TRAEFIK_HOST" +echo " Database: ${ENABLE_MYSQL:+MySQL ($DB_NAME)}${ENABLE_MYSQL:+}$([ "$ENABLE_MYSQL" = "false" ] && echo "disabled")" +echo " Redis: $ENABLE_REDIS" +echo " Queue: $ENABLE_QUEUE" +echo " Scheduler: $ENABLE_SCHEDULER" +echo " Horizon: $ENABLE_HORIZON" +echo " WebSocket: ${ENABLE_WEBSOCKET}$([ "$ENABLE_WEBSOCKET" = "true" ] && echo " (port ${WEBSOCKET_PORT})")" +echo "==========================================" + +# --------------------------------------------------------------------------- +# Create directory for custom supervisor configs +# --------------------------------------------------------------------------- +mkdir -p docker/supervisor + +# --------------------------------------------------------------------------- +# Generate docker-compose.yml +# --------------------------------------------------------------------------- +cat > docker-compose.yml <> docker-compose.yml +fi + +# --- Services --- +cat >> docker-compose.yml <> docker-compose.yml <> docker-compose.yml <> docker-compose.yml <> docker-compose.yml <> docker-compose.yml < docker/supervisor/websocket.conf < .env.docker <> .env.docker <> .env.docker <> .env.docker </dev/null || echo 'n/a')" +echo "Composer: $(composer --version --no-ansi 2>/dev/null | head -1 || echo 'n/a')" +echo "==========================================" + +start_ts=$(date +%s) + +# --------------------------------------------------------------------------- +# 1) Writable directories +# --------------------------------------------------------------------------- +echo "[1/5] Preparing writable dirs..." + +mkdir -p /.composer && chmod 0777 /.composer 2>/dev/null || true + +if [ "${ENABLE_LARAVEL_PERMS:-0}" = "1" ]; then + echo " ENABLE_LARAVEL_PERMS=1 — applying targeted writable-dir fixes" + for p in /var/www/html/storage /var/www/html/bootstrap/cache; do + if [ -d "$p" ]; then + chmod ug+rwX "$p" 2>/dev/null || true + else + echo " WARN: $p does not exist" + fi + done +else + echo " Skipping Laravel chmod (set ENABLE_LARAVEL_PERMS=1 to enable)" +fi + +mkdir -p /var/log/supervisor /var/log/nginx /var/log/php + +# --------------------------------------------------------------------------- +# 2) Generate optional supervisor programs based on ENABLE_* env vars +# --------------------------------------------------------------------------- +echo "[2/5] Configuring services..." + +echo " Core programs (conf.d/):" +for f in /etc/supervisor/conf.d/*.conf; do + [ -f "$f" ] && echo " $(basename "$f")" +done + +LARAVEL_D="/etc/supervisor/laravel.d" +rm -f "${LARAVEL_D}"/*.conf 2>/dev/null || true + +if [ "${ENABLE_QUEUE:-false}" = "true" ]; then + echo " + queue worker enabled" + cat > "${LARAVEL_D}/queue.conf" <<'CONF' +[program:queue] +command=/usr/local/bin/php -d variables_order=EGPCS /var/www/html/artisan queue:work --tries=3 --sleep=5 --timeout=600 --max-jobs=500 --max-time=3600 +autostart=true +autorestart=true +user=www-data +priority=20 +startsecs=5 +stdout_logfile=/proc/1/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/proc/1/fd/2 +stderr_logfile_maxbytes=0 +CONF +fi + +if [ "${ENABLE_SCHEDULER:-false}" = "true" ]; then + echo " + scheduler enabled" + cat > "${LARAVEL_D}/scheduler.conf" <<'CONF' +[program:scheduler] +command=/usr/local/bin/php -d variables_order=EGPCS /var/www/html/artisan schedule:work +autostart=true +autorestart=true +user=www-data +priority=20 +startsecs=5 +stdout_logfile=/proc/1/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/proc/1/fd/2 +stderr_logfile_maxbytes=0 +CONF +fi + +if [ "${ENABLE_HORIZON:-false}" = "true" ]; then + echo " + Horizon enabled" + cat > "${LARAVEL_D}/horizon.conf" <<'CONF' +[program:horizon] +command=/usr/local/bin/php -d variables_order=EGPCS /var/www/html/artisan horizon +autostart=true +autorestart=true +user=www-data +priority=20 +startsecs=5 +stdout_logfile=/proc/1/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/proc/1/fd/2 +stderr_logfile_maxbytes=0 +CONF +fi + +# Report custom programs +CUSTOM_COUNT=$(find /etc/supervisor/custom.d/ -name '*.conf' 2>/dev/null | wc -l) +if [ "$CUSTOM_COUNT" -gt 0 ]; then + echo " Custom programs (custom.d/):" + for f in /etc/supervisor/custom.d/*.conf; do + [ -f "$f" ] && echo " $(basename "$f")" + done +fi + +# --------------------------------------------------------------------------- +# 3) Config tests +# --------------------------------------------------------------------------- +echo "[3/5] Testing configs..." + +php-fpm -t 2>&1 || echo " PHP-FPM config test failed (continuing)" +nginx -t 2>&1 || echo " Nginx config test failed (continuing)" + +# --------------------------------------------------------------------------- +# 4) Diagnostics +# --------------------------------------------------------------------------- +echo "[4/5] Environment" +echo " APP_ENV=${APP_ENV:-}" +echo " APP_DEBUG=${APP_DEBUG:-}" +echo " ENABLE_QUEUE=${ENABLE_QUEUE:-false}" +echo " ENABLE_SCHEDULER=${ENABLE_SCHEDULER:-false}" +echo " ENABLE_HORIZON=${ENABLE_HORIZON:-false}" + +end_ts=$(date +%s) +echo " Preflight took $((end_ts - start_ts))s" + +# --------------------------------------------------------------------------- +# 5) Launch +# --------------------------------------------------------------------------- +if [ $# -gt 0 ]; then + echo "[5/5] Running ad-hoc command: $*" + php-fpm -D + sleep 1 + nginx + exec gosu 1000 "$@" +else + echo "[5/5] Starting Supervisord..." + echo "==========================================" + exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf +fi