Rozdelenie compose súborov na znovupoužiteľné služby swarmu

Predchádzajúci článok nechal homelab bežať s úlohami, ktorých požadovaný stav žije v Gite. Takmer každá self-hosted aplikácia dodáva compose súbor napísaný pre jeden stroj: aplikácia, jej databáza, Redis, často konvertor dokumentov, všetko v jednom súbore. Na swarme sa z tejto výhody stáva plytvanie — každá aplikácia si prináša vlastný Postgres, vlastný broker, vlastné konvertory.
Riešením nie je väčší súbor. Je to rozdelenie podľa roly: stavové zdieľané zdroje dostanú vlastné stacky, aplikácie tie svoje a jedna overlay sieť ich spojí. Tento článok postaví takéto rozdelenie pre Postgres, MariaDB, Gotenberg, Tika a Mosquitto a potom naň nasadí Paperless-ngx a n8n.
Stacky navzájom nevidia
Každý stack dostane vlastnú sieť pomenovanú po stacku. Stack s názvom paperless
dostane paperless_default, stack n8n dostane n8n_default a názvy služieb sa
rozlišujú len v rámci vlastnej siete. Práve preto môžu dva stacky bežať službu s
názvom postgres bez kolízie — a práve preto sa ani jeden nedostane k databáze
toho druhého.
Zdieľanie začína jednou overlay sieťou vytvorenou mimo akéhokoľvek stacku:
docker network create --driver overlay shared
Každý stack, ktorý potrebuje zdieľanú službu, ju deklaruje ako externú:
networks:
shared:
external: true
name: shared
Jedno upozornenie. Vnútri stacku je krátky názov služby zaregistrovaný ako DNS
alias a v zdieľanej sieti všetky tieto aliasy zdieľajú jeden menný priestor. Ak
dva stacky bežia službu s názvom postgres a oba sa pripoja k shared, dopyty
sa rozdeľujú medzi dve nesúvisiace databázy. Zdieľané služby potrebujú názvy
jedinečné v celom klastri.
Jeden stack pre databázy
Postgres a MariaDB žijú spolu, pretože zdieľajú životný cyklus aj model úložiska a jeden stack sa zálohuje ľahšie ako dva.
services:
postgres:
image: docker.io/library/postgres:18
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: change-me
configs:
- source: postgres_init
target: /docker-entrypoint-initdb.d/10-databases.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 10
volumes:
- pgdata:/var/lib/postgresql
networks:
- shared
deploy:
replicas: 1
update_config:
order: stop-first
failure_action: rollback
restart_policy:
condition: on-failure
mariadb:
image: docker.io/library/mariadb:11.8
environment:
MARIADB_DATABASE: snipeit
MARIADB_USER: snipeit
MARIADB_PASSWORD: change-me
MARIADB_ROOT_PASSWORD: change-me
volumes:
- mariadbdata:/var/lib/mysql
networks:
- shared
deploy:
replicas: 1
update_config:
order: stop-first
failure_action: rollback
restart_policy:
condition: on-failure
configs:
postgres_init:
file: ./postgres-init.sql
networks:
shared:
external: true
name: shared
volumes:
pgdata:
driver: local
driver_opts:
type: nfs
o: "addr=nas.example.lan,rw,nfsvers=4,proto=tcp,nolock"
device: ":/export/swarm/postgres"
mariadbdata:
driver: local
driver_opts:
type: nfs
o: "addr=nas.example.lan,rw,nfsvers=4,proto=tcp,nolock"
device: ":/export/swarm/mariadb"
Druhá databáza vznikne raz, pri prvom štarte, keď je dátový adresár ešte prázdny:
CREATE ROLE n8n LOGIN PASSWORD 'change-me';
CREATE DATABASE n8n OWNER n8n;
replicas: 1 nie je odporúčanie. Dva tasky pripojené k rovnakému dátovému
adresáru ho poškodia a order: stop-first zabráni tomu, aby rolling update na
chvíľu spustil dvoch zapisovateľov. Healthcheck je pre ľudí, nie pre Swarm: nikto
naň nečaká.
Volumy ukazujú na NFS exporty, takže task môže pristáť na ktoromkoľvek uzle a
stále nájde svoje dáta. Pomenovaný volume s predvoleným lokálnym driverom žije len
na jednom uzle — ak sa databáza presunie, začne s prázdnym adresárom, čo vyzerá
presne ako strata dát. nolock hovorí klientovi, aby zamykal lokálne a nepýtal sa
lock manažéra servera; práve preto musia repliky zostať na jednej, keďže dva uzly
zapisujúce do rovnakého adresára by o sebe navzájom nevedeli. Na NAS treba overiť
dve veci: export potrebuje no_root_squash a uid vnútri kontajnerov — 999 pre
Postgres, 999 pre MariaDB — musia mať právo zápisu. Inak sa služba donekonečna
reštartuje na chybách oprávnení.
Postgres 18 presunul PGDATA do /var/lib/postgresql/18/docker a svoj
deklarovaný volume do /var/lib/postgresql. Namontovanie starej cesty
/var/lib/postgresql/data nechá skutočné dáta mimo volume.
Zdieľané konvertory dokumentov
Gotenberg a Tika sú bezstavové: nič na ukladanie, nič na plánovanie, nič, čo by sa staralo o to, kto ich volá. Dostanú repliky a žiadne publikované porty.
services:
gotenberg:
image: docker.io/gotenberg/gotenberg:8.37
command:
- gotenberg
- --chromium-disable-javascript=true
- --chromium-allow-list=file:///tmp/.*
networks:
- shared
deploy:
replicas: 2
restart_policy:
condition: on-failure
tika:
image: docker.io/apache/tika:3.3.1.0
networks:
- shared
deploy:
replicas: 1
restart_policy:
condition: on-failure
networks:
shared:
external: true
name: shared
Nič sa nepublikuje. Swarm drží vnútornú VIP pre každú službu a vyvažuje medzi
replikami, takže http://gotenberg:3000 sa dostane k obom taskom bez toho, aby do
toho vstupoval routing mesh.
Zdieľaný MQTT broker
Mosquitto potrebuje konfiguračný súbor a súbor s heslami. Konfigurácia patrí do swarm configu, heslá do secretu:
docker run --rm -it --user "$(id -u):$(id -g)" -v "$PWD:/work" \
docker.io/library/eclipse-mosquitto:2.1.2-alpine \
mosquitto_passwd -c -b /work/mqtt.passwd mqtt 'a-long-password'
docker secret create mosquitto_passwd ./mqtt.passwd
services:
mosquitto:
image: docker.io/library/eclipse-mosquitto:2.1.2-alpine
configs:
- source: mosquitto_conf
target: /mosquitto/config/mosquitto.conf
secrets:
- mosquitto_passwd
volumes:
- data:/mosquitto/data
networks:
- shared
ports:
- target: 1883
published: 1883
- target: 9001
published: 9001
deploy:
replicas: 1
restart_policy:
condition: on-failure
configs:
mosquitto_conf:
file: ./mosquitto.conf
secrets:
mosquitto_passwd:
external: true
networks:
shared:
external: true
name: shared
volumes:
data:
driver: local
driver_opts:
type: nfs
o: "addr=nas.example.lan,rw,nfsvers=4,proto=tcp,nolock"
device: ":/export/swarm/mosquitto"
listener 1883
protocol mqtt
listener 9001
protocol websockets
allow_anonymous false
password_file /run/secrets/mosquitto_passwd
persistence true
persistence_location /mosquitto/data/
Configy a secrety sú vo Swarme nemenné: upravte mosquitto.conf, znova nasaďte
ten istý stack a zlyhá na „config already exists”. Config potrebuje nový názov,
alebo ho treba najprv odstrániť.
Ako ich používa Paperless
Keď sú zdieľané časti na mieste, aplikácii zmiznú databáza, oba konvertory a posledné cesty na hostiteľa:
services:
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:3.3.4
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: postgres
PAPERLESS_DBNAME: paperless
PAPERLESS_DBUSER: paperless
PAPERLESS_DBPASS: change-me
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- consume:/usr/src/paperless/consume
- export:/usr/src/paperless/export
networks:
- default
- shared
ports:
- target: 8000
published: 8000
deploy:
replicas: 1
restart_policy:
condition: on-failure
broker:
image: docker.io/valkey/valkey:9-alpine
volumes:
- broker:/data
networks:
- default
deploy:
replicas: 1
restart_policy:
condition: on-failure
networks:
default:
shared:
external: true
name: shared
volumes:
data:
driver: local
driver_opts:
type: nfs
o: "addr=nas.example.lan,rw,nfsvers=4,proto=tcp,nolock"
device: ":/export/swarm/paperless/data"
media:
driver: local
driver_opts:
type: nfs
o: "addr=nas.example.lan,rw,nfsvers=4,proto=tcp,nolock"
device: ":/export/swarm/paperless/media"
consume:
driver: local
driver_opts:
type: nfs
o: "addr=nas.example.lan,rw,nfsvers=4,proto=tcp,nolock"
device: ":/export/swarm/paperless/consume"
export:
driver: local
driver_opts:
type: nfs
o: "addr=nas.example.lan,rw,nfsvers=4,proto=tcp,nolock"
device: ":/export/swarm/paperless/export"
broker:
driver: local
driver_opts:
type: nfs
o: "addr=nas.example.lan,rw,nfsvers=4,proto=tcp,nolock"
device: ":/export/swarm/paperless/broker"
Broker zostáva v tomto stacku. Je to pracovný front Paperlessu, nie zdieľaná
služba a nič iné doň nemá čo čítať. Databáza a oba konvertory sa rozlišujú podľa
názvu cez shared a keďže volumy sú NFS exporty, naskenované adresáre aj exporty
sú len adresáre na NAS, do ktorých môžete vkladať súbory z ktoréhokoľvek stroja.
Ako ich používa n8n
Ten istý nápad, s Postgresom a runnermi:
services:
n8n:
image: docker.n8n.io/n8nio/n8n:2.44.7
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: change-me
N8N_RUNNERS_MODE: external
N8N_RUNNERS_AUTH_TOKEN: change-me
N8N_RUNNERS_BROKER_LISTEN_ADDRESS: 0.0.0.0
volumes:
- data:/home/node/.n8n
networks:
- default
- shared
ports:
- target: 5678
published: 5678
deploy:
replicas: 1
restart_policy:
condition: on-failure
runner:
image: docker.io/n8nio/runners:2.44.7
environment:
N8N_RUNNERS_AUTH_TOKEN: change-me
N8N_RUNNERS_TASK_BROKER_URI: http://n8n:5679
networks:
- default
deploy:
replicas: 2
restart_policy:
condition: on-failure
networks:
default:
shared:
external: true
name: shared
volumes:
data:
driver: local
driver_opts:
type: nfs
o: "addr=nas.example.lan,rw,nfsvers=4,proto=tcp,nolock"
device: ":/export/swarm/n8n"
n8n volume nie je voliteľný, aj keď je databáza externá: drží šifrovací kľúč, ktorý dešifruje všetky uložené prístupy. Keď ho stratíte, workflowy pobežia ďalej, ale s heslami, ktoré nikto neprečíta.
Zálohy
Jedna zálohovacia služba pokryje všetky databázy namiesto sidecaru pre každú aplikáciu. GoBackup číta config namontovaný ako swarm config a beží podľa plánu:
services:
gobackup:
image: huacnlee/gobackup:v3.1.1
configs:
- source: gobackup_yml
target: /etc/gobackup/gobackup.yml
environment:
POSTGRES_PASSWORD: change-me
MARIADB_ROOT_PASSWORD: change-me
volumes:
- backups:/root/backups
networks:
- shared
deploy:
replicas: 1
restart_policy:
condition: on-failure
configs:
gobackup_yml:
file: ./gobackup.yml
networks:
shared:
external: true
name: shared
volumes:
backups:
driver: local
driver_opts:
type: nfs
o: "addr=nas.example.lan,rw,nfsvers=4,proto=tcp,nolock"
device: ":/export/swarm/backups"
models:
databases:
schedule:
every: "1day"
at: "02:30"
compress_with:
type: tgz
storages:
nas:
type: local
keep: 14
path: /root/backups
databases:
paperless:
type: postgresql
host: postgres
port: 5432
database: paperless
username: paperless
password: $POSTGRES_PASSWORD
snipeit:
type: mysql
host: mariadb
port: 3306
database: snipeit
username: root
password: $MARIADB_ROOT_PASSWORD
$POSTGRES_PASSWORD tam rozbaľuje GoBackup z prostredia služby, nie Compose,
pretože konfiguračný súbor sa montuje až v runtime. Compose by si vyžiadal
${POSTGRES_PASSWORD} a premennú v shelli pri nasadení.
Nasadenie
docker network create --driver overlay shared
docker stack deploy -c databases.yml databases
docker stack deploy -c documents.yml documents
docker stack deploy -c mosquitto.yml mqtt
docker stack deploy -c paperless.yml paperless
docker stack deploy -c n8n.yml n8n
docker stack deploy -c backup.yml backup
Poradie je dôležité len preto, že zdieľaná sieť musí existovať prvá a zdieľané
služby by mali bežať skôr, než ich aplikácie zavolajú. Swarm ani jedno nevynucuje
— depends_on nasadenie neprežije, a to je téma nasledujúceho článku.
Čo to stojí
Toto nie je predvolená odpoveď a nie je to správne pre každú situáciu. Databáza na
NFS znamená, že diskom databázy sa stáva NAS: výpadok NAS zastaví každú aplikáciu,
ktorá na ňom závisí, a export musí bežať so synchronnými zápismi, inak je fsync
sľub, ktorý server nedodrží. Jeden Postgres pre veľa aplikácií tiež znamená jedno
okno na upgrade a jednu doménu zlyhania — docker service update na ňom je
oznámenie údržby, nie rutinné nasadenie.
Ten kompromis je zámerný: swarm postavený zo šrotu má málo pamäte a CPU, nie málo aplikácií. V takej situácii jedna starostlivo prevádzkovaná databáza porazí päť zanedbaných. Keď je cieľom kompletná zostava pre jedného zákazníka alebo jednorazové testovacie prostredie, samostatný stack je stále lepšia odpoveď.