Compare commits

..

No commits in common. "d8da3ba678ccc0f7aa7110c43d10fd1c89ebd950" and "862b9f6c1632289e90d77d0bbd7e7001244210ab" have entirely different histories.

13 changed files with 112 additions and 383 deletions

View file

@ -1,37 +1,10 @@
node_modules
vendor
.git .git
.gitignore
.gitattributes
.env .env
.env.*
!.env.example
.sisyphus/
.dev.pid
.DS_Store
.editorconfig
.php-cs-fixer.cache
vendor/
node_modules/
tests/
test-results/
phpunit.xml
playwright.config.ts
.idea/
.vscode/
*.swp
*.swo
docs/
README.md
ref/
start_dev.sh
stop_dev.sh
use_local_pp_lib.sh
storage/logs/* storage/logs/*
storage/framework/cache/* storage/framework/cache/*
storage/framework/views/* storage/framework/views/*
storage/framework/sessions/* test-results
tests/e2e/.auth
.dev.pid

View file

@ -1,145 +1,44 @@
# ============================================================================= FROM php:8.4-fpm-alpine
# Stage 1: Build
# Installs all PHP + Node dependencies and builds Vite assets.
# This stage is NOT used in production — only its outputs are copied.
# =============================================================================
FROM php:8.4-cli-bookworm AS build
# System deps for build stage RUN apk add --no-cache \
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
unzip \
zip \
curl \ curl \
git \
zip \
unzip \
libzip-dev \ libzip-dev \
libfreetype6-dev \ imagemagick \
libjpeg62-turbo-dev \ libreoffice \
libpng-dev \ libreoffice-lang-de \
&& rm -rf /var/lib/apt/lists/*
# PHP extensions needed for composer install / post-autoload-dump
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install gd zip
# Install Node.js 20 LTS via NodeSource
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
# ── PHP dependency layer (cached until composer.lock changes) ────────────────
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--optimize-autoloader \
--no-scripts \
--no-interaction \
--prefer-dist
# ── Node dependency layer (cached until package-lock.json changes) ───────────
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
# ── Full source + build ──────────────────────────────────────────────────────
COPY . .
# Run composer post-autoload-dump scripts (package discovery etc.)
RUN composer run-script post-autoload-dump --no-interaction || true
# Build Vite assets (outputs to public/build/)
RUN npm run build
# Copy public/ → public-build/ so the production stage retains built assets
# even after public/ becomes a runtime bind-mount target.
RUN cp -r public/ public-build/
# =============================================================================
# Stage 2: Production
# PHP-FPM runtime image with all system dependencies.
# Node.js is NOT present here.
# =============================================================================
FROM php:8.4-fpm-bookworm AS production
# System packages (excluding Node.js — not needed at runtime)
RUN apt-get update && apt-get install -y --no-install-recommends \
supervisor \
libzip-dev \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
libsqlite3-dev \
default-mysql-client \
ghostscript \ ghostscript \
poppler-utils \ poppler-utils \
sqlite3 \ sqlite \
libfcgi-bin \ sqlite-dev \
unzip \ postgresql-client \
zip \ mysql-client \
curl \ nodejs \
git \ npm \
&& rm -rf /var/lib/apt/lists/* fcgi
# LibreOffice for PowerPoint → PDF conversion (large layer, separate cache) RUN docker-php-ext-install \
RUN apt-get update && apt-get install -y --no-install-recommends \
libreoffice-impress \
libreoffice-l10n-de \
&& rm -rf /var/lib/apt/lists/*
# PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install \
gd \
pdo_sqlite \ pdo_sqlite \
pdo_mysql \ pdo_mysql \
zip \ zip \
bcmath bcmath
# Install Composer (needed for artisan commands, not for installing packages) COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app WORKDIR /app
# ── Copy built application from build stage ────────────────────────────────── COPY build/fpm-healthcheck.conf /usr/local/etc/php-fpm.d/zz-healthcheck.conf
COPY --from=build /app /app COPY build/php-error-logging.conf /usr/local/etc/php-fpm.d/zz-error-logging.conf
COPY build/php-errors.ini /usr/local/etc/php/conf.d/errors.ini
COPY build/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Remove node_modules — not needed at runtime
RUN rm -rf /app/node_modules
# ── Install Docker config files ──────────────────────────────────────────────
# Supervisord (manages php-fpm, queue:work, schedule:work)
COPY build/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# PHP runtime config (upload limits, error logging, timezone)
COPY build/php.ini /usr/local/etc/php/conf.d/zz-app.ini
# FPM pool config (replaces default www pool — sets ping endpoint, clear_env=no)
COPY build/fpm-pool.conf /usr/local/etc/php-fpm.d/zz-www.conf
# FPM error logging to stderr
COPY build/fpm-error-logging.conf /usr/local/etc/php-fpm.d/zz-error-logging.conf
# Remove default FPM pool config (replaced by our zz-www.conf above)
RUN rm -f /usr/local/etc/php-fpm.d/www.conf /usr/local/etc/php-fpm.d/www.conf.default
# ── Boot / Init scripts ───────────────────────────────────────────────────────
RUN chmod +x /app/build/boot-container.sh /app/build/init-app.sh
# ── Runtime setup ─────────────────────────────────────────────────────────────
EXPOSE 9000 EXPOSE 9000
# Healthcheck: FPM ping endpoint (starts checking after 60s to allow boot) HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD SCRIPT_NAME=/ping SCRIPT_FILENAME=/ping REQUEST_METHOD=GET cgi-fcgi -bind -connect 127.0.0.1:9000 | grep -q pong || exit 1
CMD SCRIPT_NAME=/ping SCRIPT_FILENAME=/ping REQUEST_METHOD=GET \
cgi-fcgi -bind -connect 127.0.0.1:9000 2>/dev/null | grep -q "pong" || exit 1
# boot-container.sh runs as root: creates dirs, sets permissions, ENTRYPOINT ["entrypoint.sh"]
# creates DB on first run, runs migrations, runs cache commands, CMD ["php-fpm"]
# syncs built assets to the bind-mounted public/ directory,
# then exec's supervisord (CMD).
ENTRYPOINT ["/app/build/boot-container.sh"]
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View file

@ -1,55 +0,0 @@
#!/bin/bash
set -e
cd /app
echo "[boot] Starting pp-planer container boot..."
# Ensure all required directories exist (volumes may be freshly mounted)
mkdir -p \
storage/logs \
storage/framework/views \
storage/framework/cache/data \
storage/framework/sessions \
storage/app/public \
database \
public
# Run first-time initialization (idempotent — safe to call every boot)
/app/build/init-app.sh
# Fix permissions: www-data must own all writable directories.
# The || true prevents exit on macOS Docker Desktop (bind-mount ownership restrictions).
# On a Linux host running as root, chown will succeed silently.
chown -R www-data:www-data storage bootstrap/cache database 2>/dev/null || true
chmod -R 775 storage bootstrap/cache database 2>/dev/null || true
# Sync built Vite assets from the image (public-build/) into the
# bind-mounted public/ directory. This ensures Caddy always serves
# up-to-date assets even though public/ is a host bind-mount.
if [ -d /app/public-build ]; then
echo "[boot] Syncing built assets to public/..."
cp -r /app/public-build/. /app/public/
chown -R www-data:www-data /app/public 2>/dev/null || true
fi
# Create the storage symlink after volumes are mounted
# (public/storage → storage/app/public)
echo "[boot] Creating storage symlink..."
php artisan storage:link --force 2>/dev/null || true
# Run database migrations
echo "[boot] Running migrations..."
php artisan migrate --force
# Warm up Laravel caches
echo "[boot] Warming caches..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
echo "[boot] Boot complete. Starting supervisord..."
# Hand off to CMD (supervisord)
exec "$@"

26
build/entrypoint.sh Executable file
View file

@ -0,0 +1,26 @@
#!/bin/sh
set -e
if [ -n "$WWWUSER" ] && [ "$WWWUSER" != "0" ]; then
deluser www-data 2>/dev/null || true
adduser -D -u "$WWWUSER" -G www-data www-data 2>/dev/null || true
fi
if [ -n "$WWWGROUP" ] && [ "$WWWGROUP" != "0" ]; then
delgroup www-data 2>/dev/null || true
addgroup -g "$WWWGROUP" www-data 2>/dev/null || true
fi
composer install --no-interaction
npm install
mkdir -p storage/logs storage/framework/views storage/framework/cache storage/framework/sessions
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
if [ "$APP_ENV" = "production" ] || [ "$APP_ENV" = "prod" ]; then
npm run build
fi
php artisan migrate --force
exec "$@"

View file

@ -0,0 +1,3 @@
[www]
ping.path = /ping
ping.response = pong

View file

@ -1,28 +0,0 @@
; PHP-FPM pool configuration for Laravel
[www]
user = www-data
group = www-data
; Listen on TCP (needed for Docker networking with Caddy)
listen = 9000
listen.owner = www-data
listen.group = www-data
; Process management
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 500
; Healthcheck ping endpoint
ping.path = /ping
ping.response = pong
; Pass environment variables from Docker into PHP
clear_env = no
; Access log to /dev/null (access logs bloat container logs)
access.log = /dev/null

View file

@ -1,23 +0,0 @@
#!/bin/bash
set -e
cd /app
DB_PATH="/app/database/database.sqlite"
if [ -f "$DB_PATH" ]; then
echo "[init] Database already exists, skipping first-run init."
exit 0
fi
echo "[init] First run detected — initializing application..."
touch "$DB_PATH"
chmod 664 "$DB_PATH"
if [ -z "${APP_KEY}" ]; then
echo "[init] Generating application key..."
php artisan key:generate --force
fi
echo "[init] First-run init complete."

View file

@ -1,7 +1,5 @@
; FPM worker error logging to stderr (Docker logs)
[www] [www]
catch_workers_output = yes catch_workers_output = yes
decorate_workers_output = no decorate_workers_output = no
php_admin_flag[log_errors] = on php_admin_flag[log_errors] = on
php_admin_value[error_log] = /dev/stderr php_admin_value[error_log] = /proc/self/fd/2

5
build/php-errors.ini Normal file
View file

@ -0,0 +1,5 @@
log_errors = On
error_log = /proc/self/fd/2
error_reporting = E_ALL
display_errors = Off
display_startup_errors = On

View file

@ -1,23 +0,0 @@
; Production PHP configuration for Laravel Docker deployment
; Upload limits (matches UPLOAD_MAX_FILE_SIZE in .env = 100MB)
upload_max_filesize = 100M
post_max_size = 110M
; Memory and execution
memory_limit = 256M
max_execution_time = 300
max_input_time = 300
; Error logging (logs to stderr → Docker logs)
log_errors = On
error_log = /dev/stderr
error_reporting = E_ALL
display_errors = Off
display_startup_errors = Off
; Timezone
date.timezone = Europe/Berlin
; File uploads temp dir
upload_tmp_dir = /tmp

View file

@ -1,54 +0,0 @@
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
pidfile=/var/run/supervisord.pid
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
[rpcinterface:supervisor]
supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface
[program:php-fpm]
command=php-fpm --nodaemonize
autostart=true
autorestart=true
priority=10
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stdout_events_enabled=true
stderr_events_enabled=true
[program:laravel-worker]
command=php /app/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
priority=20
numprocs=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stdout_events_enabled=true
stderr_events_enabled=true
[program:laravel-scheduler]
command=php /app/artisan schedule:work
autostart=true
autorestart=true
user=www-data
priority=20
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stdout_events_enabled=true
stderr_events_enabled=true

View file

@ -37,8 +37,8 @@
'database' => env('DB_DATABASE', database_path('database.sqlite')), 'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '', 'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => 5000, 'busy_timeout' => null,
'journal_mode' => 'wal', 'journal_mode' => null,
'synchronous' => null, 'synchronous' => null,
'transaction_mode' => 'DEFERRED', 'transaction_mode' => 'DEFERRED',
], ],

View file

@ -1,23 +1,3 @@
# =============================================================================
# Caddy configuration (add to your Caddy instance's Caddyfile):
#
# your-domain.example.com {
# root * /path/on/caddy/host/to/pp-planer/public
# php_fastcgi pp-planer-app:9000 {
# root /app/public
# }
# file_server
# encode gzip
# }
#
# Notes:
# - "root *" = path on the Caddy HOST to the bind-mounted ./public directory
# - "root /app/public" inside php_fastcgi = path INSIDE this container
# - "pp-planer-app" resolves via the shared "caddy" Docker network
# - Both Caddy and this container must be on the "caddy" external network
# - The ./public directory is bind-mounted so Caddy can serve static files
# =============================================================================
services: services:
app: app:
build: build:
@ -25,23 +5,54 @@ services:
dockerfile: build/Dockerfile dockerfile: build/Dockerfile
container_name: pp-planer-app container_name: pp-planer-app
restart: unless-stopped restart: unless-stopped
working_dir: /app
environment:
- WWWUSER=${WWWUSER:-1000}
- WWWGROUP=${WWWGROUP:-1000}
- APP_ENV=${APP_ENV}
- APP_DEBUG=${APP_DEBUG}
- APP_KEY=${APP_KEY}
- APP_URL=${APP_URL}
- DB_CONNECTION=${DB_CONNECTION}
- CTS_API_URL=${CTS_API_URL}
- CTS_API_TOKEN=${CTS_API_TOKEN}
- CHURCHTOOLS_URL=${CHURCHTOOLS_URL}
- CHURCHTOOLS_CLIENT_ID=${CHURCHTOOLS_CLIENT_ID}
- CHURCHTOOLS_CLIENT_SECRET=${CHURCHTOOLS_CLIENT_SECRET}
- CHURCHTOOLS_REDIRECT_URI=${CHURCHTOOLS_REDIRECT_URI}
volumes: volumes:
- ./.env:/app/.env:ro - ./:/app
- ./storage:/app/storage - /app/node_modules
- ./database:/app/database - /app/vendor
- ./public:/app/public
expose: expose:
- "9000" - "9000"
networks: networks:
- caddy - caddy
- internal - internal
node:
image: node:20-alpine
container_name: pp-planer-node
restart: unless-stopped
working_dir: /app
environment:
- NODE_ENV=development
volumes:
- ./:/app
- /app/node_modules
expose:
- "5173"
networks:
- caddy
- internal
command: sh -c "npm install && npm run dev"
# mysql: # mysql:
# image: mysql:8.0 # image: mysql:8.0
# container_name: pp-planer-mysql # container_name: pp-planer-mysql
# restart: unless-stopped # restart: unless-stopped
# environment: # environment:
# MYSQL_DATABASE: ${DB_DATABASE:-pp_planer} # MYSQL_DATABASE: ${DB_DATABASE}
# MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} # MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
# MYSQL_PASSWORD: ${DB_PASSWORD} # MYSQL_PASSWORD: ${DB_PASSWORD}
# MYSQL_USER: ${DB_USERNAME} # MYSQL_USER: ${DB_USERNAME}
@ -50,9 +61,6 @@ services:
# networks: # networks:
# - internal # - internal
# volumes:
# mysql_data:
networks: networks:
caddy: caddy:
external: true external: true