From 22d1ce310259353131b05e8fe6be8212dad0dd8d26b4044aeb994cb888ec8897 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 26 Apr 2026 22:50:18 -0400 Subject: [PATCH] feat(docker): production multi-stage Dockerfile Three named targets (app, horizon, scheduler) sharing a runtime-base with PHP 8.3-FPM, opcache, redis, and pinned php-fpm pool config. Composer + Node build stages are separate so vendor/ and public/build/ are baked into the runtime image. Co-Authored-By: Claude Opus 4.7 (1M context) --- .dockerignore | 30 +++++++++ Dockerfile | 127 ++++++++++++++++++++++++++++++++++++++ docker/prod-entrypoint.sh | 22 +++++++ 3 files changed, 179 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100755 docker/prod-entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0dfeaa3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +# .dockerignore +.git +.gitignore +.github +.gitea +.idea +.vscode +.claude +.superpowers +.playwright-mcp +docker +!docker/prod-entrypoint.sh +docker-compose*.yml +.env* +!website/.env.example +docs +helm +node_modules +website/node_modules +website/vendor +website/storage/logs/* +website/storage/framework/cache/data/* +website/storage/framework/sessions/* +website/storage/framework/views/* +website/.phpunit.cache +website/tests +*.md +Makefile +scripts/whmcs-migrate +ipv4-outreach-tickets.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..46c1b42 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,127 @@ +# syntax=docker/dockerfile:1.7 + +# ============================================================================== +# composer-deps — install PHP deps (no scripts, no dev) +# ============================================================================== +FROM composer:2 AS composer-deps +WORKDIR /app +COPY website/composer.json website/composer.lock ./ +RUN composer install \ + --no-dev \ + --no-scripts \ + --no-autoloader \ + --prefer-dist \ + --no-interaction \ + --no-progress \ + --ignore-platform-reqs +COPY website/ ./ +RUN composer dump-autoload --optimize --classmap-authoritative + +# ============================================================================== +# node-build — compile assets (Vite) +# ============================================================================== +FROM node:24-alpine AS node-build +WORKDIR /app +COPY website/package.json website/package-lock.json* ./ +RUN npm ci --no-audit --no-fund +COPY website/ ./ +# vendor/ is needed because Vite reads Laravel's helpers via @vite() +COPY --from=composer-deps /app/vendor ./vendor +RUN npm run build + +# ============================================================================== +# runtime-base — common PHP 8.3-FPM image with extensions +# ============================================================================== +FROM php:8.3-fpm-bookworm AS runtime-base + +ENV COMPOSER_ALLOW_SUPERUSER=1 \ + COMPOSER_NO_INTERACTION=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + git unzip curl ca-certificates \ + libzip-dev libpng-dev libjpeg-dev libfreetype6-dev libwebp-dev \ + libicu-dev libonig-dev libxml2-dev libcurl4-openssl-dev libssl-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \ + && docker-php-ext-install -j"$(nproc)" \ + pdo_mysql intl bcmath gd zip pcntl posix exif sockets opcache + +RUN pecl install redis \ + && docker-php-ext-enable redis + +# Production PHP / opcache config +RUN { \ + echo 'memory_limit=512M'; \ + echo 'upload_max_filesize=50M'; \ + echo 'post_max_size=50M'; \ + echo 'max_execution_time=120'; \ + echo 'expose_php=Off'; \ + } > /usr/local/etc/php/conf.d/zz-app.ini + +RUN { \ + echo 'opcache.enable=1'; \ + echo 'opcache.enable_cli=0'; \ + echo 'opcache.validate_timestamps=0'; \ + echo 'opcache.memory_consumption=256'; \ + echo 'opcache.interned_strings_buffer=16'; \ + echo 'opcache.max_accelerated_files=20000'; \ + echo 'opcache.preload_user=www-data'; \ + } > /usr/local/etc/php/conf.d/zz-opcache.ini + +# php-fpm pool — listen on 0.0.0.0:9000 (sidecar nginx connects to localhost) +RUN { \ + echo '[www]'; \ + echo 'user = www-data'; \ + echo 'group = www-data'; \ + echo 'listen = 0.0.0.0:9000'; \ + echo 'pm = dynamic'; \ + echo 'pm.max_children = 20'; \ + echo 'pm.start_servers = 4'; \ + echo 'pm.min_spare_servers = 2'; \ + echo 'pm.max_spare_servers = 6'; \ + echo 'pm.max_requests = 500'; \ + echo 'clear_env = no'; \ + } > /usr/local/etc/php-fpm.d/zz-app.conf + +WORKDIR /var/www/html + +# Bring in source + autoloaded vendor + built assets +COPY --chown=www-data:www-data --from=composer-deps /app /var/www/html +COPY --chown=www-data:www-data --from=node-build /app/public/build /var/www/html/public/build + +# Cache config & routes at build time. Skip if no APP_KEY available. +# These are best-effort — caches will rebuild at runtime if needed. +RUN php artisan config:clear || true \ + && php artisan route:clear || true \ + && php artisan view:clear || true + +# Entrypoint +COPY docker/prod-entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +USER www-data + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] + +# ============================================================================== +# app — php-fpm (paired with nginx sidecar in k8s) +# ============================================================================== +FROM runtime-base AS app +EXPOSE 9000 +CMD ["php-fpm"] + +# ============================================================================== +# horizon — queue worker +# ============================================================================== +FROM runtime-base AS horizon +STOPSIGNAL SIGTERM +CMD ["php", "artisan", "horizon"] + +# ============================================================================== +# scheduler — schedule:work loop +# ============================================================================== +FROM runtime-base AS scheduler +CMD ["php", "artisan", "schedule:work", "--no-interaction"] diff --git a/docker/prod-entrypoint.sh b/docker/prod-entrypoint.sh new file mode 100755 index 0000000..bf3bbf3 --- /dev/null +++ b/docker/prod-entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -e + +cd /var/www/html + +# Install Passport keys from Secret-mounted location, if present. +# Chart mounts the keys at /var/www/html/secrets/oauth-{public,private}.key. +if [ -f /var/www/html/secrets/oauth-private.key ] \ + && [ -f /var/www/html/secrets/oauth-public.key ]; then + cp /var/www/html/secrets/oauth-private.key storage/oauth-private.key + cp /var/www/html/secrets/oauth-public.key storage/oauth-public.key + chmod 600 storage/oauth-private.key storage/oauth-public.key +fi + +# Refresh Laravel caches against current env. Safe to run on every boot — +# config/route/view caching is per-pod and idempotent. +php artisan config:cache +php artisan route:cache +php artisan view:cache +php artisan event:cache || true + +exec "$@"