diff --git a/Dockerfile b/Dockerfile index a1e9654..7fa8175 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,50 +1,44 @@ -# ---------- 1) Build Composer deps ---------- +# ---------- 1) Build Composer deps (no scripts; avoids artisan during build) ---------- FROM composer:2 AS vendor ARG COMPOSER_NO_DEV=1 WORKDIR /app COPY composer.json composer.lock ./ - # Disable scripts to avoid "artisan not found" RUN if [ "$COMPOSER_NO_DEV" = "1" ]; then \ composer install --no-dev --prefer-dist --no-progress --no-interaction --classmap-authoritative --no-scripts; \ else \ composer install --prefer-dist --no-progress --no-interaction --no-scripts; \ fi - - # ---------- 2) Copy app source (optional separate build stage) ---------- + # ---------- 2) Bring in app sources ---------- FROM alpine:3.20 AS app_src WORKDIR /app COPY . ./ - - # ---------- 3) Final image: Nginx + PHP-FPM ---------- + # ---------- 3) Final image: Nginx + PHP-FPM + PGSQL ---------- FROM php:8.4-fpm-alpine - # System dependencies & build tools + # System deps (build + runtime) RUN set -eux; \ - apk add --no-cache --virtual .build-deps \ - $PHPIZE_DEPS \ - icu-dev libzip-dev postgresql-dev \ - freetype-dev libjpeg-turbo-dev libpng-dev \ - oniguruma-dev pkgconf \ - && apk add --no-cache \ - icu-libs libzip libpq tzdata bash git nginx supervisor curl \ - oniguruma \ - \ - # PHP extensions - && docker-php-ext-configure intl \ - && docker-php-ext-configure gd --with-freetype --with-jpeg \ - && docker-php-ext-install -j"$(nproc)" \ - intl opcache zip bcmath mbstring gd pdo pdo_pgsql pgsql \ - \ - # Cleanup - && apk del .build-deps \ - && rm -rf /var/cache/apk/* /tmp/* - + apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS icu-dev libzip-dev postgresql-dev \ + freetype-dev libjpeg-turbo-dev libpng-dev \ + oniguruma-dev pkgconf \ + && apk add --no-cache \ + icu-libs libzip libpq tzdata bash git curl \ + nginx supervisor \ + oniguruma \ + freetype libjpeg-turbo libpng \ + # PHP extensions + && docker-php-ext-configure intl \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j"$(nproc)" \ + intl opcache zip bcmath mbstring gd pdo pdo_pgsql pgsql \ + # Cleanup + && apk del .build-deps \ + && rm -rf /var/cache/apk/* /tmp/* - # Configure PHP-FPM to use UNIX socket for Nginx + # PHP runtime tuning RUN set -eux; \ - sed -ri 's|^;?listen = .*$|listen = /run/php-fpm.sock|' /usr/local/etc/php-fpm.d/zz-docker.conf; \ { \ echo "memory_limit=512M"; \ echo "upload_max_filesize=64M"; \ @@ -60,34 +54,27 @@ echo "opcache.max_accelerated_files=20000"; \ } > /usr/local/etc/php/conf.d/laravel.ini - # Prepare directories - RUN mkdir -p /run/nginx /run/php /var/log/supervisor /var/www/html /etc/nginx/conf.d + # Prepare dirs + RUN mkdir -p /run/php /run/nginx /var/log/supervisor /var/www/html /etc/nginx/http.d - # Copy app & vendor + # Copy app and vendor COPY --from=app_src /app /var/www/html COPY --from=vendor /app/vendor /var/www/html/vendor - # Copy configs - COPY deploy/nginx/default.conf /etc/nginx/conf.d/default.conf - COPY deploy/supervisord.conf /etc/supervisord.conf - - WORKDIR /var/www/html - - # (Optional) Run artisan discovery after copy — nonfatal - RUN php artisan package:discover --ansi || true - - # Fix permissions for Laravel - RUN set -eux; \ - mkdir -p storage bootstrap/cache; \ - chown -R www-data:www-data storage bootstrap/cache; \ - find storage -type d -exec chmod 775 {} \; ; \ - find storage -type f -exec chmod 664 {} \; ; \ - chmod -R 775 bootstrap/cache - - EXPOSE 80 - HEALTHCHECK --interval=30s --timeout=5s --retries=5 \ - CMD curl -fsS http://localhost/healthz || exit 1 - - # Supervisor runs both services - CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] - \ No newline at end of file +# Copy configs +COPY deploy/nginx/default.conf /etc/nginx/http.d/default.conf +COPY deploy/supervisord.conf /etc/supervisord.conf +COPY deploy/php-fpm/zz-socket.conf /usr/local/etc/php-fpm.d/zz-socket.conf +COPY deploy/entrypoint.sh /usr/local/bin/entrypoint.sh + +# Ensure executable +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Healthcheck and ports (unchanged) +EXPOSE 80 +HEALTHCHECK --interval=30s --timeout=5s --retries=5 \ + CMD curl -fsS http://localhost/healthz || exit 1 + +# Use the entrypoint to run artisan warmups, then exec supervisord +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] \ No newline at end of file diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh new file mode 100644 index 0000000..448d1c0 --- /dev/null +++ b/deploy/entrypoint.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env sh +set -e + +# -------- Options (env flags) -------- +: "${ARTISAN_BOOT:=1}" # set to 0 to skip all artisan work +: "${ARTISAN_DB_WAIT:=0}" # set to 1 to wait for DB before artisan +: "${ARTISAN_MIGRATE:=0}" # set to 1 to run php artisan migrate --force +: "${ARTISAN_HORIZON:=0}" # set to 1 to start Horizon under supervisord (add program if you want) +: "${ARTISAN_VERBOSE:=0}" # set to 1 for more logs + +log() { [ "$ARTISAN_VERBOSE" = "1" ] && echo "[entrypoint] $*"; } +warn() { echo "[entrypoint] $*" 1>&2; } + +cd /var/www/html || true + +if [ "$ARTISAN_BOOT" != "1" ]; then + log "ARTISAN_BOOT=0 → skipping artisan boot steps." + exec /usr/bin/supervisord -c /etc/supervisord.conf +fi + +if [ ! -f artisan ]; then + warn "artisan not found; skipping artisan steps." + exec /usr/bin/supervisord -c /etc/supervisord.conf +fi + +# -------- Optional: wait for Postgres -------- +if [ "$ARTISAN_DB_WAIT" = "1" ] && [ -n "$DB_HOST" ] && [ -n "$DB_PORT" ]; then + # Use PHP + sockets to test TCP if pg_isready isn't available + log "Waiting for DB ${DB_HOST}:${DB_PORT} ..." + i=0 + until php -r ' + $h=getenv("DB_HOST"); $p=(int)getenv("DB_PORT")?:5432; + $s=@fsockopen($h,$p,$errno,$errstr,1.0); + if($s){fclose($s); exit(0);} exit(1); + '; do + i=$((i+1)) + if [ $i -gt 60 ]; then + warn "DB wait timed out after 60s — continuing anyway." + break + fi + sleep 1 + done +fi + +# -------- Safe, cache-warming steps -------- +# Don’t fail the container if any step errors (env may be partial in some setups) +php artisan package:discover --ansi || warn "package:discover failed" +php artisan config:cache --ansi || warn "config:cache failed" +php artisan route:cache --ansi || warn "route:cache failed" +php artisan view:cache --ansi || warn "view:cache failed" + +# -------- (Optional) database migrations -------- +if [ "$ARTISAN_MIGRATE" = "1" ]; then + php artisan migrate --force --no-interaction --ansi || warn "migrate failed" +fi + +# -------- Hand off to supervisord -------- +exec /usr/bin/supervisord -c /etc/supervisord.conf diff --git a/deploy/nginx/default.conf b/deploy/nginx/default.conf index e50b199..0e81bae 100644 --- a/deploy/nginx/default.conf +++ b/deploy/nginx/default.conf @@ -5,10 +5,10 @@ server { root /var/www/html/public; index index.php; - # Healthcheck + # Health check (no PHP) location = /healthz { return 200 "ok\n"; add_header Content-Type text/plain; } - # Static + # Static assets location ~* \.(?:css|js|mjs|map|jpg|jpeg|png|gif|ico|svg|webp|avif|ttf|otf|woff|woff2)$ { access_log off; log_not_found off; expires 7d; @@ -16,16 +16,16 @@ server { try_files $uri =404; } - # Front controller + # Laravel front controller location / { try_files $uri $uri/ /index.php?$query_string; } - # PHP via unix socket + # PHP via unix socket (shared with php-fpm) location ~ \.php$ { include fastcgi_params; fastcgi_index index.php; - fastcgi_pass unix:/run/php-fpm.sock; + fastcgi_pass unix:/run/php/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; fastcgi_read_timeout 60s; diff --git a/deploy/php-fpm/zz-socket.conf b/deploy/php-fpm/zz-socket.conf new file mode 100644 index 0000000..fc278b2 --- /dev/null +++ b/deploy/php-fpm/zz-socket.conf @@ -0,0 +1,24 @@ +; Use a unix socket and ensure nginx can access it +[global] +; (keep global defaults) + +[www] +listen = /run/php/php-fpm.sock +listen.owner = www-data +listen.group = nginx +listen.mode = 0660 + +; Keep default user/group for PHP workers +user = www-data +group = www-data + +; Sensible FPM settings +pm = dynamic +pm.max_children = 10 +pm.start_servers = 2 +pm.min_spare_servers = 2 +pm.max_spare_servers = 5 + +; Clear env for security, but allow PATH +clear_env = yes +env[PATH] = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin diff --git a/deploy/supervisord.conf b/deploy/supervisord.conf index 53536fe..5273030 100644 --- a/deploy/supervisord.conf +++ b/deploy/supervisord.conf @@ -4,6 +4,7 @@ logfile=/var/log/supervisor/supervisord.log [program:php-fpm] command=/usr/local/sbin/php-fpm -F +user=www-data autorestart=true priority=10 stdout_logfile=/dev/stdout @@ -13,6 +14,7 @@ stderr_logfile_maxbytes=0 [program:nginx] command=/usr/sbin/nginx -g "daemon off;" +user=nginx autorestart=true priority=20 stdout_logfile=/dev/stdout diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh deleted file mode 100644 index 4a38753..0000000 --- a/docker/entrypoint.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh -set -eu - -cd /var/www/html || exit 1 - -# Ensure Laravel dirs exist (idempotent) -mkdir -p storage/framework/cache storage/framework/sessions storage/framework/views storage/logs bootstrap/cache - -# Fix perms (in case volumes are mounted) -chown -R nginx:nginx storage bootstrap/cache || true -chmod -R ug+rwX storage bootstrap/cache || true - -# If Laravel present: clear stale build-stage caches pointing to /app, then warm new caches -if [ -f artisan ]; then - php artisan config:clear --no-ansi || true - php artisan cache:clear --no-ansi || true - php artisan route:clear --no-ansi || true - php artisan view:clear --no-ansi || true - # Optional: warm fresh caches in runtime path - php artisan config:cache --no-ansi || true - php artisan route:cache --no-ansi || true - php artisan view:cache --no-ansi || true -fi - -exit 0