diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..88f6a9a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,49 @@ +# Version control +.git +.gitignore +.gitattributes + +# Dev environment +shell.nix +Dockerfile.dev +docker/ + +# Tests (not needed in prod image) +tests/ +phpunit.xml +.phpunit.result.cache +phpstan.neon + +# Dependencies (rebuilt during image build) +node_modules/ +vendor/ + +# Build artifacts (frontend stage produces these) +public/build/ +public/hot + +# Editor / OS +.editorconfig +.idea/ +.vscode/ +.DS_Store +*.swp +*.swo + +# Env / secrets +.env +.env.* +!.env.example + +# Logs and runtime caches +storage/logs/*.log +storage/framework/cache/data/ +storage/framework/sessions/ +storage/framework/views/ + +# CI +.forgejo/ + +# Docs / project meta +README.md +LICENSE diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 53d63b2..3676bb3 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -5,8 +5,7 @@ on: branches: [main] tags: ['v*'] paths: - - 'Dockerfile' - - 'docker/**' + - 'docker/prod/Dockerfile' - 'app/**' - 'bootstrap/**' - 'config/**' @@ -51,6 +50,6 @@ jobs: uses: https://data.forgejo.org/docker/build-push-action@v5 with: context: . - file: Dockerfile + file: docker/prod/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile new file mode 100644 index 0000000..46d8c2f --- /dev/null +++ b/docker/prod/Dockerfile @@ -0,0 +1,128 @@ +# 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"]