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:
@@ -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
@@ -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"]
|
||||||
@@ -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:
|
||||||
@@ -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 "$@"
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user