Initial commit: multi-version PHP+Nginx Docker image for Laravel

This commit is contained in:
Fabian @ Blax Software 2026-04-15 09:57:37 +02:00
commit 7ccb8b94fe
16 changed files with 1679 additions and 0 deletions

13
.dockerignore Normal file
View File

@ -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

178
Dockerfile Normal file
View File

@ -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/<policy domain="coder" rights="none" pattern="PDF" \/>/<policy domain="coder" rights="read|write" pattern="PDF" \/>/g' "$IMGK_CONF" && \
sed -i '/<\/policymap>/i\ <policy domain="coder" rights="read|write" pattern="LABEL" />' "$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"]

149
README.md Normal file
View File

@ -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
```

34
config/default.conf Normal file
View File

@ -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;
}
}

79
config/nginx.conf Normal file
View File

@ -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/*;
}

17
config/opcache.ini Normal file
View File

@ -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

22
config/php.ini Normal file
View File

@ -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

View File

@ -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

View File

@ -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

35
config/supervisord.conf Normal file
View File

@ -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

123
docker-bake.hcl Normal file
View File

@ -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"]
}

210
docs/examples.md Normal file
View File

@ -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 |

137
scripts/build.sh Executable file
View File

@ -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

384
scripts/plug-n-pray.sh Executable file
View File

@ -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 <<YAML
# ===========================================================================
# Generated by plug-n-pray.sh — $(date +%Y-%m-%d)
# Image: ${IMAGE}:php${PHP_VERSION}
# ===========================================================================
networks:
web:
external: true
internal:
driver: bridge
YAML
# --- Volumes ---
VOLUMES_SECTION=""
if [ "$ENABLE_MYSQL" = "true" ]; then
VOLUMES_SECTION="volumes:
mysql-data:
"
fi
if [ "$ENABLE_REDIS" = "true" ]; then
if [ -n "$VOLUMES_SECTION" ]; then
VOLUMES_SECTION="${VOLUMES_SECTION} redis-data:
"
else
VOLUMES_SECTION="volumes:
redis-data:
"
fi
fi
if [ -n "$VOLUMES_SECTION" ]; then
echo "$VOLUMES_SECTION" >> docker-compose.yml
fi
# --- Services ---
cat >> docker-compose.yml <<YAML
services:
# -------------------------------------------------------------------------
# App (PHP-FPM + Nginx)
# -------------------------------------------------------------------------
app:
image: ${IMAGE}:php${PHP_VERSION}
container_name: ${PROJECT_NAME}-app
restart: unless-stopped
working_dir: /var/www/html
volumes:
- ./:/var/www/html
- ./docker/supervisor:/etc/supervisor/custom.d
environment:
ENABLE_QUEUE: "${ENABLE_QUEUE}"
ENABLE_SCHEDULER: "${ENABLE_SCHEDULER}"
ENABLE_HORIZON: "${ENABLE_HORIZON}"
ENABLE_LARAVEL_PERMS: "1"
$([ "$ENABLE_WEBSOCKET" = "true" ] && echo " PUSHER_PORT: \"${WEBSOCKET_PORT}\"")
networks:
- web
- internal
depends_on:
YAML
if [ "$ENABLE_MYSQL" = "true" ]; then
cat >> docker-compose.yml <<YAML
mysql:
condition: service_healthy
YAML
fi
if [ "$ENABLE_REDIS" = "true" ]; then
cat >> docker-compose.yml <<YAML
redis:
condition: service_healthy
YAML
fi
cat >> docker-compose.yml <<YAML
labels:
- traefik.enable=true
- traefik.docker.network=web
# HTTP
- traefik.http.routers.${PROJECT_NAME}.rule=Host(\`${TRAEFIK_HOST}\`)
- traefik.http.routers.${PROJECT_NAME}.entrypoints=web
- traefik.http.routers.${PROJECT_NAME}.service=${PROJECT_NAME}-http
- traefik.http.services.${PROJECT_NAME}-http.loadbalancer.server.port=80
# HTTPS
- traefik.http.routers.${PROJECT_NAME}-tls.rule=Host(\`${TRAEFIK_HOST}\`)
- traefik.http.routers.${PROJECT_NAME}-tls.entrypoints=websecure
- traefik.http.routers.${PROJECT_NAME}-tls.tls=true
- traefik.http.routers.${PROJECT_NAME}-tls.service=${PROJECT_NAME}-https
- traefik.http.services.${PROJECT_NAME}-https.loadbalancer.server.port=80
$(if [ "$ENABLE_WEBSOCKET" = "true" ]; then
cat <<WSLABELS
# WebSocket
- traefik.http.routers.${PROJECT_NAME}-ws.rule=Host(\`ws-${TRAEFIK_HOST}\`)
- traefik.http.routers.${PROJECT_NAME}-ws.entrypoints=web
- traefik.http.routers.${PROJECT_NAME}-ws.service=${PROJECT_NAME}-ws
- traefik.http.services.${PROJECT_NAME}-ws.loadbalancer.server.port=${WEBSOCKET_PORT}
- traefik.http.routers.${PROJECT_NAME}-wss.rule=Host(\`ws-${TRAEFIK_HOST}\`)
- traefik.http.routers.${PROJECT_NAME}-wss.entrypoints=websecure
- traefik.http.routers.${PROJECT_NAME}-wss.tls=true
- traefik.http.routers.${PROJECT_NAME}-wss.service=${PROJECT_NAME}-wss
- traefik.http.services.${PROJECT_NAME}-wss.loadbalancer.server.port=${WEBSOCKET_PORT}
WSLABELS
fi)
YAML
# --- MySQL ---
if [ "$ENABLE_MYSQL" = "true" ]; then
cat >> docker-compose.yml <<YAML
# -------------------------------------------------------------------------
# MySQL
# -------------------------------------------------------------------------
mysql:
image: mysql:8.0
container_name: ${PROJECT_NAME}-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "${DB_PASSWORD}"
MYSQL_DATABASE: "${DB_NAME}"
volumes:
- mysql-data:/var/lib/mysql
networks:
- internal
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${DB_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
YAML
fi
# --- Redis ---
if [ "$ENABLE_REDIS" = "true" ]; then
cat >> docker-compose.yml <<YAML
# -------------------------------------------------------------------------
# Redis
# -------------------------------------------------------------------------
redis:
image: redis:7-alpine
container_name: ${PROJECT_NAME}-redis
restart: unless-stopped
volumes:
- redis-data:/data
networks:
- internal
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
YAML
fi
# --- WebSocket supervisor config ---
if [ "$ENABLE_WEBSOCKET" = "true" ]; then
cat > docker/supervisor/websocket.conf <<CONF
[program:websocket]
command=/usr/local/bin/php -d variables_order=EGPCS /var/www/html/artisan websockets:serve --host=0.0.0.0 --port=${WEBSOCKET_PORT}
autostart=true
autorestart=true
user=www-data
priority=30
startsecs=5
startretries=100
stopsignal=TERM
stopwaitsecs=15
stdout_logfile=/proc/1/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/proc/1/fd/2
stderr_logfile_maxbytes=0
CONF
echo " Created docker/supervisor/websocket.conf"
fi
# ---------------------------------------------------------------------------
# Generate .env.docker
# ---------------------------------------------------------------------------
cat > .env.docker <<ENV
# ===========================================================================
# Docker environment — generated by plug-n-pray.sh
# Merge these into your .env or source this file
# ===========================================================================
# App
APP_URL=http://${TRAEFIK_HOST}
ENV
if [ "$ENABLE_MYSQL" = "true" ]; then
cat >> .env.docker <<ENV
# Database
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=${DB_NAME}
DB_USERNAME=root
DB_PASSWORD=${DB_PASSWORD}
ENV
fi
if [ "$ENABLE_REDIS" = "true" ]; then
cat >> .env.docker <<ENV
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
ENV
fi
if [ "$ENABLE_WEBSOCKET" = "true" ]; then
cat >> .env.docker <<ENV
# WebSocket (blax/laravel-websockets)
BROADCAST_CONNECTION=pusher
PUSHER_APP_ID=app-id
PUSHER_APP_KEY=app-key
PUSHER_APP_SECRET=app-secret
PUSHER_HOST=127.0.0.1
PUSHER_PORT=${WEBSOCKET_PORT}
PUSHER_SCHEME=http
LARAVEL_WEBSOCKETS_PORT=${WEBSOCKET_PORT}
ENV
fi
# ---------------------------------------------------------------------------
# Done
# ---------------------------------------------------------------------------
echo ""
echo "=========================================="
echo " Files created:"
echo "=========================================="
echo " docker-compose.yml — Full stack (app + db + redis, Traefik labels)"
echo " .env.docker — Environment variables to merge into .env"
echo " docker/supervisor/ — Mount dir for custom supervisor programs"
echo ""
echo " Next steps:"
echo " 1. Merge .env.docker into your .env"
echo " 2. Create the external network (once): docker network create web"
echo " 3. Start: docker compose up -d"
echo " 4. Visit: http://${TRAEFIK_HOST}"
echo ""
echo " Pray it works. 🙏"
echo "=========================================="

133
scripts/publish.sh Executable file
View File

@ -0,0 +1,133 @@
#!/usr/bin/env bash
# ===========================================================================
# publish.sh — Push all built images to a container registry
#
# Usage:
# REGISTRY=ghcr.io/myorg ./publish.sh # push all versions
# REGISTRY=ghcr.io/myorg ./publish.sh 8.4 # push only PHP 8.4 tags
# REGISTRY=ghcr.io/myorg ./publish.sh 8.3 8.4 # push PHP 8.3 + 8.4
#
# Environment:
# REGISTRY — registry prefix (REQUIRED, e.g. ghcr.io/myorg)
# IMAGE_NAME — image name (default: docker-laravel)
# DRY_RUN — set to 1 to print commands without executing
# ===========================================================================
set -euo pipefail
if [ -z "${REGISTRY:-}" ]; then
echo "ERROR: REGISTRY is required."
echo ""
echo "Usage: REGISTRY=ghcr.io/myorg ./publish.sh [php_versions...]"
echo ""
echo "Examples:"
echo " REGISTRY=ghcr.io/myorg ./publish.sh"
echo " REGISTRY=docker.io/myuser ./publish.sh 8.4"
exit 1
fi
IMAGE_NAME="${IMAGE_NAME:-docker-laravel}"
LOCAL="${IMAGE_NAME}"
REMOTE="${REGISTRY}/${IMAGE_NAME}"
# ---------------------------------------------------------------------------
# PHP → Laravel version mapping (must match build.sh)
# ---------------------------------------------------------------------------
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"
)
declare -A LARAVEL_RECOMMENDED_PHP=(
["9"]="8.1"
["10"]="8.3"
["11"]="8.4"
["12"]="8.5"
["13"]="8.5"
)
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 push
# ---------------------------------------------------------------------------
if [ $# -gt 0 ]; then
PUSH_VERSIONS=("$@")
else
PUSH_VERSIONS=("${ALL_PHP_VERSIONS[@]}")
fi
for v in "${PUSH_VERSIONS[@]}"; do
if [[ ! -v "PHP_LARAVEL_MAP[$v]" ]]; then
echo "ERROR: Unknown PHP version: $v"
exit 1
fi
done
# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------
run() {
if [ "${DRY_RUN:-0}" = "1" ]; then
echo "[dry-run] $*"
else
"$@"
fi
}
tag_and_push() {
local LOCAL_TAG="$1"
local REMOTE_TAG="$2"
run docker tag "${LOCAL_TAG}" "${REMOTE_TAG}"
run docker push "${REMOTE_TAG}"
}
# ---------------------------------------------------------------------------
# Push
# ---------------------------------------------------------------------------
TOTAL=0
PUSHED=0
for PHP_VERSION in "${PUSH_VERSIONS[@]}"; do
echo ""
echo "==========================================="
echo " Pushing PHP ${PHP_VERSION}"
echo "==========================================="
# Base PHP tag
tag_and_push "${LOCAL}:php${PHP_VERSION}" "${REMOTE}:php${PHP_VERSION}"
TOTAL=$((TOTAL + 1)); PUSHED=$((PUSHED + 1))
# Laravel combo tags
LARAVEL_VERSIONS="${PHP_LARAVEL_MAP[$PHP_VERSION]}"
for LV in $LARAVEL_VERSIONS; do
tag_and_push "${LOCAL}:php${PHP_VERSION}" "${REMOTE}:laravel${LV}-php${PHP_VERSION}"
TOTAL=$((TOTAL + 1)); PUSHED=$((PUSHED + 1))
# Bare laravelN tag
if [ "${LARAVEL_RECOMMENDED_PHP[$LV]}" = "$PHP_VERSION" ]; then
tag_and_push "${LOCAL}:php${PHP_VERSION}" "${REMOTE}:laravel${LV}"
TOTAL=$((TOTAL + 1)); PUSHED=$((PUSHED + 1))
fi
done
# latest
if [ "$PHP_VERSION" = "$LATEST_PHP" ]; then
tag_and_push "${LOCAL}:php${PHP_VERSION}" "${REMOTE}:latest"
TOTAL=$((TOTAL + 1)); PUSHED=$((PUSHED + 1))
fi
done
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo ""
echo "==========================================="
echo " Publish complete"
echo "==========================================="
echo "Pushed ${PUSHED} tags to ${REGISTRY}/${IMAGE_NAME}"

145
scripts/start-container Executable file
View File

@ -0,0 +1,145 @@
#!/usr/bin/env bash
set -euo pipefail
echo "=========================================="
echo " docker-laravel container starting"
echo "=========================================="
echo "Date: $(date)"
echo "Hostname: $(hostname)"
echo "PHP: $(php -r 'echo PHP_VERSION;')"
echo "Node: $(node --version 2>/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:-<unset>}"
echo " APP_DEBUG=${APP_DEBUG:-<unset>}"
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