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.
master
Boban Blaskovic 2026-05-31 17:44:35 +02:00
parent 6681df5a10
commit 87f41387ce
7 changed files with 185 additions and 40 deletions

27
.dockerignore Normal file
View File

@ -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

56
Dockerfile Normal file
View File

@ -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

View File

@ -1,17 +1,53 @@
name: ${STACK_NAME:-clupilot} 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: services:
nginx: nginx:
build: ./docker/nginx build:
context: .
dockerfile: Dockerfile
target: nginx
image: ${STACK_NAME:-clupilot}/nginx:latest image: ${STACK_NAME:-clupilot}/nginx:latest
container_name: ${STACK_NAME:-clupilot}-nginx container_name: ${STACK_NAME:-clupilot}-nginx
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${HTTP_PORT:-80}:80" - "${HTTP_PORT:-80}:80"
volumes:
- ./app:/var/www/html
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on: depends_on:
app: app:
condition: service_started condition: service_started
@ -25,12 +61,13 @@ services:
retries: 3 retries: 3
app: app:
build: ./docker/php build: *php-build
image: ${STACK_NAME:-clupilot}/php:latest image: ${STACK_NAME:-clupilot}/php:latest
container_name: ${STACK_NAME:-clupilot}-app container_name: ${STACK_NAME:-clupilot}-app
restart: unless-stopped restart: unless-stopped
environment: *php-env
volumes: volumes:
- ./app:/var/www/html - app-storage:/var/www/html/storage
depends_on: depends_on:
mysql: mysql:
condition: service_healthy condition: service_healthy
@ -39,37 +76,51 @@ services:
networks: [clu] networks: [clu]
queue: queue:
build: *php-build
image: ${STACK_NAME:-clupilot}/php:latest image: ${STACK_NAME:-clupilot}/php:latest
container_name: ${STACK_NAME:-clupilot}-queue container_name: ${STACK_NAME:-clupilot}-queue
restart: unless-stopped restart: unless-stopped
command: php artisan queue:work --tries=3 --timeout=120 command: php artisan queue:work --tries=3 --timeout=120
environment: *php-env
volumes: volumes:
- ./app:/var/www/html - app-storage:/var/www/html/storage
depends_on: depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
app: app:
condition: service_started condition: service_started
networks: [clu] networks: [clu]
scheduler: scheduler:
build: *php-build
image: ${STACK_NAME:-clupilot}/php:latest image: ${STACK_NAME:-clupilot}/php:latest
container_name: ${STACK_NAME:-clupilot}-scheduler container_name: ${STACK_NAME:-clupilot}-scheduler
restart: unless-stopped restart: unless-stopped
command: php artisan schedule:work command: php artisan schedule:work
environment: *php-env
volumes: volumes:
- ./app:/var/www/html - app-storage:/var/www/html/storage
depends_on: depends_on:
mysql:
condition: service_healthy
app: app:
condition: service_started condition: service_started
networks: [clu] networks: [clu]
reverb: reverb:
build: *php-build
image: ${STACK_NAME:-clupilot}/php:latest image: ${STACK_NAME:-clupilot}/php:latest
container_name: ${STACK_NAME:-clupilot}-reverb container_name: ${STACK_NAME:-clupilot}-reverb
restart: unless-stopped restart: unless-stopped
command: php artisan reverb:start --host=0.0.0.0 --port=8080 command: php artisan reverb:start --host=0.0.0.0 --port=8080
environment: *php-env
volumes: volumes:
- ./app:/var/www/html - app-storage:/var/www/html/storage
depends_on: depends_on:
redis:
condition: service_healthy
app: app:
condition: service_started condition: service_started
networks: [clu] networks: [clu]
@ -118,6 +169,7 @@ services:
volumes: volumes:
mysql-data: mysql-data:
redis-data: redis-data:
app-storage:
networks: networks:
clu: clu:

View File

@ -1,2 +0,0 @@
FROM nginx:1.27-alpine
COPY default.conf /etc/nginx/conf.d/default.conf

View File

@ -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"]

View File

@ -1,8 +1,20 @@
#!/bin/sh #!/bin/sh
set -e set -e
if [ -d /var/www/html/storage ]; then cd /var/www/html
chmod -R 777 /var/www/html/storage /var/www/html/bootstrap/cache 2>/dev/null || true
# 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 fi
exec "$@" exec "$@"

View File

@ -1,14 +1,34 @@
# Portainer Stack environment variables. # 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 STACK_NAME=clupilot
# Host port mappings
HTTP_PORT=80 HTTP_PORT=80
MAILPIT_UI_PORT=8025 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_DATABASE=clupilot
DB_USERNAME=clupilot DB_USERNAME=clupilot
DB_PASSWORD=clupilot DB_PASSWORD=change-me
DB_ROOT_PASSWORD=rYHIlYyiC8jNy167votk8vhvUmXy 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