build: Docker-Setup und Apache-Konfiguration

Apache mit mod_php statt PHP-FPM plus Nginx: dadurch verhalten sich
Docker und klassisches Shared Hosting gleich, es gilt dieselbe
.htaccess. MariaDB liegt hinter einem Compose-Profil.

Die Entwicklungspakete bleiben im Image installiert - beim Aufraeumen
mit apt-get purge --auto-remove verschwindet sonst auch die von imagick
benoetigte Laufzeitbibliothek.

Die .htaccess im Stammverzeichnis wird nur gebraucht, wenn der Webroot
nicht auf public/ gelegt werden kann; sie sperrt dann alle uebrigen
Verzeichnisse.
This commit is contained in:
2026-08-03 18:11:40 +02:00
parent 8a7939bee2
commit f4aece9557
5 changed files with 258 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
# =============================================================
# urlshort-qr-gen – .htaccess fuer das Projekt-Stammverzeichnis
#
# Diese Datei wird NUR gebraucht, wenn beim Hoster der Webroot
# nicht auf /public gelegt werden kann (Shared-Hosting-Variante B
# in der README). Dann liegt das komplette Projekt im Webroot und
# diese Regeln leiten intern nach public/ weiter – gleichzeitig
# werden alle uebrigen Ordner gesperrt.
#
# Liegt der Webroot korrekt auf /public, ist diese Datei
# wirkungslos (sie liegt dann ausserhalb des Webroots).
# =============================================================
Options -Indexes
AddDefaultCharset UTF-8
<IfModule mod_rewrite.c>
RewriteEngine On
# 1) Nicht-oeffentliche Verzeichnisse hart sperren.
RewriteRule ^(src|templates|migrations|tests|docker|data|vendor|\.git)(/|$) - [F,L]
# 2) Konfigurations- und Projektdateien sperren.
RewriteRule ^(\.env|composer\.(json|lock)|phpunit\.xml|Dockerfile|docker-compose\.ya?ml|.*\.md)$ - [F,L]
# 3) Bereits nach public/ aufgeloeste Anfragen nicht erneut umschreiben.
RewriteCond %{REQUEST_URI} !^/?public/
RewriteRule ^(.*)$ public/$1 [L]
</IfModule>
# Zusaetzlicher Schutz, falls mod_rewrite fehlt.
<FilesMatch "^(\.env.*|composer\.(json|lock)|phpunit\.xml|.*\.md|Dockerfile)$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>
</FilesMatch>
+75
View File
@@ -0,0 +1,75 @@
# =============================================================
# urlshort-qr-gen – Apache + mod_php
# Bewusst dasselbe Setup wie klassisches Shared Hosting, damit
# sich Docker und FTP-Deployment identisch verhalten (.htaccess).
# =============================================================
FROM php:8.3-apache
# --- Systempakete & PHP-Erweiterungen -------------------------
# gd -> PNG-Ausgabe der QR-Codes
# intl -> Umlaut-Domains (IDN) nach UTS-46; optional, es gibt einen
# eigenen Punycode-Rueckfallweg fuer Hosting ohne intl
# pdo_mysql -> optionaler MySQL/MariaDB-Betrieb (SQLite ist eingebaut)
# imagick -> rastert SVG-Logos fuer die PNG-Ausgabe (optional)
#
# Die Entwicklungspakete bleiben absichtlich installiert: beim Aufraeumen
# mit "apt-get purge --auto-remove" verschwindet sonst auch die von
# imagick benoetigte Laufzeitbibliothek libMagickWand. Das Image ist
# dadurch groesser, aber die Erweiterungen laden zuverlaessig.
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
libpng-dev libjpeg62-turbo-dev libfreetype6-dev \
libzip-dev libicu-dev libmagickwand-dev unzip; \
docker-php-ext-configure gd --with-freetype --with-jpeg; \
docker-php-ext-install -j"$(nproc)" gd intl pdo_mysql zip; \
{ pecl install imagick && docker-php-ext-enable imagick; } || \
echo "Hinweis: imagick konnte nicht installiert werden – SVG-Logos wirken dann nur im SVG-Download."; \
rm -rf /var/lib/apt/lists/* /tmp/pear; \
php -m | grep -q '^gd$'; \
php -m | grep -q '^pdo_mysql$'
# --- Apache ---------------------------------------------------
RUN set -eux; \
a2enmod rewrite headers expires; \
sed -ri 's!/var/www/html!/var/www/html/public!g' /etc/apache2/sites-available/000-default.conf; \
printf '%s\n' \
'<Directory /var/www/html/public>' \
' Options -Indexes +FollowSymLinks' \
' AllowOverride All' \
' Require all granted' \
'</Directory>' \
'ServerTokens Prod' \
'ServerSignature Off' \
> /etc/apache2/conf-available/urlshort.conf; \
a2enconf urlshort
COPY docker/php.ini /usr/local/etc/php/conf.d/urlshort.ini
# --- Composer -------------------------------------------------
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
# Abhaengigkeiten zuerst: bleibt beim Aendern des Codes im Cache.
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --no-progress --optimize-autoloader --no-scripts
COPY . .
RUN set -eux; \
mkdir -p data/uploads; \
chown -R www-data:www-data data; \
chmod -R 775 data
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD php -r 'exit(@file_get_contents("http://127.0.0.1/login") === false ? 1 : 0);'
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["apache2-foreground"]
+78
View File
@@ -0,0 +1,78 @@
# =============================================================
# urlshort-qr-gen – Docker Compose
#
# Schnellstart (SQLite, nichts weiter noetig):
# docker compose up -d --build
# -> http://localhost:8080
#
# Mit MariaDB statt SQLite:
# docker compose --profile mysql up -d --build
# (vorher in der .env DB_DRIVER=mysql und DB_HOST=db setzen)
# =============================================================
services:
app:
build:
context: .
dockerfile: Dockerfile
image: urlshort-qr-gen:latest
container_name: urlshort-app
restart: unless-stopped
ports:
- "${APP_PORT:-8080}:80"
environment:
# Diese Werte haben Vorrang vor Defaults, aber NICHT vor einer
# gemounteten .env-Datei. Fuer Docker daher entweder hier
# konfigurieren oder eine .env mitliefern - nicht beides.
APP_BASE_URL: "${APP_BASE_URL:-http://localhost:8080}"
APP_ENV: "${APP_ENV:-production}"
APP_TIMEZONE: "${APP_TIMEZONE:-Europe/Berlin}"
REDIRECT_STATUS: "${REDIRECT_STATUS:-302}"
DB_DRIVER: "${DB_DRIVER:-sqlite}"
DB_SQLITE_PATH: "${DB_SQLITE_PATH:-data/database.sqlite}"
DB_HOST: "${DB_HOST:-db}"
DB_PORT: "${DB_PORT:-3306}"
DB_NAME: "${DB_NAME:-urlshort}"
DB_USER: "${DB_USER:-urlshort}"
DB_PASSWORD: "${DB_PASSWORD:-urlshort}"
# Leer lassen -> beim ersten Aufruf startet der Setup-Assistent.
ADMIN_USER: "${ADMIN_USER:-admin}"
ADMIN_PASSWORD_HASH: "${ADMIN_PASSWORD_HASH:-}"
SESSION_SECURE: "${SESSION_SECURE:-false}"
TRACK_IP: "${TRACK_IP:-none}"
IP_HASH_SALT: "${IP_HASH_SALT:-}"
VISIT_RETENTION_DAYS: "${VISIT_RETENTION_DAYS:-365}"
volumes:
# Datenbank und hochgeladene Logos ueberleben Neubauten.
- app-data:/var/www/html/data
healthcheck:
test: ["CMD", "php", "-r", "exit(@file_get_contents('http://127.0.0.1/login') === false ? 1 : 0);"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
db:
image: mariadb:11
container_name: urlshort-db
restart: unless-stopped
profiles: ["mysql"]
environment:
MARIADB_DATABASE: "${DB_NAME:-urlshort}"
MARIADB_USER: "${DB_USER:-urlshort}"
MARIADB_PASSWORD: "${DB_PASSWORD:-urlshort}"
MARIADB_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:-root-geheim}"
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
volumes:
app-data:
db-data:
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh
# -------------------------------------------------------------
# Startskript des Containers.
# Sorgt dafuer, dass die Datenverzeichnisse existieren und dem
# Webserver gehoeren - auch wenn ein Volume gemountet wurde.
# -------------------------------------------------------------
set -e
mkdir -p /var/www/html/data/uploads
if [ "$(id -u)" = "0" ]; then
chown -R www-data:www-data /var/www/html/data 2>/dev/null || true
fi
# Wartet kurz auf die Datenbank, falls MySQL/MariaDB genutzt wird.
if [ "${DB_DRIVER}" = "mysql" ] && [ -n "${DB_HOST}" ]; then
echo "Warte auf Datenbank ${DB_HOST}:${DB_PORT:-3306} ..."
i=0
while [ "$i" -lt 30 ]; do
if php -r '
$host = getenv("DB_HOST"); $port = (int) (getenv("DB_PORT") ?: 3306);
$fp = @fsockopen($host, $port, $errno, $errstr, 1);
exit($fp === false ? 1 : 0);
'; then
echo "Datenbank erreichbar."
break
fi
i=$((i + 1))
sleep 1
done
fi
exec "$@"
+32
View File
@@ -0,0 +1,32 @@
; -------------------------------------------------------------
; urlshort-qr-gen – PHP-Einstellungen im Container
; -------------------------------------------------------------
expose_php = Off
; Logos duerfen bis 2 MB gross sein (siehe UPLOAD_MAX_BYTES).
upload_max_filesize = 4M
post_max_size = 8M
memory_limit = 256M
max_execution_time = 30
; Fehler ins Log, nicht in die Ausgabe.
display_errors = Off
log_errors = On
error_log = /dev/stderr
; Sessions
session.cookie_httponly = On
session.cookie_samesite = "Lax"
session.use_strict_mode = On
session.use_only_cookies = On
; Zeitzone (kann per APP_TIMEZONE ueberschrieben werden)
date.timezone = Europe/Berlin
; OPcache fuer eine schnelle Weiterleitung
opcache.enable = 1
opcache.memory_consumption = 96
opcache.max_accelerated_files = 4000
opcache.validate_timestamps = 1
opcache.revalidate_freq = 15