Vorbemerkung: warum MySQL 5.7 in Docker
Wir brauchten MySQL 5.7 in Docker auf einem Server, der sonst mit einer aktuellen Distribution läuft. Einige Altanwendungen sind nicht mit MySQL 8 kompatibel (geänderte Standard-Authentifizierung, strengere SQL-Modi, neue reservierte Wörter), und sie kurzfristig zu migrieren kam nicht in Frage.

Eine so alte Version direkt auf dem Host zu installieren, bringt mehrere Probleme. Die offiziellen Repositories aktueller Distributionen bieten sie nicht mehr an. Pakete von Drittanbietern können mit dem Rest des Systems in Konflikt geraten, und ein OS-Update könnte sie zerstören.
Wir haben uns deshalb entschieden, sie in einem Docker-Container zu isolieren. Der Host bleibt sauber und aktuell, die Datenbankversion ist festgelegt und reproduzierbar. Daten und Konfiguration liegen in einem Ordner, der sich leicht sichern und verschieben lässt.
Dieser Artikel erzählt, wie wir sie eingerichtet haben und vor allem, auf welche Hindernisse wir unterwegs gestoßen sind. Es sind die unbeabsichtigte Freigabe des Ports 3306 und ein iptables-Fehler beim Neustart. Dazu kam das Schreiben einer Firewall mit nftables, die nur autorisierte IPs durchlässt.
Hinweis: MySQL 5.7 hat im Oktober 2023 das Ende des Supports (EOL) erreicht und erhält keine Sicherheitspatches mehr. Es ist eine Übergangslösung: Das Ziel bleibt die Migration auf MySQL 8.x oder MariaDB. Gerade deshalb ist es umso wichtiger, es nicht offen zu lassen.
1. Ordnerstruktur
/opt/mysql57/
├── docker-compose.yml
├── .env
├── conf.d/ # configurazioni personalizzate (my.cnf aggiuntivi)
├── data/ # dati del database (volume persistente)
└── logs/
Die Datei .env enthält die Zugangsdaten, damit sie nicht im Compose landen.
TZ=Europe/Rome
MYSQL_ROOT_PASSWORD=una_password_molto_lunga_e_casuale
MYSQL_DATABASE=nome_db
MYSQL_USER=nome_utente
MYSQL_PASSWORD=altra_password_robusta
Schützen Sie sie mit chmod 600 .env.
2. Die docker-compose.yml
services:
mysql57:
image: mysql:5.7
container_name: mysql57
restart: unless-stopped
env_file:
- .env
ports:
- "3306:3306"
environment:
TZ: "${TZ:-Europe/Rome}"
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
MYSQL_DATABASE: "${MYSQL_DATABASE}"
MYSQL_USER: "${MYSQL_USER}"
MYSQL_PASSWORD: "${MYSQL_PASSWORD}"
command:
- --default-authentication-plugin=mysql_native_password
volumes:
- ./data:/var/lib/mysql
- ./conf.d:/etc/mysql/conf.d:ro
- ./logs:/var/log/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
interval: 20s
timeout: 5s
retries: 10
start_period: 45s
ulimits:
nofile:
soft: 65535
hard: 65535
Einige Entscheidungen, die man beachten sollte.
restart: unless-stopped: Die Datenbank startet nach einem Neustart des Servers von selbst wieder.- Volumes als Bind-Mount: Daten, Konfiguration und Logs liegen auf dem Host, Backups und Migrationen sind also einfache Ordnerkopien.
- Healthcheck: erlaubt anderen Diensten zu warten, bis MySQL wirklich bereit ist (
$$sorgt dafür, dass die Variable im Container und nicht von Compose aufgelöst wird). ulimits nofile: vermeidet den Fehler „too many open files“ unter Last.
3. Erstes Problem: Der Port von MySQL 5.7 in Docker ist für die ganze Welt offen
Die Zeile - "3306:3306" entspricht 0.0.0.0:3306:3306: Docker veröffentlicht den Port auf allen Schnittstellen des Hosts, auch der öffentlichen.
Das Heimtückischste ist, dass Docker seine eigenen Regeln direkt in die Firewall des Kernels schreibt und diese vor denen von UFW oder firewalld ausgewertet werden. Eine aktive Firewall genügt nicht: Der Port bleibt erreichbar. Die Docker-Dokumentation beschreibt dieses Verhalten unter Packet filtering and firewalls.
Die möglichen Wege sind.
- Nur localhost:
"127.0.0.1:3306:3306", wenn die Datenbank nur vom Host oder über einen SSH-Tunnel genutzt wird. - Kein veröffentlichter Port: Sind die Clients andere Container, genügt ein gemeinsames Docker-Netzwerk.
- Bestimmte IP: nur auf einer Adresse eines privaten Netzes/VPN veröffentlichen.
- Öffentlicher Port mit IP-Filter: unser Fall, weil sich ein externer Client (eine phpMyAdmin-Instanz auf einem anderen Server) verbinden können muss.
Für Fall 4 braucht man eine Firewall, die den Verkehr der von Docker veröffentlichten Ports korrekt filtert.
4. Zweites Problem: iptables-Fehler beim Start
Nachdem wir die Firewall angefasst hatten, lieferte docker compose up.
failed to create network mysql57_default: Error response from daemon:
Failed to Setup IP tables: Unable to enable ACCEPT OUTGOING rule:
(iptables failed: iptables --wait -t filter -A DOCKER-FORWARD -i br-... -j ACCEPT:
iptables: No chain/target/match by that name.
Der Fehler hängt nicht vom Compose ab: Die Kette DOCKER-FORWARD (in neueren Docker-Versionen eingeführt) existierte nicht mehr. Sie war von etwas gelöscht worden, das die Regeln nach dem Start des Daemons bereinigt hatte. Ein Neustart von Docker (systemctl restart docker) legt sie neu an, doch das Problem kehrt zurück, bis der wahre Schuldige beseitigt ist.
Der Schuldige: flush ruleset
Unsere /etc/nftables.conf begann mit.
flush ruleset
Dieser Befehl löscht alle nftables-Tabellen, auch die von Docker angelegten. Außerdem enthielt die Datei eine statische Kopie der Docker-Regeln, mit von Hand eingetragenem Bridge-Namen (br-278550235c3f) und Container-IP (172.18.0.2). Der Bridge-Name ändert sich jedoch jedes Mal, wenn das Compose-Netzwerk neu erstellt wird. Die Kopie war also sofort veraltet, und die Tabelle ip nat geriet in Konflikt mit der von Docker verwalteten.
Goldene Regel: Mit Docker verwendet man in der nftables-Datei kein flush ruleset und kopiert keine seiner Regeln. Man verwaltet nur seine eigene Firewall, in eigenen Tabellen.
5. Die Firewall mit nftables für MySQL 5.7 in Docker
Das Prinzip: Die von Docker veröffentlichten Ports werden mit DNAT umgeschrieben und dann vom Host an den Container weitergeleitet, durchlaufen also die Kette forward, nicht input. Eine Regel drop in einer eigenen Kette forward mit etwas niedrigerer Priorität (also zuerst ausgewertet) verwirft das Paket unabhängig von den accept-Regeln von Docker.
Um fremde Tabellen nicht anzufassen, verwenden wir das Muster table ... + delete table ...: Es macht die Datei ohne Fehler und ohne flush wiederholt ausführbar.
/etc/nftables.conf.
#!/usr/sbin/nft -f
# NIENTE "flush ruleset": cancellerebbe anche le tabelle di Docker
# ---------------- firewall host ----------------
table inet filter
delete table inet filter
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iifname "lo" accept
ip6 nexthdr icmpv6 accept
iifname "enp7s0" accept # rete privata
ip saddr 203.0.113.20 tcp dport 22 accept # SSH da host fidato
ip saddr 203.0.113.30 tcp dport 22 accept # SSH da VPN
ip saddr 203.0.113.20 icmp type echo-request accept
tcp dport { 80, 443 } accept
}
chain output {
type filter hook output priority filter; policy accept;
}
}
# ---------------- guardia MySQL (porta pubblicata da Docker) ----------------
table ip mysql_guard
delete table ip mysql_guard
table ip mysql_guard {
set allowed {
type ipv4_addr
flags interval
elements = {
203.0.113.10 # phpMyAdmin
}
}
chain forward {
type filter hook forward priority filter - 5; policy accept;
iifname "enp7s0" accept
ct status dnat tcp dport 3306 ip saddr @allowed accept
ct status dnat tcp dport 3306 counter drop
}
}
Wie die Schutzregel funktioniert.
ct status dnatwählt nur Pakete aus, die durch DNAT gelaufen sind, also die für die von Docker veröffentlichten Ports bestimmten.- Die erste Regel akzeptiert, wer im Set
allowedist; die zweite verwirft alle anderen und zählt sie (counter). - Lokaler Verkehr (
127.0.0.1) und Verkehr zwischen Containern im selben Netzwerk läuft nicht über das Forward des Routers und wird daher nicht berührt.
Kompatibilität: In einer ersten Version verwendeten wir
ct original proto-dst 3306, aber einige Versionen vonnftantworten mit „Can’t parse symbolic invalid expressions“. Im Hookforwardist das DNAT bereits erfolgt und der Container-Port entspricht dem veröffentlichten (3306:3306). Daher isttcp dport 3306zusammen mitct status dnatgleichwertig und funktioniert überall. Ändern Sie das Mapping (z. B.3307:3306), muss die Regel an den internen Port angepasst werden.
Änderungen anwenden, ohne sich auszusperren
Halten Sie eine zweite SSH-Sitzung als Reserve offen, dann.
nft -c -f /etc/nftables.conf # solo verifica della sintassi
nft delete table ip nat # una tantum: elimina la vecchia copia statica
nft -f /etc/nftables.conf
systemctl restart docker # Docker ricrea le sue tabelle pulite
cd /opt/mysql57 && docker compose up -d
Damit es beim Booten geladen wird, stellen Sie sicher, dass der Dienst aktiv ist (systemctl enable --now nftables) und dass /etc/nftables.conf kein flush ruleset enthält.
IPs zur Laufzeit verwalten
nft add element ip mysql_guard allowed { 203.0.113.50 }
nft delete element ip mysql_guard allowed { 203.0.113.50 }
Um es dauerhaft zu machen, aktualisieren Sie auch das Set in der Datei.
6. Überprüfung
Vom autorisierten Server.
nc -zv IP_PUBBLICO_DB 3306 # deve rispondere "succeeded"
Von jeder anderen IP.
nmap -Pn -p 3306 IP_PUBBLICO_DB # deve risultare "filtered" / timeout
Auf dem Server muss der Zähler der Drop-Regel bei jedem nicht autorisierten Versuch wachsen.
nft list chain ip mysql_guard forward
ss -tlnp | grep 3306
7. Zweite Barriere: die MySQL-Benutzer
Die Firewall ersetzt nicht die Berechtigungen der Datenbank. Legen Sie den Benutzer mit auf die IP des Clients beschränktem Host an.
CREATE USER 'pma_user'@'203.0.113.10' IDENTIFIED BY 'password_robusta';
GRANT ALL ON nome_db.* TO 'pma_user'@'203.0.113.10';
FLUSH PRIVILEGES;
Gelingt die TCP-Verbindung, MySQL antwortet aber Access denied, liegt das Problem hier und nicht in der Firewall.
8. Was man sich zu MySQL 5.7 in Docker merken sollte
- Docker und die Host-Firewall ignorieren sich nicht: Zu verstehen, welche Kette der Verkehr durchläuft (
inputoderforward), ist der Schlüssel zu allem. - Niemals
flush rulesetauf einem Host mit Docker; verwenden Sie eigene Tabellen. - Kopieren Sie keine von Docker erzeugten Regeln in Ihre Dateien: Bridge-Namen und IPs ändern sich.
- Haben Sie den Port schon freigegeben, prüfen Sie die Logs in
./logsund erwägen Sie, die Passwörter zu ändern, vor allem das von root. - Wo möglich, ziehen Sie ein VPN (WireGuard) oder einen SSH-Tunnel der öffentlichen Freigabe einer Datenbank vor.
- Planen Sie die Migration von MySQL 5.7 auf eine unterstützte Version.
Haben Sie Fragen oder eine andere Lösung für dasselbe Problem? Schreiben Sie es in die Kommentare. Zum Thema MySQL und Debian lesen Sie auch: MySQL startet nach dem Debian-12-Upgrade nicht mehr.
Schreibe einen Kommentar