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
This commit is contained in:
2026-09-02 23:51:53 +02:00
commit 86ea181c76
31 changed files with 2010 additions and 0 deletions
+59
View File
@@ -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/<name>.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
+73
View File
@@ -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"
]
}
]
}
+50
View File
@@ -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"
]
}
]
}
+81
View File
@@ -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"
]
}
]
}
+55
View File
@@ -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"
]
}
]
}
+37
View File
@@ -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"
]
}
]
}
+47
View File
@@ -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"
]
}
]
}
+64
View File
@@ -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"
]
}
]
}
+41
View File
@@ -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"
]
}
]
}
+81
View File
@@ -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"
]
}
]
}
+274
View File
@@ -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"
]
}
]
}
+107
View File
@@ -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"
]
}
]
}
+75
View File
@@ -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"
]
}
]
}
+39
View File
@@ -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"
]
}
]
}
+49
View File
@@ -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"
]
}
]
}
+41
View File
@@ -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"
]
}
]
}
+40
View File
@@ -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"
]
}
]
}
+87
View File
@@ -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"
]
}
]
}
+36
View File
@@ -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"
]
}
]
}
+101
View File
@@ -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"
]
}
]
}
+49
View File
@@ -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"
]
}
]
}
+57
View File
@@ -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"
]
}
]
}
+100
View File
@@ -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"
]
}
]
}
+52
View File
@@ -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"
]
}
]
}
+12
View File
@@ -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"}
]
}
+18
View File
@@ -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"]}
]
}
+69
View File
@@ -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"
]
}
]
}
+65
View File
@@ -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"
]
}
]
}
+41
View File
@@ -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"
]
}
]
}
+59
View File
@@ -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"
]
}
]
}
+51
View File
@@ -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"
]
}
]
}