Database in Docker of op de host draaien?
Is het verstandig om PostgreSQL, MySQL of Redis in Docker te draaien? Ontdek de operationele risico's bij volumes, upgrades en back-ups voor een stabiele productieomgeving.
Moet de database in Docker of op de host draaien?
Draai de database in Docker. Voor een applicatiestack op één VPS is een gecontaineriseerde PostgreSQL, MySQL, MongoDB of Redis een normale keuze voor productieomgevingen, en de discussie die hierover gevoerd wordt, gaat meestal over de verkeerde punten. Een container is een Linux-proces met namespaces en cgroups eromheen, geen virtuele machine; er is dus geen hypervisor tussen de database en de schijf. Met een bind mount of een lokaal named volume komen lees- en schrijfacties direct op het bestandssysteem van de host terecht, hetzelfde bestandssysteem dat een pakketinstallatie zou gebruiken.
De werkelijke kosten zijn operationeel. Vier factoren bepalen of deze opzet in orde is of uitdraait op een langzaam drama: waar de data staat, wie de eigenaar is van die map, hoe een grote versie-upgrade verloopt en of u ooit een back-up heeft teruggezet. Als u die zaken goed regelt, is de container slechts een detail. Als u ze verkeerd regelt, zult u de container de schuld geven.
Dit is dezelfde beslissing voor elke serverdatabase. De onderstaande voorbeelden gebruiken PostgreSQL, MySQL, MongoDB en Redis, en de productspecifieke verschillen worden benoemd waar ze relevant zijn.
Wat een container daadwerkelijk verandert
Niet het opslagpad, zolang u een volume koppelt. Dezelfde kernel, dezelfde page cache, hetzelfde bestandssysteem.
Er is één reëel prestatieprobleem, en dat is het geval waarin u niets koppelt. Zonder volume komt de datamap in de beschrijfbare laag van de container terecht; dit is een overlay-bestandssysteem dat boven op de image is geplaatst. Schrijfacties zijn daar trager en de gehele laag wordt verwijderd wanneer de container wordt verwijderd. Dat is de oorzaak van "mijn database was vanmorgen leeg".
Wat daadwerkelijk verandert:
- De levenscyclus.
docker compose downvernietigt de container. Alles wat niet in een volume stond, gaat daarmee verloren. - De versie. De image-tag is de versie. Er is geen
apt upgradebinnen een databasecontainer die een volgendedocker compose pulloverleeft. - Geheugenbeheer. Een cgroup-limiet is een harde grens die door de kernel wordt afgedwongen, en de database is zich hier niet van bewust.
- De gebruiker. Het proces draait als een numeriek user id binnen de container, dat mogelijk niets bezit op uw host.
Waar de data staat, bepaalt alles
Er zijn twee goede opties en één veelgemaakte fout.
- Een named volume:
pgdata:/var/lib/postgresql/data. Docker maakt de map aan op/var/lib/docker/volumes/<project>_pgdata/_dataen het entrypoint van de image stelt bij de eerste uitvoering de eigendomsrechten in. Dit is de standaardkeuze. - Een bind mount:
/srv/appname/pg:/var/lib/postgresql/data. U kiest het pad, dus u bent zelf verantwoordelijk voor de rechten. - Geen mount. Zie hierboven. De data staat in de container.
De volledige afweging is een onderwerp op zich, en bind mounts versus named volumes behandelt dit. Voor een database is de korte versie: gebruik een named volume tenzij u een specifieke reden heeft om het hostpad te kennen. Als u toch een bind mount gebruikt, plaats deze dan op een stabiele locatie zoals /srv/appname/pg in plaats van in de projectmap waar een git clean erbij kan.
Eén harde grens: plaats een datadirectory van een database nooit op NFS (network file system) of een andere netwerk-mount waarvan u het locking- en fsync-gedrag niet heeft getest. Databases gaan ervan uit dat een succesvolle fsync betekent dat de bytes op stabiele opslag staan. Wanneer die aanname onjuist is, ontstaat er corruptie die pas weken later aan het licht komt.
Bevestig de volumenaam voordat het volume verdwijnt
Compose geeft een volume de naam <project>_<volume>, waarbij de projectnaam standaard gelijk is aan de mapnaam. De identiteit van het volume hangt dus af van een mapnaam, iets wat gebruikers vaak ondoordacht wijzigen.
Verplaats /srv/app naar /srv/app-old, of hernoem de pgdata-sleutel in het compose-bestand, en de volgende docker compose up -d maakt een volledig nieuw, leeg volume aan. Postgres initialiseert hierin een nieuwe cluster. De container is gezond, de applicatie start, en alle tabellen zijn verdwenen. Het oude volume staat gelukkig nog steeds op de schijf onder de oude naam.
docker volume ls
docker volume inspect app_pgdataBevestig de namen zodat dit niet kan gebeuren. Stel de projectnaam en de volumenaam expliciet in:
name: myapp
services:
db:
image: postgres:17
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
name: myapp_pgdataAls er al een verdwaald volume met uw data bestaat, kopieer deze dan terwijl de database is gestopt:
docker compose stop db
docker run --rm -v app_pgdata:/from -v myapp_pgdata:/to alpine sh -c 'cp -a /from/. /to/'
docker compose start dbKopieert u dit terwijl de database draait, dan krijgt u een inconsistente kopie van bestanden die op dat moment werden geschreven. Stop de database eerst.
Wie is de eigenaar van de datamap
De officiële Postgres-, MySQL- en MongoDB-images draaien hun server als een gebruiker zonder privileges, meestal 999. Wanneer de container als root start, wijzigt het entrypoint het eigenaarschap van de datamap naar die gebruiker en verlaagt vervolgens de privileges. Daarom werkt een lege bind mount meestal bij de eerste poging.
Dit gaat mis zodra u user: instelt in het compose-bestand, omdat het entrypoint dan geen privileges meer heeft om iets te corrigeren. Postgres geeft dit direct aan:
initdb: error: could not change permissions of directory "/var/lib/postgresql/data": Operation not permittedEen datamap die bestaat met de verkeerde rechten geeft een andere melding, en deze is belangrijk om te herkennen omdat de oplossing chmod is, niet chown:
FATAL: data directory "/var/lib/postgresql/data" has invalid permissions
DETAIL: Permissions should be u=rwx (0700) or u=rwx,g=rx (0750).MongoDB op een bind mount die eigendom is van root faalt bij het lock-bestand:
Unable to create/open the lock file: /data/db/mongod.lock (Permission denied). Ensure the user executing mongod is the owner of the lock file and has the appropriate permissions.De oplossing is om de hostmap met chown toe te wijzen aan het numerieke id, niet aan een naam:
sudo chown -R 999:999 /srv/appname/pg
sudo chmod 700 /srv/appname/pg
ls -ldn /srv/appname/pgls -ldn toont getallen in plaats van namen, en dit zou 999 999 moeten weergeven. Het account genaamd postgres op uw host en het account genaamd postgres in de image zijn niet aan elkaar gerelateerd: de kernel vergelijkt getallen, en de namen worden aan beide kanten afzonderlijk opgezocht. hoe PUID en PGID hostgebruikers toewijzen aan een container behandelt die mapping correct. Bij rootless Docker of user namespace remapping verschuiven de getallen opnieuw, dus lees de ids uit de draaiende container in plaats van uit te gaan van 999.
Named volumes laten dit hele hoofdstuk vervallen bij de eerste uitvoering, omdat Docker een lege map aanmaakt en het entrypoint hiervan de eigenaar wordt.
Upgrades: een pakketupgrade versus een wijziging van een image-tag
Op de host verplaatst apt upgrade u naar een minor-versie. Uw distributie zal niet automatisch een major-versie van een database voor u overschakelen. Wanneer u besluit om over te stappen, kunnen beide sets binaries tegelijkertijd worden geïnstalleerd; dit is precies wat pg_upgrade vereist.
In een container is de tag de versie, dus een upgrade is het aanpassen van één regel. Dat maakt minor-upgrades triviaal en major-upgrades een procedure.
Wijzig postgres:16 naar postgres:17, voer docker compose up -d uit, en de container stopt direct:
PostgreSQL Database directory appears to contain a database; Skipping initialization
FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.Er is niets beschadigd. De nieuwe binaries weigeren de oude catalogusstructuur op de schijf te lezen, aangezien deze verandert tussen major-versies. Zet de tag terug naar postgres:16 en de container start weer. Die rollback is het enige echte voordeel van containers bij upgrades.
Het ondersteunde pad is dumpen en herstellen. PostgreSQL geeft de voorkeur aan een dump die door de nieuwere client wordt gemaakt. Voer deze daarom uit vanuit de nieuwe image tegen de nog draaiende oude server op het compose-netwerk:
docker run --rm --network myapp_default -e PGPASSWORD="$POSTGRES_PASSWORD" \
postgres:17 pg_dumpall -h db -U postgres > /srv/backups/all.sql
ls -lh /srv/backups/all.sql
tail -n 2 /srv/backups/all.sqlHet bestand moet minimaal enkele tientallen kilobytes groot zijn en eindigen met een regel die PostgreSQL database cluster dump complete bevat. Een bestand van enkele honderden bytes betekent dat de dump is mislukt en u op het punt staat een volume voor niets te verwijderen. Doe dit pas na die controle:
docker compose down
docker volume rm myapp_pgdata
# edit the compose file: image: postgres:17
docker compose up -d db
docker compose exec -T db psql -U postgres -f /dev/stdin < /srv/backups/all.sqlDe andere engines verschillen:
- MySQL 8 upgradet zijn eigen datadictionary bij het opstarten, dus een minor-tag-wijziging is meestal slechts een herstart. Lees de release notes voordat u overstapt tussen release-series en maak in elk geval eerst een dump.
- MariaDB verwacht dat
mariadb-upgradewordt uitgevoerd nadat de server is opgestart op de nieuwe versie. - MongoDB moet één major-versie per keer worden geüpgraded. Na elke stap stelt u de feature compatibility version in voordat u verdergaat. Het overslaan van een versie betekent dat
mongodweigert te starten en eenUPGRADE PROBLEM-regel in de logs plaatst met de vermeldingfeatureCompatibilityVersion. Vanaf MongoDB 7.0 vereist het commando een expliciete bevestigingsvlag:db.adminCommand({ setFeatureCompatibilityVersion: "8.0", confirm: true }). - Redis laadt oudere snapshot-bestanden zonder problemen, maar nieuwere niet. Een upgrade is dus een herstart, en een downgrade kan ertoe leiden dat de data niet wordt geladen.
De algemene regel: een container maakt een downgrade eenvoudig, maar maakt een upgrade niet makkelijker.
Waarom sluit mijn databasecontainer af met code 137?
Dit komt doordat de out-of-memory (OOM) killer van de kernel het proces heeft beëindigd. 137 is 128 plus signaal 9.
docker compose ps
docker inspect myapp-db-1 | grep -i oomkilled
journalctl -k | tail -n 20docker compose ps toont Exited (137), de inspect-regel leest "OOMKilled": true, en het kernellogboek bevat een overeenkomstige vermelding:
Memory cgroup out of memory: Killed process 4711 (postgres) total-vm:2170416kBDit is het mechanisme, en het verrast veel mensen. PostgreSQL en MySQL baseren de grootte van hun buffers op de totale hoeveelheid geheugen die de host rapporteert. Een cgroup-limiet verandert dat getal niet voor hen. Op een host met 16 GB geheugen en een limiet van 2 GB, plant de database alsof er 16 GB beschikbaar is. De cgroup beëindigt het proces dan lang voordat de host zelf onder druk staat. Een geheugenlimiet alleen is dus niet voldoende. U moet de database ook vertellen wat de werkelijke limiet is:
- PostgreSQL: stel
shared_buffersin en let opwork_mem.work_memwordt per sorteeroperatie per verbinding toegewezen, dus een ruime waarde vermenigvuldigd met vijftig verbindingen is de gebruikelijke oorzaak van een container die onder belasting sterft in plaats van bij het opstarten. - MySQL en MariaDB: stel
innodb_buffer_pool_sizein, dat standaard op 128M staat. Laatinnodb_dedicated_serveruitgeschakeld in een container, omdat de functie daarvan is om zichzelf te dimensioneren op basis van het gedetecteerde machinegeheugen. - MongoDB: stel de WiredTiger-cachegrootte expliciet in in plaats van deze te laten gokken op basis van het hostgeheugen.
- Redis:
maxmemorystaat standaard op onbeperkt, waardoor Redis groeit totdat de cgroup het stopt. Stelmaxmemoryruim onder de containerlimiet in en kies eenmaxmemory-policy.
Postgres rapporteert het voorval ook vanuit zichzelf, en dit paar regels is wat u in het logboek zult vinden:
LOG: server process (PID 123) was terminated by signal 9: Killed
LOG: terminating any other active sessions due to crash of another server processWanneer één backend wordt beëindigd, moeten alle andere backends opnieuw opstarten, omdat het gedeelde geheugen mogelijk inconsistent is geworden. Dit veroorzaakt een storm aan verbindingen voor uw applicatie; het is geen geruisloze gebeurtenis. geheugenlimieten instellen in Docker Compose behandelt de syntaxis en het verschil tussen mem_limit en de deploy.resources-vorm.
Niets van dit alles verdwijnt op de host. Het verplaatst zich enkel. Zonder cgroup concurreert de database met alles wat op de server draait. De OOM-killer van de host kiest dan een slachtoffer op basis van een score, wat ook sshd kan zijn. Een limiet die de database voorspelbaar beëindigt, is eenvoudiger te beheren dan een OOM-situatie op de host die u de toegang ontzegt.
Backups: dump intern, back-up extern
Maak geen back-up van een actieve database door de datamap te kopiëren. Een kopie op bestandsniveau die wordt gemaakt terwijl de server schrijft, resulteert in een inconsistente kopie (torn copy), wat pas bij het herstellen aan het licht komt.
Er zijn twee betrouwbare methoden: maak een dump met de eigen tool van de database terwijl deze draait en back-up de dump, of stop de container en kopieer het volume in koude toestand.
docker compose exec -T db pg_dump -U postgres -Fc appdb > /srv/backups/appdb.dump
docker compose exec -T db mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction --all-databases > /srv/backups/mysql.sql
docker compose exec -T db mongodump --archive --gzip --db appdb > /srv/backups/appdb.archive.gz
docker compose exec -T redis redis-cli BGSAVEDe -T is van belang. Zonder deze vlag kan docker compose exec een terminal aan het commando koppelen, waarbij de terminal-laag carriage returns toevoegt aan de uitvoerstroom. Een tekst-dump herstelt vervolgens met vreemde fouten en een binaire dump is simpelweg corrupt. Dit faalt geruisloos tijdens het maken van de back-up en met veel lawaai een maand later.
--single-transaction geeft mysqldump een consistent snapshot van InnoDB-tabellen zonder de hele server te vergrendelen.
Deze commando's schrijven elk één bestand. Dit is geen back-upsysteem: er is geen retentiebeleid, geen kopie buiten de server en geen verificatie. Geef de dump-map door aan een tool die al deze drie zaken afhandelt, wat precies is waar restic backups vanaf een VPS voor dient. Back-up /srv/backups, niet /var/lib/docker/volumes.
Voer daarna de restore uit, want een back-up die u nooit heeft hersteld, is geen back-up:
docker compose exec -T db createdb -U postgres restore_test
docker compose exec -T db pg_restore -U postgres -d restore_test < /srv/backups/appdb.dump
docker compose exec -T db psql -U postgres -d restore_test -c '\dt'\dt hoort de tabellen van uw applicatie weer te geven. Een leeg resultaat, of Did not find any relations., betekent dat de dump niet is wat u denkt dat het is. Verwijder restore_test wanneer u klaar bent.
Het commando dat alles verwijdert
docker compose down -v.
De standaard down verwijdert de containers en het netwerk. De -v verwijdert ook elk benoemd volume dat in dat compose-bestand is gedeclareerd, plus elk anoniem volume dat aan die containers is gekoppeld. Er volgt geen bevestigingsvraag en er is geen mogelijkheid tot ongedaan maken. Dit is de meest voorkomende manier waarop een zelfgehoste database wordt vernietigd, en dit gebeurt meestal tijdens het oplossen van problemen met iets anders, omdat een forumantwoord adviseerde om het uit te voeren.
Vier zaken verkleinen de impact:
- Declareer het databasevolume als
external: true. Compose verwijdert geen volume dat het niet beheert, dus-vkan er niet bij. U maakt het eenmalig aan metdocker volume create myapp_pgdata. - Gebruik
docker compose stopendocker compose startvoor routine-herstarts. down versus stop in Compose legt uit wat elk commando precies verwijdert. - Bewaar dumps op een host-pad buiten elk volume dat Compose beheert.
- Plak nooit
-vuit een antwoord voor probleemoplossing in een stack die data bevat die u wilt behouden.
Publiceer de databasepoort niet
Deze regel plaatst uw database op het openbare internet:
ports:
- "5432:5432"Dit bindt de service aan elke interface. Docker publiceert een poort door de bestemmingsbestemming van het pakket te herschrijven voordat uw firewall-inputregels het zien, en ufw-regels bevinden zich in de input-chain, waardoor ufw deny 5432 helemaal niets doet. waarom Docker gepubliceerde poorten ufw omzeilen toont de chain-traversal.
Een applicatie in hetzelfde compose-project bereikt de database via de servicenaam op het compose-netwerk, dus deze heeft geen gepubliceerde poort nodig. Verwijder het blok. Als u een client op de host wilt gebruiken, bind dan alleen aan de loopback:
ports:
- "127.0.0.1:5432:5432"Controleer wat er daadwerkelijk luistert:
sudo ss -ltnp | grep 5432127.0.0.1:5432 is wat u wilt zien. 0.0.0.0:5432 betekent dat iedereen uw wachtwoord kan proberen.
Wat draait u waar
Eén applicatie op één VPS. Container. Een named volume met een vastgelegde naam, geen gepubliceerde poort, een geheugenlimiet met bijbehorende database-instellingen, en een dagelijkse dump naar een host-pad dat door restic wordt opgehaald. Begin bij een schone Docker-installatie op een VPS en bewaar de stack in één compose-bestand dat u commit. Het voordeel is reëel: de databaseversie wordt een controleerbare regel in git.
Een host die meerdere services draait. Containers, één database per applicatie, niet één gedeelde server voor allemaal. Een gedeelde server koppelt elke applicatie aan één upgrade-schema, en één uit de hand gelopen query wordt een storing voor iedereen. Geef elke container een eigen geheugenlimiet zodat een slechte query beperkt blijft tot de app die deze heeft geschreven. Meerdere kleine Postgres-instanties kosten iets meer schijfruimte, maar vereisen veel minder coördinatie.
De database is het product. Draai deze op de host vanuit de pakketrepository van de leverancier, of betaal voor een managed service. pg_upgrade vereist dat beide major-versies van de binaries tegelijkertijd zijn geïnstalleerd, wat pakketten u bieden en een image met één versie niet. Replicatie en point-in-time recovery met WAL (write ahead log) archiving zijn beide eenvoudiger wanneer de database de machine en de schijven beheert. Kies de saaie weg voor het systeem dat u om 03:00 uur wakker belt.
De applicatie is klein. Overweeg om helemaal geen server-database te draaien. Een webapplicatie met één schrijver op één VPS is vaak beter af met SQLite in productie op een VPS, waarbij de back-up één bestand is en het upgrade-pad een bibliotheekversie.
FAQ
Is het veilig om een productiedatabase in Docker te draaien?
Ja, voor een applicatiestack op één server. Een container is een Linux-proces met namespaces en cgroups eromheen. Met een gekoppelde volume schrijft de database naar hetzelfde host-bestandssysteem als bij een installatie via een pakketbeheerder. De risico's zijn operationeel in plaats van prestatiegericht: een volume waarvan de naam niet is vastgelegd, een bind mount die eigendom is van de verkeerde user id, een restore die u nooit heeft getest, en docker compose down -v. Als u deze vier punten afdekt, functioneert de container naar behoren. Stap over op een host-installatie wanneer de database de voornaamste werklast is en u pg_upgrade, replicatie of point-in-time recovery nodig heeft.
Moet ik een bind mount of een named volume gebruiken voor databasegegevens?
Gebruik een named volume, tenzij u een specifieke reden heeft om het host-pad te kennen. Docker maakt de map aan en de entrypoint van de image stelt bij de eerste start de rechten in, waardoor rechtenproblemen uitblijven. Leg het volume vast met een expliciete name: of markeer het als external: true; anders leidt het hernoemen van de projectmap ongemerkt tot een nieuw, leeg volume en een lege database. Een bind mount is prima als u de host-map via chown toewijst aan de numerieke user id waaronder de image draait (999 voor de officiële Postgres-, MySQL- en MongoDB-images). Controleer dit met ls -ldn, aangezien ls -l de host-naam voor dat nummer toont, wat binnen de container betekenisloos is.
Wat verwijdert docker compose down -v?
Het verwijdert de containers en het netwerk zoals een standaard down, en de -v verwijdert bovendien elk named volume dat in dat compose-bestand is gedeclareerd, samen met elk anoniem volume dat aan die containers is gekoppeld. Dat is inclusief de database. Er volgt geen bevestigingsvraag en er is geen herstel mogelijk. Volumes gemarkeerd als external: true worden niet verwijderd; dit is de voornaamste reden om een databasevolume als external te markeren. Gebruik voor een routineherstart liever docker compose stop en docker compose start.
Hoe upgrade ik PostgreSQL naar een nieuwe major-versie in Docker?
Dump en restore. Het wijzigen van postgres:16 naar postgres:17 en herstarten resulteert in FATAL: database files are incompatible with server met een DETAIL-regel die beide versies benoemt, omdat de nieuwe binaries de oude catalogusstructuur niet kunnen lezen. Er is geen schade: zet de oude tag terug en het proces start weer. Maak een pg_dumpall met de client van de nieuwe versie tegen de draaiende oude container, bevestig dat het bestand eindigt met PostgreSQL database cluster dump complete, start daarna de nieuwe tag op een leeg volume en laad de dump. Voor minor-upgrades binnen één major-versie volstaan een pull en een herstart.
Waarom stopt mijn databasecontainer met exitcode 137?
137 is 128 plus signaal 9, wat betekent dat het proces abrupt is beëindigd. Voer docker inspect <container> | grep -i oomkilled uit; een waarde van true betekent dat de container de cgroup-geheugenlimiet heeft bereikt. De gebruikelijke oorzaak is dat PostgreSQL en MySQL het totale geheugen van de host lezen en de limiet van de container niet zien; ze plannen voor 16 GB terwijl ze binnen 2 GB draaien. Stel shared_buffers en work_mem, of innodb_buffer_pool_size, in op basis van de limiet die u aan de container heeft gegeven. Controleer journalctl -k voor de bijbehorende Memory cgroup out of memory-regel om te bevestigen welk proces door de kernel is geselecteerd.