commit 86ea181c7643071840bf8b78dd197b6d9b4cde5c Author: jochen Date: Wed Sep 2 23:51:53 2026 +0200 The catalogue moves to its own repository Thirty modules the mesh builds, provisions and runs, as manifests — one per module, flat under modules/. They were in mesh-control/examples/, which framed the mesh's real modules as illustrations of a control-plane package; they are neither examples nor the control plane's. The engine that reads them stays in mesh-control; the data lives here, consumed as a build source. Answers the tier-4 question novox/hq ADR 0030 left open — where the catalogue lives — in favour of one flat repository, which the drop of domain grouping (seats, claims and tags instead) makes the right shape. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF diff --git a/README.md b/README.md new file mode 100644 index 0000000..780aa49 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# mesh-catalog + +The Novox Mesh catalogue. The modules the mesh builds, provisions and runs — as manifests, one +per module under [`modules/`](modules/). + +This is **data, not a control-plane concern**. The manifests describe *what a module is*: what it +provides, what it requires, the seats it claims, the resources the host applies for it. The +engine that reads them — parsing, eligibility resolution, sealing, declaration emission — lives +in the control plane (`novox/mesh-control`, `internal/catalogue`), which consumes this repository +as a build source. The host (`novox/mesh-host`) applies the declarations the control plane emits. +Neither is here. + +## What a module is, and is not + +A module is one thing the mesh can run, named once, described completely by its manifest. A +manifest names its image (pinned by digest), the resources the host owns for it (directories, +files, the container, the private network it joins), what it `requires` from a provider and what +it `provides` to consumers, and the sealed secrets it needs filled on the machine. + +- **Core mesh components are not modules.** The node host, the substrate, the control-plane + contexts and the surfaces are the mesh itself; they ship as their own repositories + (`mesh-host`, `mesh-substrate`, `mesh-control`, `mesh-surfaces`, `mesh-sdk`), not from here. +- **Standalone applications are not here either.** A larger application lives in its own + repository with its manifest at the root, registered with the mesh as a build source + (novox/hq [ADR 0010](https://git.novox.be/novox/hq)). This repository holds the modules the + mesh maintains as its shared catalogue; an application the mesh merely hosts keeps its manifest + beside its own code. + +So there is one home for the catalogue the mesh owns, and every application that runs *on* the +mesh rather than being *of* it carries its own — both reach the pipeline the same way, as a +registered source. + +## Layout + +``` +modules/.json one manifest per module +``` + +Flat, because the catalogue's shape carries no meaning: a module is found by its name and +described by its manifest, and what relates two modules — a shared seat, a claim, a +provider/consumer edge — is data inside the manifests, not a directory the tree encodes +(novox/hq, the domain-grouping question closed in favour of seats, claims and tags). + +## The manifest contract + +The shape a manifest must satisfy is owned by the control plane's catalogue engine and is what +validates a manifest before a machine ever sees it — a stray key, a consumer contributing the +wrong provision field, an image that nothing builds. That validation belongs with this +repository and is being re-homed here from `mesh-control`; until it is, the pipeline is the +gate — it builds each module and refuses a manifest it cannot resolve. + +## Where the reasoning lives + +Design and decisions are in [`novox/hq`](https://git.novox.be/novox/hq): + +- `02-DECISIONS/0002-everything-is-a-module.md` — one unit, no second mechanism +- `02-DECISIONS/0010-applications-live-in-their-own-repository.md` — why applications are not here +- `02-DECISIONS/0030-the-repository-structure.md` — the repositories, and the open tier-4 question this repository answers +- `03-DESIGN/00-as-is/10-module-catalogue.md` — the catalogue's shape, and what it records diff --git a/modules/bazarr.json b/modules/bazarr.json new file mode 100644 index 0000000..90e6ac2 --- /dev/null +++ b/modules/bazarr.json @@ -0,0 +1,73 @@ +{ + "module": "bazarr", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 6767, + "protocol": "tcp", + "from": "mesh", + "why": "managing subtitles" + } + ], + "resources": [ + { + "id": "config", + "type": "directory", + "path": "/services/bazarr/config", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "media-movies", + "type": "directory", + "path": "/services/media/movies", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "media-series", + "type": "directory", + "path": "/services/media/series", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "media-anime", + "type": "directory", + "path": "/services/media/anime", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "media-downloads", + "type": "directory", + "path": "/services/media/downloads", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "server", + "type": "container", + "name": "bazarr", + "image": "lscr.io/linuxserver/bazarr@sha256:3a820372f19fcb2981ea19fe4b5382934d67414afaba974bce831ddda0a64a02", + "env": { + "PUID": "1000", + "PGID": "1000", + "TZ": "Etc/UTC" + }, + "ports": [ + "6767" + ], + "volumes": [ + "/services/bazarr/config:/config", + "/services/media/movies:/movies", + "/services/media/series:/series", + "/services/media/anime:/anime", + "/services/media/downloads:/downloads" + ] + } + ] +} diff --git a/modules/dnsmasq.json b/modules/dnsmasq.json new file mode 100644 index 0000000..79d311d --- /dev/null +++ b/modules/dnsmasq.json @@ -0,0 +1,50 @@ +{ + "module": "dnsmasq", + "version": "1", + "requires": [ + "resolver-data" + ], + "provides": [ + "wildcard-resolution" + ], + "claims": [ + { + "name": "the-dns-port", + "scope": "node" + } + ], + "listens": [ + { + "port": 53, + "protocol": "udp", + "from": "mesh", + "why": "names under every machine in this mesh, for this machine and what it runs", + "fixed": true + } + ], + "resources": [ + { + "id": "package", + "type": "package", + "package": "dnsmasq" + }, + { + "id": "config", + "type": "file", + "path": "/etc/dnsmasq.conf", + "mode": "0644", + "content": "# Managed by the mesh. dnsmasq's own defaults are replaced whole rather than\n# patched, because this module owns the file and a patch would leave whatever\n# was there before to be discovered later.\n\n# What the mesh computed: one wildcard per machine, its name and everything\n# under it. Rewritten whenever a machine joins or leaves, which is why the\n# service below reflects it.\nconf-file=/etc/mesh-resolver/nodes.conf\n\n# Where it answers. Both are names the mesh chose, so this file needs to know\n# nothing about this particular machine:\n#\n# mesh0 the private network, so anything on it \u2014 including a container\n# on this machine \u2014 can ask.\n# 127.0.0.55 this machine's own use, for whatever points resolution at the\n# mesh. Not .53 or .54: systemd-resolved holds BOTH \u2014 .53 is its\n# stub and .54 its proxy stub \u2014 which this module asserted was\n# free until a machine said otherwise.\n#\n# Listening on a loopback address makes dnsmasq take the rest of\n# loopback with it, 127.0.0.1 included. That is why this module\n# claims `the-dns-port`: it takes the machine's DNS port, and\n# saying it takes only one address would be the same kind of\n# comfortable claim that .54 was free.\n#\n# .55 is a convention and not a reservation. If a future systemd\n# takes it, this line changes and nothing else does, which is the\n# reason it is written once here rather than in each module that\n# points at it.\n#\n# bind-dynamic rather than bind-interfaces: mesh0 does not exist until the\n# machine is on the private network, and binding an interface that is not there\n# yet fails to start rather than waiting for it.\nbind-dynamic\ninterface=mesh0\nlisten-address=127.0.0.55\n\n# **It forwards nothing, and must not read resolv.conf to find out where to.**\n# Whatever points this machine at the mesh writes its own address into\n# resolv.conf \u2014 so a resolver that read it for upstreams would find itself,\n# and every query it could not answer locally would loop until its receive\n# queue filled. That is not theoretical: it filled with 15KB of queries and\n# every lookup on the machine hung.\n#\n# It needs no upstream because it is never asked for anything else: the\n# asking module routes only the mesh's suffix here and leaves the rest\n# wherever the machine already sent it.\nno-resolv\ndomain-needed\nbogus-priv\n" + }, + { + "id": "service", + "type": "service", + "unit": "dnsmasq.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "config", + "mesh-resolver.nodes" + ] + } + ] +} diff --git a/modules/gitea.json b/modules/gitea.json new file mode 100644 index 0000000..feb28b8 --- /dev/null +++ b/modules/gitea.json @@ -0,0 +1,81 @@ +{ + "module": "gitea", + "version": "1", + "requires": [ + "postgres-database" + ], + "contributes": { + "postgres-database": { + "name": "gitea" + } + }, + "binds": { + "postgres-database": "/var/lib/gitea/database.json" + }, + "secrets": { + "postgres-database": "/var/lib/gitea/database.secret" + }, + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 3000, + "protocol": "tcp", + "from": "mesh", + "why": "the forge, over http" + }, + { + "port": 2222, + "protocol": "tcp", + "from": "mesh", + "why": "git over ssh. Not 22: the machine's own daemon holds that, and a module does not take it" + } + ], + "own-secrets": { + "internal-token": "/var/lib/gitea/internal-token.secret" + }, + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/gitea", + "mode": "0700" + }, + { + "id": "server-env", + "type": "file", + "path": "/var/lib/gitea/server.env", + "mode": "0600", + "content": "GITEA__security__INTERNAL_TOKEN=${secret:internal-token}\nGITEA__database__DB_TYPE=postgres\nGITEA__database__HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nGITEA__database__NAME=gitea\nGITEA__database__USER=${bound:postgres-database:as}\nGITEA__database__PASSWD=${secret:postgres-database}\n" + }, + { + "id": "data", + "type": "directory", + "path": "/services/gitea/gitea", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "server", + "type": "container", + "name": "gitea", + "image": "gitea/gitea@sha256:dfc61e347c8b582df918f4556401bf2cecdfbdb56c5282ae9488dd76fca3e41c", + "env": { + "DB_TYPE": "postgres", + "USER_UID": "1000", + "USER_GID": "1000" + }, + "env-file": [ + "/var/lib/gitea/server.env" + ], + "ports": [ + "3000", + "2222:22" + ], + "volumes": [ + "/services/gitea/gitea:/data" + ] + } + ] +} diff --git a/modules/grafana.json b/modules/grafana.json new file mode 100644 index 0000000..5dcbbd5 --- /dev/null +++ b/modules/grafana.json @@ -0,0 +1,55 @@ +{ + "module": "grafana", + "version": "1", + "own-secrets": { + "admin": "/var/lib/grafana-module/admin.secret" + }, + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 3000, + "protocol": "tcp", + "from": "mesh", + "why": "the dashboards. Also 3000 inside, like the forge - which is the mesh's port assignment earning its keep" + } + ], + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/grafana-module", + "mode": "0700" + }, + { + "id": "server-env", + "type": "file", + "path": "/var/lib/grafana-module/server.env", + "mode": "0600", + "content": "GF_SECURITY_ADMIN_PASSWORD=${secret:admin}\n" + }, + { + "id": "data", + "type": "directory", + "path": "/services/grafana/data", + "mode": "0700", + "owner": "472:472" + }, + { + "id": "server", + "type": "container", + "name": "grafana", + "image": "grafana/grafana@sha256:f772d434e8fab0049deb2b1b30abd43342bcfca1537614aa8d36080232cf4283", + "env-file": [ + "/var/lib/grafana-module/server.env" + ], + "ports": [ + "3000" + ], + "volumes": [ + "/services/grafana/data:/var/lib/grafana" + ] + } + ] +} diff --git a/modules/home-assistant.json b/modules/home-assistant.json new file mode 100644 index 0000000..6003229 --- /dev/null +++ b/modules/home-assistant.json @@ -0,0 +1,37 @@ +{ + "module": "home-assistant", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 8123, + "protocol": "tcp", + "from": "mesh", + "why": "the dashboard and the API" + } + ], + "resources": [ + { + "id": "config", + "type": "directory", + "path": "/services/home-assistant/config", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "server", + "type": "container", + "name": "home-assistant", + "image": "ghcr.io/home-assistant/home-assistant@sha256:14931c6b13756317849f46da1d01b45937a1150db66c081cfe529d48215943fe", + "network": "host", + "env": { + "TZ": "Etc/UTC" + }, + "volumes": [ + "/services/home-assistant/config:/config" + ] + } + ] +} diff --git a/modules/icecast.json b/modules/icecast.json new file mode 100644 index 0000000..77d208e --- /dev/null +++ b/modules/icecast.json @@ -0,0 +1,47 @@ +{ + "module": "icecast", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "own-secrets": { + "source": "/var/lib/icecast-module/source.secret", + "admin": "/var/lib/icecast-module/admin.secret", + "relay": "/var/lib/icecast-module/relay.secret" + }, + "listens": [ + { + "port": 8000, + "protocol": "tcp", + "from": "mesh", + "why": "streams in from sources and out to listeners" + } + ], + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/icecast-module", + "mode": "0700" + }, + { + "id": "server-env", + "type": "file", + "path": "/var/lib/icecast-module/server.env", + "mode": "0600", + "content": "ICECAST_SOURCE_PASSWORD=${secret:source}\nICECAST_ADMIN_PASSWORD=${secret:admin}\nICECAST_RELAY_PASSWORD=${secret:relay}\nICECAST_ADMIN_USERNAME=admin\n" + }, + { + "id": "server", + "type": "container", + "name": "icecast", + "image": "infiniteproject/icecast@sha256:cd506cf3dfe31ce05fd37d7e672dbd1213e7255cc93d28ecf5a3b547af4e162c", + "env-file": [ + "/var/lib/icecast-module/server.env" + ], + "ports": [ + "8000" + ] + } + ] +} diff --git a/modules/influxdb.json b/modules/influxdb.json new file mode 100644 index 0000000..ad64e0c --- /dev/null +++ b/modules/influxdb.json @@ -0,0 +1,64 @@ +{ + "module": "influxdb", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "own-secrets": { + "admin": "/var/lib/influxdb-module/admin.secret", + "admin-token": "/var/lib/influxdb-module/admin-token.secret" + }, + "listens": [ + { + "port": 8086, + "protocol": "tcp", + "from": "mesh", + "why": "queries and writes, over http" + } + ], + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/influxdb-module", + "mode": "0700" + }, + { + "id": "server-env", + "type": "file", + "path": "/var/lib/influxdb-module/server.env", + "mode": "0600", + "content": "DOCKER_INFLUXDB_INIT_MODE=setup\nDOCKER_INFLUXDB_INIT_USERNAME=admin\nDOCKER_INFLUXDB_INIT_PASSWORD=${secret:admin}\nDOCKER_INFLUXDB_INIT_ADMIN_TOKEN=${secret:admin-token}\nDOCKER_INFLUXDB_INIT_ORG=mesh\nDOCKER_INFLUXDB_INIT_BUCKET=default\n" + }, + { + "id": "data", + "type": "directory", + "path": "/services/influxdb/data", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "config", + "type": "directory", + "path": "/services/influxdb/config", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "server", + "type": "container", + "name": "influxdb", + "image": "influxdb@sha256:f75e48af0598e8aec7986e991a848d19a119101a7d563a2e5db1dfaac9c45daa", + "env-file": [ + "/var/lib/influxdb-module/server.env" + ], + "ports": [ + "8086" + ], + "volumes": [ + "/services/influxdb/data:/var/lib/influxdb2", + "/services/influxdb/config:/etc/influxdb2" + ] + } + ] +} diff --git a/modules/jackett.json b/modules/jackett.json new file mode 100644 index 0000000..f1e4a74 --- /dev/null +++ b/modules/jackett.json @@ -0,0 +1,41 @@ +{ + "module": "jackett", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 9117, + "protocol": "tcp", + "from": "mesh", + "why": "the indexer proxy" + } + ], + "resources": [ + { + "id": "config", + "type": "directory", + "path": "/services/jackett/config", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "server", + "type": "container", + "name": "jackett", + "image": "lscr.io/linuxserver/jackett@sha256:fd72d42b731ebf750b5de9711127251cf3b3f609419c32083ea8b3b3ee840b77", + "env": { + "PUID": "1000", + "PGID": "1000", + "TZ": "Etc/UTC" + }, + "ports": [ + "9117" + ], + "volumes": [ + "/services/jackett/config:/config" + ] + } + ] +} diff --git a/modules/keycloak.json b/modules/keycloak.json new file mode 100644 index 0000000..5096b20 --- /dev/null +++ b/modules/keycloak.json @@ -0,0 +1,81 @@ +{ + "module": "keycloak", + "version": "1", + "requires": [ + "postgres-database" + ], + "contributes": { + "postgres-database": { + "name": "keycloak" + } + }, + "binds": { + "postgres-database": "/var/lib/keycloak/database.json" + }, + "secrets": { + "postgres-database": "/var/lib/keycloak/database.secret" + }, + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 8080, + "protocol": "tcp", + "from": "mesh", + "why": "anything the mesh runs that authenticates a person" + } + ], + "own-secrets": { + "admin": "/var/lib/keycloak/admin.secret" + }, + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/keycloak", + "mode": "0700" + }, + { + "id": "admin-env", + "type": "file", + "path": "/var/lib/keycloak/admin.env", + "mode": "0600", + "content": "KEYCLOAK_ADMIN=admin\nKEYCLOAK_ADMIN_PASSWORD=${secret:admin}\n" + }, + { + "id": "database-env", + "type": "file", + "path": "/var/lib/keycloak/database.env", + "mode": "0600", + "content": "KC_DB_URL=jdbc:postgresql://${bound:postgres-database:at}:${bound:postgres-database:port}/keycloak\nKC_DB_USERNAME=${bound:postgres-database:as}\nKC_DB_PASSWORD=${secret:postgres-database}\n" + }, + { + "id": "net", + "type": "network", + "name": "keycloak" + }, + { + "id": "server", + "type": "container", + "name": "keycloak", + "image": "quay.io/keycloak/keycloak@sha256:ecd43971114b0c764f8a3288dddab73f98cb473daccc4feaffe4dc14adeaf866", + "network": "keycloak", + "args": [ + "start-dev" + ], + "env": { + "KC_DB": "postgres", + "KC_HTTP_ENABLED": "true", + "KC_HEALTH_ENABLED": "true" + }, + "env-file": [ + "/var/lib/keycloak/admin.env", + "/var/lib/keycloak/database.env" + ], + "ports": [ + "8080" + ] + } + ] +} diff --git a/modules/mailu.json b/modules/mailu.json new file mode 100644 index 0000000..b1fe323 --- /dev/null +++ b/modules/mailu.json @@ -0,0 +1,274 @@ +{ + "module": "mailu", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 25, + "protocol": "tcp", + "from": "anywhere", + "why": "mail from other mail servers", + "fixed": true + }, + { + "port": 465, + "protocol": "tcp", + "from": "anywhere", + "why": "submission over TLS", + "fixed": true + }, + { + "port": 587, + "protocol": "tcp", + "from": "anywhere", + "why": "submission", + "fixed": true + }, + { + "port": 993, + "protocol": "tcp", + "from": "anywhere", + "why": "IMAP over TLS", + "fixed": true + }, + { + "port": 7080, + "protocol": "tcp", + "from": "mesh", + "why": "the web interface, behind a proxy" + } + ], + "own-secrets": { + "secret-key": "/var/lib/mailu/secret-key.secret", + "database": "/var/lib/mailu/database.secret", + "admin": "/var/lib/mailu/admin.secret" + }, + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/mailu", + "mode": "0700" + }, + { + "id": "secret-env", + "type": "file", + "path": "/var/lib/mailu/secret.env", + "mode": "0600", + "content": "SECRET_KEY=${secret:secret-key}\n" + }, + { + "id": "database-env", + "type": "file", + "path": "/var/lib/mailu/database.env", + "mode": "0600", + "content": "POSTGRES_USER=mailu\nPOSTGRES_DB=mailu\nPOSTGRES_PASSWORD=${secret:database}\nDB_USER=mailu\nDB_NAME=mailu\nDB_PW=${secret:database}\n" + }, + { + "id": "admin-env", + "type": "file", + "path": "/var/lib/mailu/admin.env", + "mode": "0600", + "content": "INITIAL_ADMIN_PW=${secret:admin}\n" + }, + { + "id": "data-certs", + "type": "directory", + "path": "/services/mailu/data/certs", + "mode": "0700" + }, + { + "id": "data-data", + "type": "directory", + "path": "/services/mailu/data/data", + "mode": "0700" + }, + { + "id": "data-dkim", + "type": "directory", + "path": "/services/mailu/data/dkim", + "mode": "0700" + }, + { + "id": "data-filter", + "type": "directory", + "path": "/services/mailu/data/filter", + "mode": "0700" + }, + { + "id": "data-mail", + "type": "directory", + "path": "/services/mailu/data/mail", + "mode": "0700" + }, + { + "id": "data-mailqueue", + "type": "directory", + "path": "/services/mailu/data/mailqueue", + "mode": "0700" + }, + { + "id": "data-redis", + "type": "directory", + "path": "/services/mailu/data/redis", + "mode": "0700" + }, + { + "id": "data-webmail", + "type": "directory", + "path": "/services/mailu/data/webmail", + "mode": "0700" + }, + { + "id": "data-dovecot", + "type": "directory", + "path": "/services/mailu/data/overrides/dovecot", + "mode": "0700" + }, + { + "id": "data-nginx", + "type": "directory", + "path": "/services/mailu/data/overrides/nginx", + "mode": "0700" + }, + { + "id": "data-pgdata", + "type": "directory", + "path": "/services/mailu/data/data/psql_admindb/pgdata", + "mode": "0700" + }, + { + "id": "net", + "type": "network", + "name": "mailu" + }, + { + "id": "resolver", + "type": "container", + "name": "mailu-resolver", + "image": "ghcr.io/mailu/unbound@sha256:142aaad82ad1b0d5b59a5f1303778dba61a3e0a540f5d969c48862bcc99f6f5d", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env" + ] + }, + { + "id": "redis", + "type": "container", + "name": "mailu-redis", + "image": "redis@sha256:1db42ccef14898aa29bae778452d567534b59c107129cbc1163fb552de184d3c", + "network": "mailu", + "volumes": [ + "/services/mailu/data/redis:/data" + ] + }, + { + "id": "admindb", + "type": "container", + "name": "mailu-admindb", + "image": "postgres@sha256:7456ef82e5f5bc43d997f4781bbd7c0d6389bff397564649a356e206ba473aee", + "network": "mailu", + "env": { + "PGDATA": "/var/lib/postgresql/data/pgdata" + }, + "env-file": [ + "/var/lib/mailu/database.env" + ], + "volumes": [ + "/services/mailu/data/data/psql_admindb/pgdata:/var/lib/postgresql/data/pgdata" + ] + }, + { + "id": "admin", + "type": "container", + "name": "mailu-admin", + "image": "ghcr.io/mailu/admin@sha256:dcac20e9cbdad560faef9653b1b5ac0d9266f4098dc00f0e7f0d35f4e70ed8f1", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env", + "/var/lib/mailu/database.env", + "/var/lib/mailu/admin.env" + ], + "volumes": [ + "/services/mailu/data/data:/data", + "/services/mailu/data/dkim:/dkim" + ] + }, + { + "id": "imap", + "type": "container", + "name": "mailu-imap", + "image": "ghcr.io/mailu/dovecot@sha256:46d18ba51032be8ebd6841aa49c1ef8762c729038c5fd86a081b5b884d478af9", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env" + ], + "volumes": [ + "/services/mailu/data/mail:/mail", + "/services/mailu/data/overrides/dovecot:/overrides:ro" + ] + }, + { + "id": "smtp", + "type": "container", + "name": "mailu-smtp", + "image": "ghcr.io/mailu/postfix@sha256:bbf882880f68849511710b35237a933f3fe80c4b28bf48ff20205dbd1f1433d7", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env" + ], + "volumes": [ + "/services/mailu/data/mailqueue:/queue" + ] + }, + { + "id": "antispam", + "type": "container", + "name": "mailu-antispam", + "image": "ghcr.io/mailu/rspamd@sha256:e87ab93dd252cc69499caa5317dd10d445fd4291a7ecf6bca09793c7d475a0c8", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env" + ], + "volumes": [ + "/services/mailu/data/filter:/var/lib/rspamd" + ] + }, + { + "id": "webmail", + "type": "container", + "name": "mailu-webmail", + "image": "ghcr.io/mailu/webmail@sha256:076b720fc766e58a97321cdb700e887c2008d6d323685fe59f323088333059dc", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env" + ], + "volumes": [ + "/services/mailu/data/webmail:/data" + ] + }, + { + "id": "front", + "type": "container", + "name": "mailu-front", + "image": "ghcr.io/mailu/nginx@sha256:09f28ab6d36367fcacc7994f7021f132ac845bdc05f04bf80906102d11aaa057", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env" + ], + "ports": [ + "25", + "465", + "587", + "993", + "7080:80" + ], + "volumes": [ + "/services/mailu/data/certs:/certs", + "/services/mailu/data/overrides/nginx:/overrides:ro" + ] + } + ] +} diff --git a/modules/minio.json b/modules/minio.json new file mode 100644 index 0000000..12c285a --- /dev/null +++ b/modules/minio.json @@ -0,0 +1,107 @@ +{ + "module": "minio", + "version": "1", + "provides": [ + { + "name": "s3-bucket", + "scope": "mesh" + } + ], + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 9000, + "protocol": "tcp", + "from": "mesh", + "why": "the S3 endpoint" + } + ], + "serves": { + "s3-bucket": { + "scheme": "http", + "region": "us-east-1" + } + }, + "receives": { + "s3-bucket": "/var/lib/minio/grants/mesh.json" + }, + "grants": { + "s3-bucket": "/var/lib/minio/grants" + }, + "own-secrets": { + "root": "/var/lib/minio/root.secret" + }, + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/minio", + "mode": "0700" + }, + { + "id": "grants", + "type": "directory", + "path": "/var/lib/minio/grants", + "mode": "0700" + }, + { + "id": "root-env", + "type": "file", + "path": "/var/lib/minio/root.env", + "mode": "0600", + "content": "MINIO_ROOT_USER=meshroot\nMINIO_ROOT_PASSWORD=${secret:root}\n" + }, + { + "id": "data", + "type": "directory", + "path": "/services/minio/data/data1-1", + "mode": "0700" + }, + { + "id": "net", + "type": "network", + "name": "minio" + }, + { + "id": "server", + "type": "container", + "name": "minio", + "image": "minio/minio@sha256:aefec8a86702aff0b0dcfdd9284bd7ab7c5631cbf9be63275799e6edcb30dfa2", + "network": "minio", + "args": [ + "server", + "/data", + "--console-address", + ":9001" + ], + "env-file": [ + "/var/lib/minio/root.env" + ], + "ports": [ + "9000" + ], + "volumes": [ + "/services/minio/data/data1-1:/data" + ] + }, + { + "id": "provisioner", + "type": "container", + "name": "mesh-provision-objectstore", + "image": "mesh-provision-objectstore@sha256:0000000000000000000000000000000000000000000000000000000000000000", + "network": "minio", + "env": { + "GRANTS": "/var/lib/minio/grants", + "MESH_OBJECTSTORE_URL": "http://minio:9000", + "MESH_OBJECTSTORE_ROOT_USER": "meshroot", + "MESH_OBJECTSTORE_ROOT_PASSWORD_FILE": "/run/secrets/root" + }, + "volumes": [ + "/var/lib/minio/grants:/var/lib/minio/grants:ro", + "/var/lib/minio/root.secret:/run/secrets/root:ro" + ] + } + ] +} diff --git a/modules/nextcloud.json b/modules/nextcloud.json new file mode 100644 index 0000000..20faa8d --- /dev/null +++ b/modules/nextcloud.json @@ -0,0 +1,75 @@ +{ + "module": "nextcloud", + "version": "1", + "requires": [ + "postgres-database", + "s3-bucket" + ], + "contributes": { + "postgres-database": { + "name": "nextcloud" + }, + "s3-bucket": { + "bucket": "nextcloud" + } + }, + "binds": { + "postgres-database": "/var/lib/nextcloud-module/database.json", + "s3-bucket": "/var/lib/nextcloud-module/store.json" + }, + "secrets": { + "postgres-database": "/var/lib/nextcloud-module/database.secret", + "s3-bucket": "/var/lib/nextcloud-module/store.secret" + }, + "own-secrets": { + "admin": "/var/lib/nextcloud-module/admin.secret" + }, + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 80, + "protocol": "tcp", + "from": "mesh", + "why": "files and sync, over http; a public name is a route grant later" + } + ], + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/nextcloud-module", + "mode": "0700" + }, + { + "id": "server-env", + "type": "file", + "path": "/var/lib/nextcloud-module/server.env", + "mode": "0600", + "content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=nextcloud\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=nextcloud\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\n" + }, + { + "id": "html", + "type": "directory", + "path": "/services/nextcloud/html", + "mode": "0750", + "owner": "33:33" + }, + { + "id": "server", + "type": "container", + "name": "nextcloud", + "image": "nextcloud@sha256:0b8261f6335af6b95264ce893b4d645857638e0fa151b5ba620f25f377318ae1", + "env-file": [ + "/var/lib/nextcloud-module/server.env" + ], + "ports": [ + "80" + ], + "volumes": [ + "/services/nextcloud/html:/var/www/html" + ] + } + ] +} diff --git a/modules/nodered.json b/modules/nodered.json new file mode 100644 index 0000000..b5d90d9 --- /dev/null +++ b/modules/nodered.json @@ -0,0 +1,39 @@ +{ + "module": "nodered", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 1880, + "protocol": "tcp", + "from": "mesh", + "why": "the flow editor and the endpoints flows expose" + } + ], + "resources": [ + { + "id": "data", + "type": "directory", + "path": "/services/nodered/data", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "server", + "type": "container", + "name": "nodered", + "image": "nodered/node-red@sha256:02a2b92a41b73d2bc388238b86e4fcaab7fb5466373adb24e1df6aa5845265ff", + "env": { + "TZ": "Etc/UTC" + }, + "ports": [ + "1880" + ], + "volumes": [ + "/services/nodered/data:/data" + ] + } + ] +} diff --git a/modules/nzbget.json b/modules/nzbget.json new file mode 100644 index 0000000..95b41ca --- /dev/null +++ b/modules/nzbget.json @@ -0,0 +1,49 @@ +{ + "module": "nzbget", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 6789, + "protocol": "tcp", + "from": "mesh", + "why": "the download client's pages" + } + ], + "resources": [ + { + "id": "config", + "type": "directory", + "path": "/services/nzbget/config", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "media-downloads", + "type": "directory", + "path": "/services/media/downloads", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "server", + "type": "container", + "name": "nzbget", + "image": "lscr.io/linuxserver/nzbget@sha256:5f3d3fa71029004156eff2cbf4ef4455ce4ce59517cf13fa7d1d7c8a4cd2c8a4", + "env": { + "PUID": "1000", + "PGID": "1000", + "TZ": "Etc/UTC" + }, + "ports": [ + "6789" + ], + "volumes": [ + "/services/nzbget/config:/config", + "/services/media/downloads:/downloads" + ] + } + ] +} diff --git a/modules/ombi.json b/modules/ombi.json new file mode 100644 index 0000000..45d8a62 --- /dev/null +++ b/modules/ombi.json @@ -0,0 +1,41 @@ +{ + "module": "ombi", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 3579, + "protocol": "tcp", + "from": "mesh", + "why": "requests from viewers" + } + ], + "resources": [ + { + "id": "config", + "type": "directory", + "path": "/services/ombi/config", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "server", + "type": "container", + "name": "ombi", + "image": "lscr.io/linuxserver/ombi@sha256:a6f76ac521ba01eee2e9f0c23a3fed22e56630d97a04d5eeaeaa36c1e681640d", + "env": { + "PUID": "1000", + "PGID": "1000", + "TZ": "Etc/UTC" + }, + "ports": [ + "3579" + ], + "volumes": [ + "/services/ombi/config:/config" + ] + } + ] +} diff --git a/modules/photos.json b/modules/photos.json new file mode 100644 index 0000000..e3bac87 --- /dev/null +++ b/modules/photos.json @@ -0,0 +1,40 @@ +{ + "module": "photos", + "version": "1", + "requires": [ + "s3-bucket" + ], + "contributes": { + "s3-bucket": { + "bucket": "photos" + } + }, + "binds": { + "s3-bucket": "/etc/photos/store.json" + }, + "secrets": { + "s3-bucket": "/etc/photos/store.secret" + }, + "resources": [ + { + "id": "config", + "type": "directory", + "path": "/etc/photos", + "mode": "0750" + }, + { + "id": "app", + "type": "container", + "name": "photos", + "image": "alpine@sha256:c64c687cbea9300178b30c95835354e34c4e4febc4badfe27102879de0483b5e", + "env": { + "PHOTOS_STORE": "/etc/photos/store.json", + "PHOTOS_STORE_SECRET_FILE": "/etc/photos/store.secret" + }, + "volumes": [ + "/etc/photos/store.json:/etc/photos/store.json:ro", + "/etc/photos/store.secret:/etc/photos/store.secret:ro" + ] + } + ] +} diff --git a/modules/plex.json b/modules/plex.json new file mode 100644 index 0000000..d1287f3 --- /dev/null +++ b/modules/plex.json @@ -0,0 +1,87 @@ +{ + "module": "plex", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 32400, + "protocol": "tcp", + "from": "mesh", + "why": "streaming and the app; reaching it from outside is a route grant later" + } + ], + "resources": [ + { + "id": "config", + "type": "directory", + "path": "/services/plex/config", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "transcode", + "type": "directory", + "path": "/services/plex/transcode", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "media-movies", + "type": "directory", + "path": "/services/media/movies", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "media-series", + "type": "directory", + "path": "/services/media/series", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "media-anime", + "type": "directory", + "path": "/services/media/anime", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "media-music", + "type": "directory", + "path": "/services/media/music", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "media-audiobooks", + "type": "directory", + "path": "/services/media/audiobooks", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "server", + "type": "container", + "name": "plex", + "image": "plexinc/pms-docker@sha256:83a425ae9e133b1cb2cc3b809556e01c61cd8ff65c582e41b4374bc2210bac9e", + "network": "host", + "env": { + "PLEX_UID": "1000", + "PLEX_GID": "1000", + "TZ": "Etc/UTC" + }, + "volumes": [ + "/services/plex/config:/config", + "/services/plex/transcode:/transcode", + "/services/media/movies:/movies", + "/services/media/series:/series", + "/services/media/anime:/anime", + "/services/media/music:/music", + "/services/media/audiobooks:/audiobooks" + ] + } + ] +} diff --git a/modules/portainer.json b/modules/portainer.json new file mode 100644 index 0000000..0f0da7a --- /dev/null +++ b/modules/portainer.json @@ -0,0 +1,36 @@ +{ + "module": "portainer", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 9443, + "protocol": "tcp", + "from": "mesh", + "why": "the container dashboard, over its own tls" + } + ], + "resources": [ + { + "id": "data", + "type": "directory", + "path": "/services/portainer/data", + "mode": "0700" + }, + { + "id": "server", + "type": "container", + "name": "portainer", + "image": "portainer/portainer-ce@sha256:511f3f06c96fe3b993ebeaafde311c1959cae73a7ef825dba6397d51b450dffa", + "ports": [ + "9443" + ], + "volumes": [ + "/services/portainer/data:/data", + "/var/run/docker.sock:/var/run/docker.sock" + ] + } + ] +} diff --git a/modules/postgres.json b/modules/postgres.json new file mode 100644 index 0000000..e2d8f03 --- /dev/null +++ b/modules/postgres.json @@ -0,0 +1,101 @@ +{ + "module": "postgres", + "version": "1", + "provides": [ + { + "name": "postgres-database", + "scope": "mesh" + } + ], + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 5432, + "protocol": "tcp", + "from": "mesh", + "why": "modules on any machine that were granted a database" + } + ], + "serves": { + "postgres-database": {} + }, + "receives": { + "postgres-database": "/var/lib/postgres/grants/mesh.json" + }, + "grants": { + "postgres-database": "/var/lib/postgres/grants" + }, + "own-secrets": { + "superuser": "/var/lib/postgres/superuser.secret" + }, + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/postgres", + "mode": "0700" + }, + { + "id": "grants", + "type": "directory", + "path": "/var/lib/postgres/grants", + "mode": "0700" + }, + { + "id": "superuser-env", + "type": "file", + "path": "/var/lib/postgres/superuser.env", + "mode": "0600", + "content": "POSTGRES_PASSWORD=${secret:superuser}\n" + }, + { + "id": "data", + "type": "directory", + "path": "/services/postgres/db-data", + "mode": "0700" + }, + { + "id": "net", + "type": "network", + "name": "postgres" + }, + { + "id": "server", + "type": "container", + "name": "postgres", + "image": "postgres@sha256:7456ef82e5f5bc43d997f4781bbd7c0d6389bff397564649a356e206ba473aee", + "network": "postgres", + "env": { + "POSTGRES_USER": "postgres", + "POSTGRES_DB": "postgres" + }, + "env-file": [ + "/var/lib/postgres/superuser.env" + ], + "ports": [ + "5432" + ], + "volumes": [ + "/services/postgres/db-data:/var/lib/postgresql/data" + ] + }, + { + "id": "provisioner", + "type": "container", + "name": "mesh-provision-postgres", + "image": "mesh-provision-postgres@sha256:0000000000000000000000000000000000000000000000000000000000000000", + "network": "postgres", + "env": { + "GRANTS": "/var/lib/postgres/grants", + "MESH_PROVISION_POSTGRES": "postgres://postgres@postgres:5432/postgres?sslmode=disable", + "MESH_PROVISION_PASSWORD_FILE": "/run/secrets/superuser" + }, + "volumes": [ + "/var/lib/postgres/grants:/var/lib/postgres/grants:ro", + "/var/lib/postgres/superuser.secret:/run/secrets/superuser:ro" + ] + } + ] +} diff --git a/modules/qbittorrent.json b/modules/qbittorrent.json new file mode 100644 index 0000000..a8e8cb5 --- /dev/null +++ b/modules/qbittorrent.json @@ -0,0 +1,49 @@ +{ + "module": "qbittorrent", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 8080, + "protocol": "tcp", + "from": "mesh", + "why": "the download client's pages" + } + ], + "resources": [ + { + "id": "config", + "type": "directory", + "path": "/services/qbittorrent/config", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "media-downloads", + "type": "directory", + "path": "/services/media/downloads", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "server", + "type": "container", + "name": "qbittorrent", + "image": "lscr.io/linuxserver/qbittorrent@sha256:a00b6a597a3832a1814cde0ef60abc55c94644f3f80902c3432f6af6de8d4a96", + "env": { + "PUID": "1000", + "PGID": "1000", + "TZ": "Etc/UTC" + }, + "ports": [ + "8080" + ], + "volumes": [ + "/services/qbittorrent/config:/config", + "/services/media/downloads:/downloads" + ] + } + ] +} diff --git a/modules/radarr.json b/modules/radarr.json new file mode 100644 index 0000000..51a782f --- /dev/null +++ b/modules/radarr.json @@ -0,0 +1,57 @@ +{ + "module": "radarr", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 7878, + "protocol": "tcp", + "from": "mesh", + "why": "managing films" + } + ], + "resources": [ + { + "id": "config", + "type": "directory", + "path": "/services/radarr/config", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "media-movies", + "type": "directory", + "path": "/services/media/movies", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "media-downloads", + "type": "directory", + "path": "/services/media/downloads", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "server", + "type": "container", + "name": "radarr", + "image": "lscr.io/linuxserver/radarr@sha256:119aaa4a4f7349bcd2a136c5373a0d7925b5479915c7dfe0c0ad352db2a6d438", + "env": { + "PUID": "1000", + "PGID": "1000", + "TZ": "Etc/UTC" + }, + "ports": [ + "7878" + ], + "volumes": [ + "/services/radarr/config:/config", + "/services/media/movies:/movies", + "/services/media/downloads:/downloads" + ] + } + ] +} diff --git a/modules/redis.json b/modules/redis.json new file mode 100644 index 0000000..5f223c6 --- /dev/null +++ b/modules/redis.json @@ -0,0 +1,100 @@ +{ + "module": "redis", + "version": "1", + "provides": [ + { + "name": "redis-cache", + "scope": "mesh" + } + ], + "capabilities": [ + "container-runtime" + ], + "serves": { + "redis-cache": {} + }, + "receives": { + "redis-cache": "/var/lib/redis-module/grants/mesh.json" + }, + "grants": { + "redis-cache": "/var/lib/redis-module/grants" + }, + "own-secrets": { + "default": "/var/lib/redis-module/default.secret" + }, + "listens": [ + { + "port": 6379, + "protocol": "tcp", + "from": "mesh", + "why": "modules on any machine that were granted a cache" + } + ], + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/redis-module", + "mode": "0700" + }, + { + "id": "grants-dir", + "type": "directory", + "path": "/var/lib/redis-module/grants", + "mode": "0700" + }, + { + "id": "data", + "type": "directory", + "path": "/services/redis/data", + "mode": "0700", + "owner": "999:999" + }, + { + "id": "server-conf", + "type": "file", + "path": "/var/lib/redis-module/redis.conf", + "mode": "0600", + "content": "requirepass ${secret:default}\nappendonly yes\ndir /data\n", + "owner": "999:999" + }, + { + "id": "net", + "type": "network", + "name": "redis" + }, + { + "id": "server", + "type": "container", + "name": "redis", + "image": "redis@sha256:ff02b58f971e7d7d156a1267e283fcbbeee91773b6aa36c49dac28ecfe28eadf", + "network": "redis", + "ports": [ + "6379" + ], + "volumes": [ + "/services/redis/data:/data", + "/var/lib/redis-module/redis.conf:/etc/redis/redis.conf:ro" + ], + "args": [ + "/etc/redis/redis.conf" + ] + }, + { + "id": "provisioner", + "type": "container", + "name": "mesh-provision-redis", + "image": "mesh-provision-redis@sha256:0000000000000000000000000000000000000000000000000000000000000000", + "network": "redis", + "env": { + "GRANTS": "/var/lib/redis-module/grants", + "MESH_PROVISION_REDIS": "redis:6379", + "MESH_PROVISION_PASSWORD_FILE": "/run/secrets/default" + }, + "volumes": [ + "/var/lib/redis-module/grants:/var/lib/redis-module/grants:ro", + "/var/lib/redis-module/default.secret:/run/secrets/default:ro" + ] + } + ] +} diff --git a/modules/registry.json b/modules/registry.json new file mode 100644 index 0000000..6fe2ba0 --- /dev/null +++ b/modules/registry.json @@ -0,0 +1,52 @@ +{ + "module": "registry", + "version": "1", + "provides": [ + { + "name": "artifact-store", + "scope": "mesh" + } + ], + "claims": [ + { + "name": "the-artifact-store", + "scope": "node" + } + ], + "capabilities": [ + "container-runtime" + ], + "serves": { + "artifact-store": { + "port": 5000 + } + }, + "listens": [ + { + "port": 5000, + "protocol": "tcp", + "from": "mesh", + "why": "every machine pulls images and artifacts from here" + } + ], + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/mesh/registry", + "mode": "0700" + }, + { + "id": "store", + "type": "container", + "name": "mesh-registry", + "image": "registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373", + "ports": [ + "5000:5000" + ], + "volumes": [ + "mesh-registry-data:/var/lib/registry" + ] + } + ] +} diff --git a/modules/resolv-conf.json b/modules/resolv-conf.json new file mode 100644 index 0000000..d7b161f --- /dev/null +++ b/modules/resolv-conf.json @@ -0,0 +1,12 @@ +{ + "module": "resolv-conf", + "version": "1", + + "requires": ["wildcard-resolution"], + "claims": [{"name": "the-resolver-configuration", "scope": "node"}], + + "resources": [ + {"id": "resolv", "type": "file", "path": "/etc/resolv.conf", "mode": "0644", + "content": "# Managed by the mesh.\n#\n# For a machine where nothing else owns this file. On one where systemd-resolved\n# or NetworkManager does, assign that module instead — this one and those claim\n# the same thing, so the mesh refuses the pair rather than letting them take\n# turns overwriting each other, which is the failure this claim exists to stop.\n#\n# The mesh's resolver first, because it answers only the mesh's names and\n# forwards nothing: a query it does not recognise falls through to the next\n# line rather than being answered wrongly.\nnameserver 127.0.0.55\n\n# And what this machine used before. Replace this line with the resolver this\n# machine should use for everything that is not the mesh — it is not the mesh's\n# to choose, and a public one written here by default would send every query\n# this machine makes somewhere nobody agreed to.\nnameserver 127.0.0.53\n"} + ] +} diff --git a/modules/resolved-split-dns.json b/modules/resolved-split-dns.json new file mode 100644 index 0000000..dd3ea6d --- /dev/null +++ b/modules/resolved-split-dns.json @@ -0,0 +1,18 @@ +{ + "module": "resolved-split-dns", + "version": "1", + + "requires": ["wildcard-resolution"], + "claims": [{"name": "the-resolver-configuration", "scope": "node"}], + + "resources": [ + {"id": "drop-in", "type": "directory", "path": "/etc/systemd/resolved.conf.d", "mode": "0755"}, + + {"id": "route", "type": "file", + "path": "/etc/systemd/resolved.conf.d/mesh.conf", "mode": "0644", + "content": "# Managed by the mesh.\n#\n# **Only the mesh's names.** The tilde makes this a routing domain rather than a\n# search domain: queries under it go to the resolver below, and everything else\n# keeps going wherever this machine already sent it. A resolver that took over\n# all of DNS would be this module claiming the machine's whole network, which\n# is not what it says it claims.\n#\n# 127.0.0.55 is where the mesh's resolver answers on every machine — a fixed\n# address, so this file needs to know nothing about this particular machine.\n# systemd-resolved holds .53 and .54, which is why it is neither.\n[Resolve]\nDNS=127.0.0.55\nDomains=~internal\n"}, + + {"id": "resolved", "type": "service", "unit": "systemd-resolved.service", + "state": "running", "boot": "enabled", "restart-on": ["route"]} + ] +} diff --git a/modules/searxng.json b/modules/searxng.json new file mode 100644 index 0000000..8262a4b --- /dev/null +++ b/modules/searxng.json @@ -0,0 +1,69 @@ +{ + "module": "searxng", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "own-secrets": { + "secret": "/var/lib/searxng-module/secret.secret" + }, + "listens": [ + { + "port": 8080, + "protocol": "tcp", + "from": "mesh", + "why": "the search pages" + } + ], + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/searxng-module", + "mode": "0700" + }, + { + "id": "server-env", + "type": "file", + "path": "/var/lib/searxng-module/server.env", + "mode": "0600", + "content": "SEARXNG_SECRET=${secret:secret}\nSEARXNG_VALKEY_URL=valkey://valkey:6379/0\n" + }, + { + "id": "net", + "type": "network", + "name": "searxng" + }, + { + "id": "cache", + "type": "container", + "name": "valkey", + "image": "valkey/valkey@sha256:b21fd94099dcd4bc6b2b9230daef69b6558b887ad4a2a1afe56ff6e745a88cdb", + "network": "searxng", + "args": [ + "valkey-server", + "--save", + "30", + "1", + "--loglevel", + "warning" + ], + "volumes": [ + "searxng-valkey-data:/data" + ] + }, + { + "id": "server", + "type": "container", + "name": "searxng", + "image": "searxng/searxng@sha256:c7cc75852051bf6254afda6ed1b920dd1677d8efe4ab141bf558f02e582f4371", + "network": "searxng", + "env-file": [ + "/var/lib/searxng-module/server.env" + ], + "ports": [ + "8080" + ] + } + ] +} diff --git a/modules/sonarr.json b/modules/sonarr.json new file mode 100644 index 0000000..b42fb6c --- /dev/null +++ b/modules/sonarr.json @@ -0,0 +1,65 @@ +{ + "module": "sonarr", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 8989, + "protocol": "tcp", + "from": "mesh", + "why": "managing series" + } + ], + "resources": [ + { + "id": "config", + "type": "directory", + "path": "/services/sonarr/config", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "media-series", + "type": "directory", + "path": "/services/media/series", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "media-anime", + "type": "directory", + "path": "/services/media/anime", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "media-downloads", + "type": "directory", + "path": "/services/media/downloads", + "mode": "0755", + "owner": "1000:1000" + }, + { + "id": "server", + "type": "container", + "name": "sonarr", + "image": "lscr.io/linuxserver/sonarr@sha256:c19aa4ecdf03d73e1d5c901da33744cb7eb4d921f89bafed1ca264601d7fa224", + "env": { + "PUID": "1000", + "PGID": "1000", + "TZ": "Etc/UTC" + }, + "ports": [ + "8989" + ], + "volumes": [ + "/services/sonarr/config:/config", + "/services/media/series:/series", + "/services/media/anime:/anime", + "/services/media/downloads:/downloads" + ] + } + ] +} diff --git a/modules/tautulli.json b/modules/tautulli.json new file mode 100644 index 0000000..da05783 --- /dev/null +++ b/modules/tautulli.json @@ -0,0 +1,41 @@ +{ + "module": "tautulli", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 8181, + "protocol": "tcp", + "from": "mesh", + "why": "watch statistics" + } + ], + "resources": [ + { + "id": "config", + "type": "directory", + "path": "/services/tautulli/config", + "mode": "0700", + "owner": "1000:1000" + }, + { + "id": "server", + "type": "container", + "name": "tautulli", + "image": "lscr.io/linuxserver/tautulli@sha256:13f03ecfc61a7af89d492677389771ac29682a72153ce08a5cd4faffbc0197e8", + "env": { + "PUID": "1000", + "PGID": "1000", + "TZ": "Etc/UTC" + }, + "ports": [ + "8181" + ], + "volumes": [ + "/services/tautulli/config:/config" + ] + } + ] +} diff --git a/modules/umami.json b/modules/umami.json new file mode 100644 index 0000000..aa341e7 --- /dev/null +++ b/modules/umami.json @@ -0,0 +1,59 @@ +{ + "module": "umami", + "version": "1", + "requires": [ + "postgres-database" + ], + "contributes": { + "postgres-database": { + "name": "umami" + } + }, + "binds": { + "postgres-database": "/var/lib/umami/database.json" + }, + "secrets": { + "postgres-database": "/var/lib/umami/database.secret" + }, + "own-secrets": { + "app-secret": "/var/lib/umami/app.secret" + }, + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 3000, + "protocol": "tcp", + "from": "mesh", + "why": "the analytics pages and the collection endpoint" + } + ], + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/umami", + "mode": "0700" + }, + { + "id": "server-env", + "type": "file", + "path": "/var/lib/umami/server.env", + "mode": "0600", + "content": "DATABASE_URL=postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/umami\nAPP_SECRET=${secret:app-secret}\n" + }, + { + "id": "server", + "type": "container", + "name": "umami", + "image": "ghcr.io/umami-software/umami@sha256:fa32d116cf20cad52cbc3fad9a63b46e7fa02299d8f967168eb453d49c476b4a", + "env-file": [ + "/var/lib/umami/server.env" + ], + "ports": [ + "3000" + ] + } + ] +} diff --git a/modules/verdaccio.json b/modules/verdaccio.json new file mode 100644 index 0000000..34196f2 --- /dev/null +++ b/modules/verdaccio.json @@ -0,0 +1,51 @@ +{ + "module": "verdaccio", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 4873, + "protocol": "tcp", + "from": "mesh", + "why": "the package registry, for installs and publishes" + } + ], + "resources": [ + { + "id": "conf", + "type": "directory", + "path": "/services/verdaccio/conf", + "mode": "0755", + "owner": "10001:10001" + }, + { + "id": "storage", + "type": "directory", + "path": "/services/verdaccio/storage", + "mode": "0700", + "owner": "10001:10001" + }, + { + "id": "config", + "type": "file", + "path": "/services/verdaccio/conf/config.yaml", + "mode": "0644", + "content": "storage: /verdaccio/storage\nauth:\n htpasswd:\n file: /verdaccio/conf/htpasswd\n max_users: 10\nuplinks:\n npmjs:\n url: https://registry.npmjs.org/\npackages:\n \"**\":\n access: $all\n publish: $authenticated\n proxy: npmjs\nserver:\n keepAliveTimeout: 60\n maxBodySize: 10mb\nmiddlewares:\n audit:\n enabled: true\nlog:\n type: stdout\n format: pretty\n level: http\n" + }, + { + "id": "server", + "type": "container", + "name": "verdaccio", + "image": "verdaccio/verdaccio@sha256:fcb86134563534e2f634752e6c6c3edcdb78242ec16578c73ce39d1dadbaa801", + "ports": [ + "4873" + ], + "volumes": [ + "/services/verdaccio/storage:/verdaccio/storage", + "/services/verdaccio/conf:/verdaccio/conf" + ] + } + ] +}