In dieser Anleitung zeige ich euch, wie ihr innerhalb weniger Minuten einen “LAMP”-Stack (Apache, MySQL, PHP) mittels Docker und Traefik bereitstellen könnt.
Wir installieren uns also alles notwendige um eine aufwändigere Webseite selbst zu betreiben und zu hosten – inklusive Cron-Jobs und phpMyAdmin.
Updates
Datum | Änderungen |
---|---|
16.08.2023 | Erstellung dieser Anleitung |
03.06.2023 | Anpassung der Traefik Labels, Healthchecks für MariaDB- und PMA-Container hinzugefügt |
1. Vorraussetzung
- Docker mit Docker Compose installiert (Anleitung für Ubuntu / Debian)
- TRAEFIK V2 + 3 – REVERSE-PROXY MIT CROWDSEC IM STACK EINRICHTEN
2. Verzeichnisse anlegen
Als ersten Schritt legen wir uns ein Verzeichnis für den “LAMP”-Stack an:
mkdir -p /opt/containers/lamp/{backups,database,configs,logs,www}
Entsprechende Unterverzeichnisse werden so direkt mit angelegt.
Erklärung für die Unterverzeichnisse:
- backup – Datenbank-Backups werden automatisiert hier abgelegt
- database – permanenter Speicher für unsere Datenbank-Daten
- configs – benutzerdefinierte Anpassungen für z.B. PHP
- logs – Log-Dateien werden hier abgelegt
- www – Verzeichnis für die Daten der Webseite (im jeweiligen Unterordner)
3. Benutzerdefinierte Overrides für die “php.ini”
nano /opt/containers/lamp/configs/php_ini_custom.ini
Inhalt der “php_ini_custom.ini”
date.timezone = Europe/Berlin
Notwendige Anpassungen:
Hier könnt ihr alle Variablen setzen, die ihr für eure Homepage benötigt und die nicht den Default-Werten von PHP entsprechen.
4. Cron-Jobs erstellen
nano /opt/containers/lamp/configs/crontab_custom
Inhalt der “crontab_custom”-Datei
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin 0 0 * * * runitor -uuid UUID -- php /var/www/html/cron/cron.daily.php >> /var/log/cron/cron.log */1 * * * * runitor -uuid UUID -- php /var/www/html/cron/cron.minute.php >> /var/log/cron/cron.log
Notwendige Anpassungen:
Die Cron-Jobs sind natürlich extrem individuell. Hier nur zwei Beispiele wie die Syntax funktionieren würde. Wir nutzen hier Runitor für das Überwachen der Cron-Jobs mit Healthchecks. Wie man das einrichtet findet ihr in dieser Anleitung. Ansonsten einfach ohne Runitor nutzen. 😉
5. Dockerfile für PHP anlegen
nano /opt/containers/lamp/php.Dockerfile
Inhalt der “php.Dockerfile”-Datei
# PHP-Image mit Apache-Server laden FROM php:apache # Erweiterung für SQL-DB installieren RUN docker-php-ext-install mysqli # "php.ini" für Produktion auswählen und kopieren RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" # Änderungen an "php.ini" kopieren COPY configs/php_ini_custom.ini /usr/local/etc/php/conf.d/php_ini_custom.ini RUN chmod 755 /usr/local/etc/php/conf.d/php_ini_custom.ini # "Cron", "tzdata" und "wget" installieren RUN apt-get update && \ apt-get -y install cron tzdata wget # Zeit-Informationen kopieren RUN cp /usr/share/zoneinfo/Europe/Berlin /etc/localtime && \ echo "Europe/Berlin" > /etc/timezone # "Runitor" installieren ARG RUNITOR_VERSION=v0.10.1 RUN wget https://github.com/bdd/runitor/releases/download/$RUNITOR_VERSION/runitor-$RUNITOR_VERSION-linux-amd64 -O runitor && \ mv runitor /usr/local/bin/ && \ chmod +x /usr/local/bin/runitor # Cache des Paket-Managers leeren RUN rm -rf /var/lib/apt/lists/* # "Cron"-Jobs kopieren und Berechtigungen anpassen COPY configs/crontab_custom /etc/cron.d/cron RUN chmod 0644 /etc/cron.d/cron # "Cron"-Jobs ausführen RUN crontab /etc/cron.d/cron # Log-Verzeichnis erstellen RUN mkdir -p /var/log/cron # Entrypoint von Apache bearbeiten und "Cron" ergänzen RUN sed -i 's/^exec /service cron start\n\nexec /' /usr/local/bin/apache2-foreground # Server-Name für Apache ergänzen RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf
Notwendige Anpassungen:
Hier könnt ihr natürlich noch Dinge entfernen, die ihr nicht benötigt (z.B. Runitor oder Cron-Jobs) oder auch noch PHP-Erweiterungen oder andere benötigte Dienste hinzufügen.
6. Anlegen der docker-compose.yml
nano /opt/containers/lamp/docker-compose.yml
Inhalt der docker-compose.yml
version: "3.1" services: lamp-app: image: my_php_apache:latest build: context: ./ dockerfile: php.Dockerfile container_name: lamp-app restart: unless-stopped healthcheck: test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:80"] interval: 30s timeout: 10s retries: 5 volumes: - /etc/localtime:/etc/localtime:ro - ./www/deine_homepage/:/var/www/html/ - ./logs/apache2:/var/log/apache2 - ./logs/cron:/var/log/cron labels: - "traefik.enable=true" - "traefik.http.routers.lamp-app.entrypoints=websecure" - "traefik.http.routers.lamp-app.rule=Host(`web.euredomain.de`)" - "traefik.http.routers.lamp-app.tls=true" - "traefik.http.routers.lamp-app.tls.certresolver=http_resolver" - "traefik.http.routers.lamp-app.middlewares=default@file" - "traefik.http.routers.lamp-app.service=lamp-app" - "traefik.http.services.lamp-app.loadbalancer.server.port=80" - "traefik.docker.network=proxy" depends_on: - lamp-db networks: - proxy - default lamp-backup: image: tiredofit/db-backup container_name: lamp-backup restart: unless-stopped environment: - DB_TYPE=mariadb - DB_HOST=lamp-db - DB_NAME=database_name - DB_USER=db_user - DB_PASS=db_user_pw - DB_DUMP_FREQ=1440 - DB_DUMP_BEGIN=0300 - DB_CLEANUP_TIME=8640 - COMPRESSION=GZ - CONTAINER_ENABLE_MONITORING=FALSE volumes: - /etc/localtime:/etc/localtime:ro - ./backups:/backup depends_on: - lamp-db networks: - default lamp-db: image: mariadb:latest container_name: lamp-db restart: unless-stopped environment: - MARIADB_ROOT_PASSWORD=db_root_pw - MARIADB_DATABASE=database_name - MARIADB_USER=db_user - MARIADB_PASSWORD=db_user_pw volumes: - /etc/localtime:/etc/localtime:ro - ./database:/var/lib/mysql healthcheck: test: ["CMD-SHELL", "mysqladmin ping -P 3306 -p$$MARIADB_ROOT_PASSWORD | grep 'mysqld is alive' || exit 1"] interval: 2s retries: 120 networks: - default lamp-pma: image: phpmyadmin:latest container_name: lamp-pma restart: unless-stopped environment: - PMA_HOST=lamp-db - UPLOAD_LIMIT=10M labels: - "traefik.enable=true" - "traefik.http.routers.lamp-pma.entrypoints=websecure" - "traefik.http.routers.lamp-pma.rule=Host(`pma.euredomain.de`)" - "traefik.http.routers.lamp-pma.tls=true" - "traefik.http.routers.lamp-pma.tls.certresolver=http_resolver" - "traefik.http.routers.lamp-pma.middlewares=default@file" - "traefik.http.routers.lamp-pma.service=lamp-pma" - "traefik.http.services.lamp-pma.loadbalancer.server.port=80" - "traefik.docker.network=proxy" healthcheck: test: ["CMD", "curl", "-Ss", "http://localhost/robots.txt"] start_period: 5s interval: 3s timeout: 60s retries: 10 depends_on: - lamp-db networks: - proxy - default networks: proxy: external: true
Notwendige Anpassungen:
- 1x eure URL für die Webseite (“web.euredomain.de”)
- 1x eure URL für phpMyAdmin (“pma.euredomain.de”)
- 1x Pfad zu euren Homepage-Daten (“www/deine_homepage”)
- 2x DB-Name (“database_name”)
- 2x DB-User (“db_user”)
- 2x DB-User-Passwort (“db_user_pw”)
- 1x DB-Root-Passwort (“db_root_pw”)
Anmerkung zum Backup-Container:
Ich habe hier als vierten Container – neben dem Web-Server, der Datenbank und phpMyAdmin – noch einen Backup-Container hinzugefügt. Dieser macht automatisch ein Backup eurer Datenbank. Die Parameter zum Einstellen der Backups findet ihr hier.
7. PHP-Test-Datei anlegen
Wir legen nun das Verzeichnis für die Homepage an:
mkdir -p /opt/containers/lamp/www/deine_homepage
Bitte passt den Namen des Verzeichnisses (“deine_homepage”) entsprechend an eure Bedürfnisse an. Nun erstellen wir die PHP-Test-Datei:
nano /opt/containers/lamp/www/deine_homepage/php_info.php
Inhalt der “php_info.php”
<?php phpinfo(); ?>
Diese Datei löscht ihr bitte direkt nach Abschluss des Tests wieder von eurem Server.
8. “LAMP”-Stack starten
docker compose -f /opt/containers/lamp/docker-compose.yml up -d --build
Nach einer kurzen Wartezeit könnt ihr nun die gewählte Domain “web.euredomain.de” aufrufen – genauer: https://web.euredomain.de/php_infp.php. Ihr solltet dann die typische PHP-Info-Seite sehen.
9. Zugriff via SFTP einrichten
Zunächst erstellen wir uns eine eigene Gruppe:
groupadd sftp_group
Anschließend fügen wir unseren neuen Nutzer hinzu:
useradd -d /opt/containers/lamp/www -s /bin/false -G sftp_group ftp_user
Dieser User hat entsprechend nur Zugriff auf das WWW-Verzeichnis. Den Namen der Gruppe und des Users passt ihr bitte entsprechend euren Bedürfnissen an. Wir geben dem neuen FTP-User nun noch ein eigenes, sicheres Passwort:
passwd ftp_user
Anschließend bearbeiten wir die SSH-Config:
nano /etc/ssh/sshd_config
Wir kommentieren folgende Zeile aus:
#Subsystem sftp /usr/lib/openssh/sftp-server
Direkt darunter fügen wir folgendes ein:
Subsystem sftp internal-sftp
Weitere Teile der Config hängen von eurer vorhandenen Config ab. Ihr solltet auf alle Fälle den User noch via “AllowUsers” hinzufügen (am Besten bei den weiteren “AllowUsers”-Zeilen):
AllowUsers ftp_user
Am Ende der Datei folgendes einfügen:
Match Group sftp_group ChrootDirectory %h X11Forwarding no AllowTCPForwarding no ForceCommand internal-sftp
Hier ein Ausschnitt aus meiner, funktionierenden SSH-Config (ich nutze ausschließlich den Key-Zugriff mit OTP-Absicherung – passt dies entsprechend an eure Config an):
PermitRootLogin no AllowUsers normal_user AllowUsers ftp_user ChallengeResponseAuthentication yes AuthenticationMethods publickey,password publickey,keyboard-interactive Match Group sftp_group ChrootDirectory %h X11Forwarding no AllowTCPForwarding no ForceCommand internal-sftp Match User ftp_user PasswordAuthentication yes AuthenticationMethods password Match User normal_user PasswordAuthentication no
Damit die SSH-Config wirksam wird, müsst ihr den Dienst neu starten:
service sshd restart
Lasst unbedingt eure aktuelle Session offen und testet parallel in einer neuen Session alle eure Zugriffe – nicht dass ihr euch vom Server selbst aussperrt. Ich muss hier ein wenig probieren, bis ich eine funktionierende Config hatte.
Anschließend fügen wir den FTP-User der WWW-Usergruppe hinzu:
usermod -a -G www-data ftp_user
Damit der FTP-Zugriff auch wunschgemäß funktioniert, muss der Ordner über den der FTP-User zugreift “root” gehören und die entsprechenden Zugriffsrechte besitzen (anders geht es nicht, habe ich ausgiebig getestet 😉 ):
chown root:root /opt/containers/lamp/www/ chmod -R 755 /opt/containers/lamp/www/
Danach setzen wir noch diese Berechtigungen für Ordner und Dateien:
chown www-data:www-data -R /opt/containers/lamp/www/* find /opt/containers/lamp/www/* -type d -exec chmod 775 {} \; find /opt/containers/lamp/www/* -type f -exec chmod 664 {} \; find /opt/containers/lamp/www/* -type d -exec chmod g+s {} \;
Ohne diese Schritte hatte ich Probleme beim Anlegen und Bearbeiten der PHP-Dateien. Daher habe ich diese Anpassungen vorgenommen. Ich habe damit ausgiebig hin und her getestet. Damit auch bei neu angelegten Dateien und Ordnern (die nicht via FTP-User angelegt werden) keine Probleme entstehen habe ich mir noch ein kleines Cron-Skript geschrieben. Es läuft jede Stunde.
nano /opt/containers/lamp/fix_permissions.sh
Inhalt der “fix_permissions.sh”
#!/bin/bash chown www-data:www-data -R /opt/containers/lamp/www/* find /opt/containers/lamp/www/* -type d -exec chmod 775 {} \; find /opt/containers/lamp/www/* -type f -exec chmod 664 {} \; find /opt/containers/lamp/www/* -type d -exec chmod g+s {} \;
Zum Schluss die Datei noch ausführbar machen:
chmod +x /opt/containers/lamp/fix_permissions.sh
10. Quellen und Fazit
Leider habe ich beim Probieren und Testen mit dem “LAMP”-Stack nicht genau notiert wo ich überall quer gelesen habe. Ich hatte viel gelesen und gegoogelt. Das einzige was ich mir noch notiert hatte ist diese Quelle hier (entspricht dem Schritt 9).
Ich hoffe ich konnte euch mit meiner Anleitung helfen. Mir hätte sie sehr weitergeholfen. Ich gebe aber zu, dass sehr viel etwas spezialisiert ist. Hoffentlich könnt ihr trotzdem ein paar Dinge für euch daraus ziehen. 🙂
In meinem laufenden LAMP Stack fehlt ausgerechnet die PHP GD Erweiterung.
Im laufenden Container kann ich mit:
[code]
apt-get update && apt-get install -y \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
&& docker-php-ext-configure gd –with-freetype –with-jpeg \
&& docker-php-ext-install -j$(nproc) gd
[/code]
ganz hervorragend GD nach-instaliieren.
Doch wie geht es dann weiter? Wenn ich jetzt ein ‘service restart apache2’ aufrufe, wird der Container neu gestartet und die Änderungen gehen verloren.
Wie kann ich nun die Änderungen permanent im Image übernehmen und so ein PHP mit GD erhalten?
####
Den ersten Teil selbst gelöst, doch wie erzeuge ich nun die Persistenz?
Das Image ist angeblich ein Ubuntu und wenn ich im Container nachschaue, ist’s plötzlich ein Debian Bullseye?!
Statt dem service restart im laufenden Container durchzuführen, verließ ich den laufenden Container und machte simpel und einfach ein ‘docker-compose restart’.
Jetzt wurde GD tatsächlich installiert! – Aber wie kann ich nun diesen Zustand abspeichern? Beim nächsten Neustart ist der Container ja wieder ohne diese Änderungen.
Gibt es da eine Chance eine Art erweitertes Image zu bauen und dieses dann zu verwenden?
Noch ein Hinweis zum Dockerfile:
da php:apache auf debian basiert, ist Zeile 29 im Dockerfile wahrscheinlich falsch (diese gilt für auf alpine basierende Container). Richtig wäre IMO:
RUN rm -rf /var/lib/apt/lists/*
Das führt letztendlich nur zu einem etwas kleinerem Image, aber schadet ja nicht 😉
Sonst natürlich Danke für eure unermüdliche Arbeit 👍