From 87f41387cea5c97da2dff310df5904de9ad45e3d Mon Sep 17 00:00:00 2001 From: Boban Blaskovic Date: Sun, 31 May 2026 17:44:35 +0200 Subject: [PATCH] Bake app into multi-stage image; drop bind mounts for Portainer remote endpoint - New root Dockerfile: composer-deps + node-build + assembled code stages, final PHP-FPM and nginx targets both built from same context. - docker-compose.yml: all services build/use baked images. No ./app or config bind mounts (broke with Portainer on VM-A + Docker daemon on VM-B). - app-storage named volume preserves Laravel storage across redeploys. - Laravel env passed via x-php-env anchor from stack env vars. - Removed per-service docker/php/Dockerfile and docker/nginx/Dockerfile. - entrypoint.sh: touch .env, ensure storage dirs, idempotent storage:link. --- .dockerignore | 27 +++++++++++++++ Dockerfile | 56 +++++++++++++++++++++++++++++++ docker-compose.yml | 72 ++++++++++++++++++++++++++++++++++------ docker/nginx/Dockerfile | 2 -- docker/php/Dockerfile | 20 ----------- docker/php/entrypoint.sh | 16 +++++++-- stack.env.example | 32 ++++++++++++++---- 7 files changed, 185 insertions(+), 40 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile delete mode 100644 docker/nginx/Dockerfile delete mode 100644 docker/php/Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0b2ef14 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +.git +.gitignore +.github +templates +certs +stack.env* +docker-compose*.override.yml + +# Old single-target Dockerfiles (now merged into root Dockerfile) +docker/php/Dockerfile +docker/nginx/Dockerfile + +# Laravel build artifacts / local state +app/node_modules +app/vendor +app/.env +app/.env.backup +app/.env.production +app/public/build +app/public/hot +app/storage/logs +app/storage/framework/cache +app/storage/framework/sessions +app/storage/framework/views +app/bootstrap/cache +app/.phpunit.cache +app/.phpunit.result.cache diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cbf366d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +# syntax=docker/dockerfile:1.7 + +# ---------- composer deps ---------- +FROM composer:2.7 AS composer-deps +WORKDIR /build +COPY app/composer.json app/composer.lock ./ +RUN composer install \ + --no-dev --no-scripts --no-autoloader \ + --prefer-dist --no-interaction --no-progress + +# ---------- node / vite build ---------- +FROM node:20-alpine AS node-build +WORKDIR /build +COPY app/package.json app/package-lock.json ./ +RUN npm ci --no-audit --no-fund +COPY app/ ./ +RUN npm run build + +# ---------- assembled code ---------- +FROM alpine:3.20 AS code +WORKDIR /var/www/html +COPY app/ ./ +COPY --from=composer-deps /build/vendor ./vendor +COPY --from=node-build /build/public/build ./public/build + +# ---------- PHP-FPM runtime ---------- +FROM php:8.4-fpm-alpine AS php +RUN apk add --no-cache \ + git curl libpng-dev libzip-dev oniguruma-dev icu-dev \ + autoconf g++ make linux-headers $PHPIZE_DEPS \ + && docker-php-ext-install pdo_mysql mbstring zip exif pcntl bcmath gd intl sockets \ + && pecl install redis && docker-php-ext-enable redis \ + && apk del autoconf g++ make linux-headers $PHPIZE_DEPS \ + && rm -rf /tmp/* /var/cache/apk/* + +COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html +COPY --from=code /var/www/html /var/www/html + +RUN composer dump-autoload --optimize --no-dev --no-interaction \ + && mkdir -p storage/framework/cache storage/framework/sessions storage/framework/views \ + storage/logs storage/app/public bootstrap/cache \ + && chown -R www-data:www-data storage bootstrap/cache \ + && chmod -R 775 storage bootstrap/cache + +COPY docker/php/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["php-fpm"] + +# ---------- nginx runtime ---------- +FROM nginx:1.27-alpine AS nginx +COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf +COPY --from=code /var/www/html/public /var/www/html/public diff --git a/docker-compose.yml b/docker-compose.yml index 164dd63..afa96d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,53 @@ name: ${STACK_NAME:-clupilot} +x-php-build: &php-build + context: . + dockerfile: Dockerfile + target: php + +x-php-env: &php-env + APP_NAME: ${APP_NAME:-CluPilot} + APP_ENV: ${APP_ENV:-production} + APP_KEY: ${APP_KEY:?APP_KEY required — run php artisan key:generate locally and paste the value} + APP_DEBUG: ${APP_DEBUG:-false} + APP_URL: ${APP_URL:-http://localhost} + LOG_CHANNEL: stack + LOG_LEVEL: ${LOG_LEVEL:-info} + DB_CONNECTION: mysql + DB_HOST: mysql + DB_PORT: 3306 + DB_DATABASE: ${DB_DATABASE:-clupilot} + DB_USERNAME: ${DB_USERNAME:-clupilot} + DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD required} + REDIS_HOST: redis + REDIS_PORT: 6379 + CACHE_STORE: redis + SESSION_DRIVER: redis + QUEUE_CONNECTION: redis + BROADCAST_CONNECTION: reverb + REVERB_APP_ID: ${REVERB_APP_ID:-clupilot} + REVERB_APP_KEY: ${REVERB_APP_KEY:?REVERB_APP_KEY required} + REVERB_APP_SECRET: ${REVERB_APP_SECRET:?REVERB_APP_SECRET required} + REVERB_HOST: 0.0.0.0 + REVERB_PORT: 8080 + REVERB_SCHEME: http + MAIL_MAILER: ${MAIL_MAILER:-log} + MAIL_HOST: ${MAIL_HOST:-mailpit} + MAIL_PORT: ${MAIL_PORT:-1025} + MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS:-no-reply@clupilot.local} + MAIL_FROM_NAME: ${APP_NAME:-CluPilot} + services: nginx: - build: ./docker/nginx + build: + context: . + dockerfile: Dockerfile + target: nginx image: ${STACK_NAME:-clupilot}/nginx:latest container_name: ${STACK_NAME:-clupilot}-nginx restart: unless-stopped ports: - "${HTTP_PORT:-80}:80" - volumes: - - ./app:/var/www/html - extra_hosts: - - "host.docker.internal:host-gateway" depends_on: app: condition: service_started @@ -25,12 +61,13 @@ services: retries: 3 app: - build: ./docker/php + build: *php-build image: ${STACK_NAME:-clupilot}/php:latest container_name: ${STACK_NAME:-clupilot}-app restart: unless-stopped + environment: *php-env volumes: - - ./app:/var/www/html + - app-storage:/var/www/html/storage depends_on: mysql: condition: service_healthy @@ -39,37 +76,51 @@ services: networks: [clu] queue: + build: *php-build image: ${STACK_NAME:-clupilot}/php:latest container_name: ${STACK_NAME:-clupilot}-queue restart: unless-stopped command: php artisan queue:work --tries=3 --timeout=120 + environment: *php-env volumes: - - ./app:/var/www/html + - app-storage:/var/www/html/storage depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy app: condition: service_started networks: [clu] scheduler: + build: *php-build image: ${STACK_NAME:-clupilot}/php:latest container_name: ${STACK_NAME:-clupilot}-scheduler restart: unless-stopped command: php artisan schedule:work + environment: *php-env volumes: - - ./app:/var/www/html + - app-storage:/var/www/html/storage depends_on: + mysql: + condition: service_healthy app: condition: service_started networks: [clu] reverb: + build: *php-build image: ${STACK_NAME:-clupilot}/php:latest container_name: ${STACK_NAME:-clupilot}-reverb restart: unless-stopped command: php artisan reverb:start --host=0.0.0.0 --port=8080 + environment: *php-env volumes: - - ./app:/var/www/html + - app-storage:/var/www/html/storage depends_on: + redis: + condition: service_healthy app: condition: service_started networks: [clu] @@ -118,6 +169,7 @@ services: volumes: mysql-data: redis-data: + app-storage: networks: clu: diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile deleted file mode 100644 index 7ab8af1..0000000 --- a/docker/nginx/Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM nginx:1.27-alpine -COPY default.conf /etc/nginx/conf.d/default.conf diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile deleted file mode 100644 index e526a41..0000000 --- a/docker/php/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM php:8.4-fpm-alpine - -RUN apk add --no-cache \ - git curl libpng-dev libzip-dev oniguruma-dev icu-dev \ - autoconf g++ make linux-headers $PHPIZE_DEPS - -RUN docker-php-ext-install \ - pdo_mysql mbstring zip exif pcntl bcmath gd intl sockets - -RUN pecl install redis && docker-php-ext-enable redis - -COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer - -COPY entrypoint.sh /usr/local/bin/entrypoint.sh -RUN chmod +x /usr/local/bin/entrypoint.sh - -WORKDIR /var/www/html - -ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] -CMD ["php-fpm"] diff --git a/docker/php/entrypoint.sh b/docker/php/entrypoint.sh index 9616203..f9621bd 100644 --- a/docker/php/entrypoint.sh +++ b/docker/php/entrypoint.sh @@ -1,8 +1,20 @@ #!/bin/sh set -e -if [ -d /var/www/html/storage ]; then - chmod -R 777 /var/www/html/storage /var/www/html/bootstrap/cache 2>/dev/null || true +cd /var/www/html + +# Laravel needs a .env file to exist; values come from container env. +[ -f .env ] || : > .env + +# Storage volume mounts can come up empty — ensure tree exists + writable. +mkdir -p storage/framework/cache storage/framework/sessions storage/framework/views \ + storage/logs storage/app/public bootstrap/cache +chmod -R 775 storage bootstrap/cache 2>/dev/null || true +chown -R www-data:www-data storage bootstrap/cache 2>/dev/null || true + +# Public storage symlink (idempotent). +if [ ! -L public/storage ] && [ -d storage/app/public ]; then + php artisan storage:link 2>/dev/null || true fi exec "$@" diff --git a/stack.env.example b/stack.env.example index 30a80d5..efe73a6 100644 --- a/stack.env.example +++ b/stack.env.example @@ -1,14 +1,34 @@ # Portainer Stack environment variables. -# In Portainer: Stack → Environment variables → paste these (set real secrets). +# Paste these into Portainer → Stack → Environment variables (Advanced mode), +# then fill in real secrets. Required ones will fail deploy if missing. +# ---- Stack ---- STACK_NAME=clupilot - -# Host port mappings HTTP_PORT=80 MAILPIT_UI_PORT=8025 -# Database (MariaDB) +# ---- Laravel app ---- +APP_NAME=CluPilot +APP_ENV=production +# Generate once locally: cd app && php artisan key:generate --show +APP_KEY=base64:REPLACE_ME +APP_DEBUG=false +APP_URL=http://localhost +LOG_LEVEL=info + +# ---- Database (MariaDB) ---- DB_DATABASE=clupilot DB_USERNAME=clupilot -DB_PASSWORD=clupilot -DB_ROOT_PASSWORD=rYHIlYyiC8jNy167votk8vhvUmXy +DB_PASSWORD=change-me +DB_ROOT_PASSWORD=change-me-root + +# ---- Reverb (websockets) ---- +REVERB_APP_ID=clupilot +REVERB_APP_KEY=change-me-key +REVERB_APP_SECRET=change-me-secret + +# ---- Mail ---- +MAIL_MAILER=log +MAIL_HOST=mailpit +MAIL_PORT=1025 +MAIL_FROM_ADDRESS=no-reply@clupilot.local