Det här sätter du upp
En hanterad databastjänst ger dig en adress, ett användarnamn och ett lösenord, och sköter resten. Den här guiden bygger samma sak på din egen server: MariaDB med en databas och ett konto per app, inställningar anpassade efter servern, TLS, nattliga säkerhetskopior, och åtkomst bara från servern själv och från de servrar där dina appar körs, via WireGuard. Databasporten är aldrig öppen mot internet.
MariaDB började 2009 som en fork av MySQL, ledd av MySQL:s ursprungliga upphovsman, Michael "Monty" Widenius. Den utvecklas av företaget MariaDB, med säte i USA, tillsammans med MariaDB Foundation, en ideell stiftelse som tar hand om projektet med öppen källkod. MariaDB är öppen källkod under GPL version 2, och de flesta appar som är skrivna för MySQL fungerar med den. Containeravbilden är den officiella mariadb på Docker Hub, som underhålls av MariaDB Foundation. På vår testserver öppnade MariaDB inga utgående anslutningar.
Här körs MariaDB som en enda container under en egen användare som heter mariadb, uppsatt som i Podman-guiden. Den behöver ingen Caddy: apparna pratar direkt med den, genom tunneln.
Varje steg nedan har körts på en nyinstallerad Melonslab VC-P Alloy (2 vCPU, 8 GB) med Debian 13:
- MariaDB 12.3.3, den aktuella versionen med långtidsstöd, startade med ett root-lösenord från en hemlighet i Podman, och root kunde bara logga in inifrån containern.
- Ett konto för en app, från en appserver i andra änden av en WireGuard-tunnel, kunde skapa tabeller i sin egen databas, men inte öppna en annan databas, skapa databaser eller läsa filer på servern. Utan TLS nekades det.
- Klienten
mariadboch en app i Python anslöt med TLS 1.3 och kontrollerade serverns certifikat. En klient med fel certifikatutfärdare nekades. - Ingenting lyssnade på serverns publika IPv4- eller IPv6-adress, och från internet svarade port 3306 inte.
- En körning med sysbench före och efter minnesinställningarna.
- En nattlig dump återställdes bredvid den riktiga databasen, och i en tom server med sina konton. En fysisk säkerhetskopia med
mariadb-backupåterställdes över den data som var i drift. - En uppgradering av huvudversion från MariaDB 11.8 till 12.3 genom att byta taggen på avbilden.
- Allt startade igen av sig självt efter en omstart av servern, även tunneladressen.
Efter en omstart använde hela servern 1,6 GB minne, varav MariaDB-containern 1,2 GB: MariaDB sätter upp en del av sin cache på 4 GB när den startar, och resten fylls allteftersom den läser din data.
Innan du börjar
Du behöver:
- en server uppsatt som i Podman-guiden, med ufw påslaget som i Säkra en ny Debian-server;
- WireGuard på servern, med tunneladressen
10.8.0.1, och varje appserver tillagd som en enhet, som i Lägg till fler enheter. Exemplen använder10.8.0.2för appservern.
Är appservern en annan server hos Melonslab i samma nät behöver du först Anslut två servrar, så att WireGuard når den. Tunneln ger sedan kryptering och en fast adress för varje server.
Exemplen använder appdb och appuser för appens databas och konto, och 203.0.113.10 för databasserverns publika adress. Byt ut dem mot dina egna genomgående. MariaDB använder port 3306, liksom MySQL, så kör en av dem per server.
1. Skapa användaren
Som root:
useradd -m -s /bin/bash mariadb
loginctl enable-linger mariadb
machinectl shell mariadb@
Allt fram till steg 7 körs som mariadb.
2. Skapa hemligheterna
openssl rand -base64 24 | tr -d '\n' | podman secret create mariadb-root-password -
openssl rand -base64 24 | tr -d '\n' | podman secret create appdb-password -
Den första är MariaDB:s root-lösenord, den andra lösenordet till appens konto. Podman lagrar dem, så de står inte i någon av filerna nedan. Så här visar du ett, till din lösenordshanterare eller appens inställningar:
podman secret inspect --showsecret --format '{{.SecretData}}' appdb-password
3. Skapa ett TLS-certifikat
WireGuard krypterar redan trafiken mellan dina servrar. TLS lägger till en kontroll av att appen pratar med din databas och inte med något annat i tunneln, och håller anslutningen krypterad om den någon gång tar en annan väg. Eftersom ingen utanför dina servrar ansluter behöver du inget publikt certifikat: skapa en egen liten certifikatutfärdare, och ett certifikat för servern som den signerar.
mkdir -p ~/mariadb/ca ~/mariadb/tls ~/mariadb/conf.d
cd ~/mariadb/ca
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 3650 \
-subj "/CN=MariaDB CA" -addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign" -keyout ca-key.pem -out ca.pem
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -subj "/CN=10.8.0.1" \
-addext "subjectAltName=IP:10.8.0.1,IP:127.0.0.1" -keyout ../tls/server-key.pem -out server.csr
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca-key.pem -days 3650 -copy_extensions copy -out ../tls/server-cert.pem
cp ca.pem ../tls/
podman unshare chown 999:999 ../tls/server-key.pem
ca.pemär certifikatutfärdaren. Varje appserver får en kopia, som den kontrollerar servern med.ca-key.pemsignerar certifikat. Den stannar i~/mariadb/ca, som containern aldrig ser.- Serverns certifikat anger adresserna som apparna ansluter till,
10.8.0.1och127.0.0.1. Klienterna jämför det med adressen de använde. - Båda gäller i tio år. Lägg in en påminnelse i kalendern och kör de fyra sista kommandona igen innan dess.
- Sista raden ger serverns nyckel till användaren som MariaDB körs som i containern, användar-ID 999, så att MariaDB kan läsa den men inte andra användare på servern.
4. Anpassa den efter servern
MariaDB:s standardinställningar är gjorda för att starta på vilken maskin som helst, med 128 MB till sin egen cache. Skapa ~/mariadb/conf.d/server.cnf:
[mariadb]
# Listen on this server and on the WireGuard tunnel only
bind_address = 127.0.0.1,10.8.0.1
skip_name_resolve
# TLS for connections over the network
ssl_ca = /etc/mysql/tls/ca.pem
ssl_cert = /etc/mysql/tls/server-cert.pem
ssl_key = /etc/mysql/tls/server-key.pem
# Sized for 2 vCPU and 8 GB of memory
innodb_buffer_pool_size = 4G
innodb_log_file_size = 1G
max_connections = 200
tmp_table_size = 64M
max_heap_table_size = 64M
bind_addressfår MariaDB att lyssna på servern själv och i tunneln, och ingen annanstans. Även med brandväggen avstängd lyssnar ingenting på den publika adressen. MariaDB lyssnar på10.8.0.1redan innan WireGuard har skapat adressen vid uppstart, så det spelar ingen roll i vilken ordning de startar.skip_name_resolvefår MariaDB att matcha konton bara på adress, utan att slå upp namn i DNS.innodb_buffer_pool_sizeär MariaDB:s cache för tabeller och index. Till skillnad från PostgreSQL läser MariaDB förbi systemets filcache, så på en server som bara kör databasen är hälften till två tredjedelar av minnet den vanliga storleken. 4 GB lämnar plats åt systemet, anslutningarna och några andra containrar.innodb_log_file_sizeär redo-loggen, dit ändringar skrivs innan de hamnar i tabellerna. En större logg jämnar ut tung skrivning.max_connectionshöjs från standardvärdet 151. Varje anslutning använder lite eget minne, högst några MB för vanliga frågor.tmp_table_sizeochmax_heap_table_sizelåter en fråga hålla större tillfälliga tabeller i minnet innan de flyttas till disk.
På paketet Titanium, med 16 GB, använder du innodb_buffer_pool_size = 10G.
5. Kör MariaDB
mkdir -p ~/.config/containers/systemd
Skapa ~/.config/containers/systemd/mariadb.container:
[Unit]
Description=MariaDB database server
[Container]
ContainerName=mariadb
Image=docker.io/library/mariadb:12.3
Network=host
Volume=mariadb-data:/var/lib/mysql
Volume=%h/mariadb/conf.d:/etc/mysql/conf.d:ro
Volume=%h/mariadb/tls:/etc/mysql/tls:ro
Secret=mariadb-root-password
Secret=mariadb-root-password,type=env,target=MYSQL_PWD
Environment=MARIADB_ROOT_PASSWORD_FILE=/run/secrets/mariadb-root-password
Environment=MARIADB_ROOT_HOST=localhost MARIADB_AUTO_UPGRADE=1
HealthCmd=healthcheck.sh --connect --innodb_initialized
HealthInterval=10s
Notify=healthy
AutoUpdate=registry
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target
Vad raderna gör:
mariadb:12.3är den aktuella versionen med långtidsstöd, som får stöd till 2029.AutoUpdate=registryinstallerar dess bugg- och säkerhetsfixar, som 12.3.3 till 12.3.4, men flyttar dig aldrig till en ny huvudversion.Network=hostsätter MariaDB på serverns eget nätverk, som Caddy. Då lyssnar den på exakt adresserna från steg 4, och ser varje klients riktiga adress, som kontona i steg 6 bygger på.- Root-lösenordet kommer från hemligheten.
MARIADB_ROOT_HOST=localhostskapar root bara för anslutningar inifrån containern, så root kan inte logga in över nätverket alls. - Den andra raden med
Secretger samma lösenord till kommandotmariadbi containern, så attpodman exec mariadb mariadbloggar in dig som root utan att du skriver det. MARIADB_AUTO_UPGRADE=1uppdaterar MariaDB:s systemtabeller efter en ny version, och sparar en kopia av dem först.Notify=healthygör att tjänsten räknas som startad först när MariaDB svarar, så att allt som väntar på den väntar tillräckligt länge.
Starta den, och slå på automatiska uppdateringar:
systemctl --user daemon-reload
systemctl --user start mariadb
systemctl --user enable --now podman-auto-update.timer
podman exec mariadb mariadb -e "SELECT VERSION(), @@innodb_buffer_pool_size, @@have_ssl"
VERSION() @@innodb_buffer_pool_size @@have_ssl
12.3.3-MariaDB-ubu2404 4294967296 YES
Vid första starten laddas avbilden på 346 MB ner, och det tar ungefär en halv minut.
6. Skapa en databas och ett konto för varje app
Ge varje app en egen databas och ett eget konto, så att en läcka i en app inte öppnar de andra. Skapa dem som root i MariaDB, med lösenordet från steg 2:
pw=$(podman secret inspect --showsecret --format '{{.SecretData}}' appdb-password)
podman exec -i mariadb mariadb <<SQL
CREATE DATABASE appdb;
CREATE USER 'appuser'@'10.8.0.2' IDENTIFIED BY '$pw' REQUIRE SSL;
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, DROP, INDEX, REFERENCES
ON appdb.* TO 'appuser'@'10.8.0.2';
SQL
'appuser'@'10.8.0.2'är ett konto som bara kan logga in från appserverns tunneladress. Samma namn från en annan adress nekas.REQUIRE SSLnekar kontot utan TLS.- Rättigheterna täcker vad en app och dess migreringar behöver i sin egen databas: läsa och skriva rader, och skapa, ändra och ta bort tabeller och index. Kontot kan inte skapa databaser eller konton, eller läsa filer på servern.
Vi kontrollerade från appservern, inloggade som appuser:
$ mariadb ... mysql
ERROR 1044 (42000): Access denied for user 'appuser'@'10.8.0.2' to database 'mysql'
$ mariadb ... -e "CREATE DATABASE other"
ERROR 1044 (42000) at line 1: Access denied for user 'appuser'@'10.8.0.2' to database 'other'
LOAD_FILE('/etc/passwd') gav ingenting tillbaka. Upprepa blocket för varje app, med dess egna namn och en egen hemlighet.
Adressen i kontot är den som anslutningen kommer från:
- en app på en annan server, genom tunneln: dess tunneladress, som
10.8.0.2; - en app på den här servern, utanför en container, eller genom en SSH-tunnel (steg 7):
127.0.0.1; - en app i en container på den här servern, under en annan användare:
10.8.0.1, eftersom den når MariaDB via tunneladressen och kommer därifrån.
7. Släpp in dina appar
Gå tillbaka till root med exit, och öppna port 3306 bara i tunneln:
ufw allow in on wg0 to any port 3306 proto tcp
Kopiera certifikatutfärdaren, /home/mariadb/mariadb/ca/ca.pem, till varje appserver, till exempel till /etc/ssl/certs/appdb-ca.pem. Den är inte hemlig.
Med klienten mariadb. Kör apt install mariadb-client på appservern, och sedan:
mariadb -h 10.8.0.1 -u appuser -p --ssl-ca=/etc/ssl/certs/appdb-ca.pem appdb \
-e "SELECT CURRENT_USER(); SHOW STATUS LIKE 'Ssl_version'"
CURRENT_USER()
appuser@10.8.0.2
Variable_name Value
Ssl_version TLSv1.3
Från en app. I de flesta appar är anslutningssträngen mysql://appuser:LÖSENORD@10.8.0.1:3306/appdb, med sökvägen till ca.pem i dess TLS-inställningar. I Python, med PyMySQL (apt install python3-pymysql):
import os
import pymysql
conn = pymysql.connect(
host="10.8.0.1",
user="appuser",
password=os.environ["DB_PASSWORD"],
database="appdb",
ssl_ca="/etc/ssl/certs/appdb-ca.pem",
ssl_verify_cert=True,
ssl_verify_identity=True,
)
with conn.cursor() as cur:
cur.execute("SELECT COUNT(*) FROM notes")
print(cur.fetchone()[0])
conn.close()
ssl_verify_cert och ssl_verify_identity får PyMySQL att kontrollera att certifikatet är signerat av din utfärdare och anger 10.8.0.1. Ange båda: med bara ssl_verify_identity stannar versionen i Debian 13 med Cannot set verify_mode to CERT_NONE when check_hostname is enabled. Vår testapp läste tillbaka 1 000 rader med TLS 1.3, och med en annan utfärdares ca.pem nekades den med certificate verify failed.
Från din egen dator. En SSH-tunnel behöver ingen ändring på servern. På din dator:
ssh -N -L 3307:localhost:3306 root@203.0.113.10
Medan den körs ansluter ett databasverktyg på din dator till 127.0.0.1 port 3307, och MariaDB ser anslutningen komma från 127.0.0.1. Därför behövs ett konto för den adressen (steg 6).
8. Kontrollera vad andra ser
På servern:
ss -tlnp | grep 3306
LISTEN 0 90 127.0.0.1:3306 0.0.0.0:* users:(("mariadbd",pid=1164,fd=24))
LISTEN 0 90 10.8.0.1:3306 0.0.0.0:* users:(("mariadbd",pid=1164,fd=25))
Ingenting på den publika adressen, och ingenting på IPv6. Från en dator som inte är ansluten till tunneln provar du den publika adressen:
nc -vz -w 5 203.0.113.10 3306
nc: connect to 203.0.113.10 port 3306 (tcp) timed out: Operation now in progress
ufw kastar försöket utan att svara, och på servern visar journalctl -k | grep 'DPT=3306' det som [UFW BLOCK]. Genom tunneln kommer appservern bara in med de konton du har skapat. Våra tester fick:
--skip-ssl ERROR 1045 (28000): Access denied for user 'appuser'@'10.8.0.2' (using password: YES)
-u root ERROR 1045 (28000): Access denied for user 'root'@'10.8.0.2' (using password: YES)
another CA ERROR 2026 (HY000): TLS/SSL error: self-signed certificate in certificate chain
9. Mät
Vi mätte med sysbench på en testdatabas på 1,9 GB, åtta tabeller med en miljon rader, 16 klienter i 60 sekunder, från servern själv. Varje test kördes två gånger:
| Inställningar | Läsning och skrivning (transaktioner/s) | Bara läsning (transaktioner/s) |
|---|---|---|
| Standard | 275 och 334 | 592 och 618 |
| Steg 4 | 283 och 411 | 716 och 724 |
Bara läsning ökade med ungefär 20 %. Läsning och skrivning varierade mer mellan två körningar än mellan inställningarna, så kör varje test mer än en gång innan du jämför. För att köra det själv installerar du sysbench på servern, skapar en databas sbtest och ett konto för 127.0.0.1, och kör sysbench oltp_read_write --mysql-host=127.0.0.1 --tables=8 --table-size=1000000 --threads=16 --time=60 med prepare och sedan run. Ta bort databasen och kontot efteråt.
10. Säkerhetskopiera varje natt
mariadb-dump skriver en konsekvent kopia av en databas medan apparna fortsätter att använda den. Som mariadb (machinectl shell mariadb@) skapar du ~/.local/bin/mariadb-dump-all:
#!/bin/sh
# Dump the accounts and every database, and keep 14 days of dumps
set -e
umask 077
dir=$HOME/backup
day=$(date +%F)
podman exec mariadb mariadb-dump --system=users --insert-ignore > "$dir/users-$day.sql"
for db in $(podman exec mariadb mariadb -N -e "SHOW DATABASES WHERE \`Database\` NOT IN ('information_schema','performance_schema','sys','mysql')"); do
podman exec mariadb mariadb-dump --single-transaction --routines --events "$db" | gzip > "$dir/$db-$day.sql.gz"
done
find "$dir" -type f -mtime +13 -delete
chmod 755 ~/.local/bin/mariadb-dump-all
mkdir -p ~/backup ~/.config/systemd/user
Filen users innehåller alla konton, deras rättigheter och lösenordshashar, som databasdumparna inte tar med. Bara mariadb kan läsa filerna. Kör skriptet varje natt med en systemd-tjänst och en timer. Skapa ~/.config/systemd/user/mariadb-dump.service:
[Unit]
Description=Dump MariaDB databases
[Service]
Type=oneshot
ExecStart=%h/.local/bin/mariadb-dump-all
Och ~/.config/systemd/user/mariadb-dump.timer:
[Unit]
Description=Dump MariaDB databases every night
[Timer]
OnCalendar=*-*-* 02:30
Persistent=true
[Install]
WantedBy=timers.target
Slå på timern och kör säkerhetskopian en gång nu:
systemctl --user daemon-reload
systemctl --user enable --now mariadb-dump.timer
systemctl --user start mariadb-dump.service
ls -l ~/backup
-rw------- 1 mariadb mariadb 5524 Oct 14 01:57 appdb-2026-10-14.sql.gz
-rw------- 1 mariadb mariadb 4482 Oct 14 01:57 users-2026-10-14.sql
Testa en återställning. Återställ i en ny databas, bredvid den riktiga:
podman exec mariadb mariadb -e "CREATE DATABASE appdb_restore"
gunzip -c ~/backup/appdb-2026-10-14.sql.gz | podman exec -i mariadb mariadb appdb_restore
podman exec mariadb mariadb -e "SELECT COUNT(*) FROM appdb_restore.notes; DROP DATABASE appdb_restore"
Räkna rader i en av dina egna tabeller. Vår testtabell, notes, kom tillbaka med alla sina 1 000 rader.
För att bygga upp en hel server igen sätter du upp den med steg 1 till 5, läser in filen users först och sedan varje databas:
podman exec -i mariadb mariadb < ~/backup/users-2026-10-14.sql
podman exec mariadb mariadb -e "CREATE DATABASE appdb"
gunzip -c ~/backup/appdb-2026-10-14.sql.gz | podman exec -i mariadb mariadb appdb
Vi gjorde det i en tom MariaDB: kontona kom tillbaka med sina adresser, rättigheter och lösenord, och tabellen med alla sina rader. Filen users sätter också tillbaka root-lösenordet till det som fanns i säkerhetskopian, så efter inläsningen loggar root in med det gamla lösenordet. Spara lösenorden från steg 2 i din lösenordshanterare.
Kopiera ~/backup från servern varje natt, till exempel med restic. Dumparna innehåller all din data.
Fysiska säkerhetskopior för stora databaser
En dump återställs genom att varje sats körs igen, vilket tar lång tid för databaser på många GB. mariadb-backup, i samma avbild, kopierar själva datafilerna medan MariaDB körs:
umask 077
podman exec mariadb sh -c 'mariadb-backup --backup --stream=xbstream --target-dir=/tmp 2>/tmp/backup.log' > ~/backup/mariadb-physical.xb
umask 077 gör att bara mariadb kan läsa filen, som dumparna: den innehåller alla databaser och konton. För att återställa den måste MariaDB stoppas och dess data bytas ut:
systemctl --user stop mariadb
podman volume rm mariadb-data
podman volume create mariadb-data
podman run --rm -i -v mariadb-data:/var/lib/mysql docker.io/library/mariadb:12.3 sh -c \
'mkdir /tmp/r && mbstream -x -C /tmp/r && mariadb-backup --prepare --target-dir=/tmp/r && mariadb-backup --copy-back --target-dir=/tmp/r && chown -R mysql:mysql /var/lib/mysql' \
< ~/backup/mariadb-physical.xb
systemctl --user start mariadb
Vi raderade halva testtabellen efter en fysisk säkerhetskopia, återställde den så här, och alla 1 000 rader var tillbaka. Återställ med samma version av MariaDB som gjorde backupen.
11. Uppdatera MariaDB
Bugg- och säkerhetsfixar, som 12.3.3 till 12.3.4, kommer med podman auto-update en gång om dagen, och MariaDB startar om på några sekunder. Så här ser du om någon väntar:
podman auto-update --dry-run
Huvudversioner. En version med långtidsstöd kommer en gång om året och får stöd i tre år. För att gå över till en ny tar du en säkerhetskopia, byter taggen i mariadb.container och startar om:
systemctl --user start mariadb-dump.service
sed -i 's|mariadb:12.3|mariadb:13.3|' ~/.config/containers/systemd/mariadb.container
systemctl --user daemon-reload
systemctl --user restart mariadb
podman logs mariadb 2>&1 | grep -i upgrade
Vi testade det från 11.8 till 12.3, med samma steg. MariaDB startade på den nya versionen med den gamla datan, och MARIADB_AUTO_UPGRADE säkerhetskopierade och uppdaterade systemtabellerna av sig själv:
[Note] [Entrypoint]: Backing up system database to system_mysql_backup_11.8.9-MariaDB.sql.zst
Major version upgrade detected from 11.8.9-MariaDB to 12.3.3-MariaDB. Check required!
[Note] [Entrypoint]: Finished mariadb-upgrade
Det tog 20 sekunder. Gå från en version med långtidsstöd till nästa, och läs versionsinformationen först. Använd taggen för versionen du går till; 13.3 ovan är bara ett exempel. Att gå tillbaka till den gamla versionen stöds inte: återställ säkerhetskopian i stället.
Felsökning
Access denied for user 'appuser'@'10.8.0.2'. Lösenordet, adressen eller TLS är fel. Kontrollera att klienten använder TLS, eftersom kontot har REQUIRE SSL, och att det finns ett konto för exakt den adressen: podman exec mariadb mariadb -e "SELECT user, host FROM mysql.user".
Host '10.8.0.1' is not allowed to connect. Appen körs i en container på den här servern och kommer från tunneladressen. Skapa dess konto för 10.8.0.1, som i steg 6.
certificate verify failed: CA cert does not include key usage extension. Certifikatutfärdaren skapades utan raden keyUsage i steg 3, som Python 3.13 och senare kräver. Skapa den igen med hela steg 3, och kopiera den nya ca.pem till appservrarna.
TLS/SSL error: self-signed certificate in certificate chain. Klienten har en annan ca.pem än den som signerade serverns certifikat. Kopiera ~/mariadb/ca/ca.pem från servern igen.
Appen får timeout när den ansluter till 10.8.0.1. Kontrollera att WireGuard är uppe på båda servrarna (wg show), och att ufw-regeln från steg 7 finns: ufw status | grep 3306.