Preskočiť na obsah
← Späť na blog

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

· Štefan Hudáč

Diagram: Paperless, n8n a Snipe-IT pripojené cez zdieľanú overlay sieť k jednému Postgresu, vedľa Gotenberg a Mosquitto, s ich vlastnými databázami preškrtnutými

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ď.