# syntax=docker/dockerfile:1 # ============================================================ # Stage 1: Build frontend assets # ============================================================ FROM node:20-alpine AS frontend WORKDIR /app COPY package.json package-lock.json vite.config.js ./ COPY resources/ resources/ RUN npm ci --no-audit --no-fund RUN npm run build # ============================================================ # Stage 2: Runtime (FrankenPHP) # ============================================================ FROM dunglas/frankenphp:1.1-php8.3-alpine AS runtime RUN apk add --no-cache \ git \ postgresql-client \ curl RUN install-php-extensions \ pdo_pgsql \ redis \ opcache \ zip \ gd \ intl COPY --from=composer:2 /usr/bin/composer /usr/bin/composer WORKDIR /app ENV APP_ENV=production \ APP_DEBUG=false \ LOG_CHANNEL=stack \ LOG_LEVEL=warning \ DB_CONNECTION=pgsql \ DB_HOST=db \ DB_PORT=5432 \ REDIS_HOST=redis \ REDIS_PORT=6379 \ CACHE_STORE=redis \ QUEUE_CONNECTION=redis \ SESSION_DRIVER=redis \ BROADCAST_CONNECTION=log \ MAIL_MAILER=log # Copy only the files composer needs before install, so the composer layer stays # cached when application source changes. packages/ is required because composer.json # declares it as a path repository. COPY composer.json composer.lock ./ COPY packages/ packages/ # Skip post-autoload scripts (package:discover) during build — they need a runtime # Laravel boot which fails without proper env. Discovery happens at runtime via # start-prod.sh. --classmap-authoritative implies --optimize-autoloader. RUN composer install --no-dev --no-interaction --prefer-dist --classmap-authoritative --no-scripts COPY . . COPY --from=frontend /app/public/build /app/public/build RUN chown -R www-data:www-data /app/storage /app/bootstrap/cache RUN cat > /etc/caddy/Caddyfile <<'EOF' { frankenphp order php_server before file_server } :8000 { root * /app/public php_server { index index.php } encode gzip zstd file_server header { X-Frame-Options "SAMEORIGIN" X-Content-Type-Options "nosniff" Referrer-Policy "strict-origin-when-cross-origin" } } EOF EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD curl -fsS http://localhost:8000/up || exit 1 RUN cat > /start-prod.sh <<'EOF' #!/bin/sh set -e echo "Waiting for PostgreSQL at ${DB_HOST}:${DB_PORT}..." for i in $(seq 1 60); do if pg_isready -h "${DB_HOST}" -p "${DB_PORT}" -q; then echo "PostgreSQL is ready." break fi if [ "$i" = "60" ]; then echo "Timed out waiting for PostgreSQL after 60s." >&2 exit 1 fi sleep 1 done php artisan package:discover --ansi php artisan config:cache php artisan route:cache php artisan view:cache php artisan migrate --force exec frankenphp run --config /etc/caddy/Caddyfile EOF RUN chmod +x /start-prod.sh CMD ["/start-prod.sh"]