Configure an app for Catena
Catena manages an app from labels written in its compose file: it publishes the address, puts the sign-in gate in front, backs up the data, updates the app and watches it on the status page. In this guide, you add an app from a repository, then prepare an app’s own compose file and publish it in a repository of yours. An app that goes through every step is fully managed.
When the software is in Catena’s catalog, the catalog entry is the shorter route: its labels, networks, placement and sign-in client are already in place, and you add it as described in Add an app from a repository.
Add an app from a repository
Section titled “Add an app from a repository”Administrators add, configure and remove apps in Manage applications, below the tiles of the Apps page. Each app lives in its own folder on the server: its compose file, a settings file and a note of where it came from. The folder is in every backup and runs the app without Catena (see Restore one app on any machine).
An app comes from a git repository on the server’s list. Catena’s catalog is on the list from the start and is trusted. To add an app:
- Open Apps and go to Add an application.
- Choose the Repository, enter the Application name and press Show versions.
- Choose a Version. The list holds the newest ten. A version is a tag the repository published for the app.
- Optionally enter Name on this server: lower-case letters, digits and hyphens. Without it, the app keeps its own name. A name that is already a folder or an app on the server is refused.
- Press Add.
Catena fetches the app at that version, checks its compose file (see Check an app with the panel), writes the folder and deploys it. A file with an error in the check is refused and not kept; warnings and notes do not stop the deploy. The page then reads “<app> was added.” and the app’s tile appears once Catena has picked the app up, within seconds. A refusal shows the server’s answer under the notice.
Settings, start, stop and remove
Section titled “Settings, start, stop and remove”Each app has a card under Manage applications:
- Settings. The values the app lets you change, each with its default. Type the values and press Save settings: the app is deployed again once with all of them. Only the values you changed are written, and a value that another administrator changed since the page opened is refused as out of date. A value that spans several lines is shown but not editable here. When that fails, the page reads “The settings were saved, but
<app>did not restart with them. Start it again to apply them.” A stopped app takes the values when you start it. A secret value is never shown again: its field reads “A value is set” or “No value is set”, and you type a new value in its own field and press Save to replace it. Values Catena generates, or keeps set itself, are listed by name and are read-only. A value is stored as plain text; neither Docker nor a shell runs it as a command. - Start and Stop. Stopping needs a tick on the confirmation box and keeps the app’s data.
- Remove. Needs a tick on the confirmation box. Tick Also delete its data (cannot be undone) to delete the app’s data as well; without it, the data stays.
A badge on the card says whether the app is still as Catena deployed it, and the card shows an update waiting for you. An app you changed by hand, or through another container manager, is not updated, and its badge says so. The badges, the Outside Catena list and the optional Portainer and Arcane apps of the catalog are covered in Apps changed outside Catena.
Repositories
Section titled “Repositories”Repositories, under Manage applications, lists the repositories you can add apps from. To add one:
- Enter its Repository address: an
https://address with no password, query or fragment. - For a private repository, enter an Access token. It is saved on the server and never shown again; the list only says “Token saved”. To replace it, enter a new one. To delete it, tick Delete the saved token.
- Tick Trusted: apply its updates by themselves only for a repository whose author you trust. Updates from a trusted repository apply by themselves in the nightly maintenance; for the others, each update waits for you on the app’s card (see Apps added from a repository).
- Press Save repository.
Saving an address already on the list changes its trust and its token. Remove takes a repository off the list, with its token; the apps already added from it stay.
A repository holds one folder per app under blueprints/, as blueprints/<app>/docker-compose.yml with the app’s other files beside it, at most 500 files and 5 MiB, with no link or submodule. It publishes each version as a tag spelled <app>/<version>-<revision>, such as my-app/1.2.3-1, where the version and the revision use letters, digits and the characters ., _, + and -.
Catena reads a repository’s files as data and checks them before it deploys them. Only an unchanged file of Catena’s catalog may give an app the server’s network or the container engine, read stored secrets, or take values that another app provides. Any other file that asks for them is refused.
What an app can ask of Catena
Section titled “What an app can ask of Catena”The x-catena block of an app (see Declare the settings and Declare the backup mode and update steps) can declare what the app needs of the server. Catena fulfils it before each deploy: a certificate for the app’s names and DNS records in the zones the server serves. A name outside the domains the server serves is refused, unless the file is an unchanged file of Catena’s catalog. Labels ask for sign-in (see Sign in with Catena accounts), and the block can take an app’s users from a sign-in group.
An app can also provide a service to other apps, which the apps that use it find by themselves. Once the ClamAV app of the catalog runs, Nextcloud and the mail server are deployed again once with its address and scan through it. Nextcloud sets itself up the same way for the OnlyOffice or Collabora editor of the catalog. The call relay app of the catalog provides the relay that Nextcloud Talk and Element use: adding it deploys them again once with its address and secret, and removing it sets them back. Adding Talk or Element never adds the relay, and the relay cannot be added before the server has its domain, because it needs a name and a certificate. The mail server app has Catena publish its address record, its MTA-STS record and its TLS-RPT record; you publish its MX, SPF, DKIM and DMARC records yourself.
How Catena reads an app
Section titled “How Catena reads an app”An app is a set of services. One of them, the front-door service, answers the web requests: the web server or the application server. The others (database, cache, background workers) are internal and stay out of reach of the internet and of other apps.
You write labels under each service’s own labels: key, as a list with one "key=value" per line. The status page, restores, migrations and updates read them from there; labels under deploy.labels are not seen by them.
A service that carries vps.route.host has a public address, and its port, who gets in and health check are read from that service alone. Most apps have one address, on the front-door service. An app can have several, such as a public site and an admin page served by another service. With two or more, you mark the main address with vps.route.main=true on its service. The labels that describe the app as a whole (its tile, its title on the status page, its sign-in entry, vps.auth.protected) are read from the main address’s service only, as the running app has them, and are ignored on any other service. With one address, that service is the main address and needs no mark. When an app has two or more addresses and none or several are marked main, the app gets no route.
Access is set per address and is never inherited: an address with no access label admits administrators only, even when the main address is public. Two addresses cannot share a host name: the one deployed first keeps it, and the other gets no route. The update policy and the published ports describe a single service, and each is optional. Which app a service belongs to is the app’s name on the server, and the service is its key in the file; Catena reads both from Docker Swarm itself, so no label names them. The label reference says where each label goes.
A label value can be a ${VARIABLE} from the app’s settings (see Declare the settings): Catena reads the value the deployed service received. A vps.* label that Catena does not recognise has no effect.
The skeleton below has every piece the steps add: a front-door service web and a database db.
services: web: image: example/web:1.2.3 deploy: placement: constraints: - node.labels.catena.role==data volumes: - web-data:/data cap_drop: - NET_RAW networks: - catena-network - default labels: - "vps.route.host=my-app.yourdomain.com" - "vps.route.port=8080" - "vps.auth.groups=staff" - "vps.health.path=/health"
db: image: postgres:1.2.3-alpine deploy: placement: constraints: - node.labels.catena.role==data environment: POSTGRES_USER: my-app POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - db-data:/var/lib/postgresql/data networks: - default
volumes: web-data: db-data:
networks: catena-network: external: trueStep 1: Choose the app’s name
Section titled “Step 1: Choose the app’s name”Give the app one short name made of lower-case letters, digits and hyphens, such as my-app. It is the app’s name on the server, which you give when you add the app (step 7). Restores, migrations and updates find the app by that name, and the proxy reaches the front-door service at my-app_web: the app’s name, an underscore and the service’s key in the file.
Step 2: Make the file ready for Docker Swarm
Section titled “Step 2: Make the file ready for Docker Swarm”Apps run on Docker Swarm, which refuses some compose keys outright and silently ignores others. The catalog is checked against these rules; when you write a file by hand, you follow them yourself.
- Restart policy. Swarm restarts a stopped service by default. Write
deploy.restart_policyonly for a service that must not restart (condition: none) or only after a failure (condition: on-failure), such as a setup job. - Placement for data. Every service that mounts a named volume or a host folder has the placement constraint
node.labels.catena.role==data. A volume is local to one server: on another, swarm creates it empty, without an error. - No
depends_on. Swarm has no start ordering and drops the key without a message. Swarm restarts a service that started too early. When a crash and restart does damage (an installer that half-completes), the entrypoint waits for the database itself. - No inline
configs:. The check refuses afile:orcontent:entry (rule S4). The service’s entrypoint writes the config file instead. Onlyexternal: trueconfigs are accepted. tmpfsin long form (type: tmpfs). The short key is dropped silently. A tmpfs mount isnoexec; a path that needs execution is a volume.- Published ports in long form with
mode: host, which keeps the real client address (the default mode replaces it). See step 4 for the label that opens them in the firewall. - Refused keys, which make swarm reject the whole file:
mem_limit,memswap_limit,cpus,cpu_shares,volumes_from,profiles,scale. Limits go indeploy.resources.limits, the replica count indeploy.replicas. - Dropped keys, with no effect:
restart,container_name,expose,links,external_links,build,network_mode,security_opt,shm_size. Images are built and published elsewhere; swarm only pulls them. - Server access keys. Swarm also drops
privileged,pidanddevices, so the app runs without what it asked for. The panel’s App check reportsprivileged: true,pid: hostanddevicesas errors (X1, X3, X5): the file asks for control of the server. The corrected file keeps the key, so you decide. - No raw network access on
catena-network. Docker gives every container the right to send raw network packets, which lets a container oncatena-networkforge traffic to Catena’s own services. Every service that joinscatena-networkhascap_drop: [NET_RAW]. An app that needs raw packets (a network scanner, for example) addscap_add: [NET_RAW]instead. The panel’s App check reports a service that keeps it (X11), and the corrected file carries the drop. An app already deployed keeps the access until you publish the corrected file as a new version. - Full version tags. Every image carries a full version tag, such as
1.2.3or1.2.3-alpine. Only those are updated automatically:latest,stable,1.2or1are never touched.
Step 3: Connect the front door
Section titled “Step 3: Connect the front door”The front-door service joins the external network catena-network and keeps the app’s own default network to reach the internal services. No alias is needed. The internal services stay on default only. The file declares catena-network as external: true (see the skeleton above).
The reverse proxy reaches the front-door service through that network, and every other service with an address the same way. A service with an address that is not on catena-network gets no route for that address. An internal service with no catena-network entry is unreachable from the proxy and from other apps.
Step 4: Label the front door
Section titled “Step 4: Label the front door”Write these labels on the front-door service:
vps.route.hostwith the public address, such asmy-app.yourdomain.com. Without it the app has no route, no tile and no status-page card.vps.route.portwith the port the app listens on inside its container, when it is not 80.- Who gets in:
vps.auth.mode=publicfor an app open to everyone. The app’s own login, if it has one, stays in charge.vps.auth.groupswith the Keycloak groups allowed in, for a private app. A person in any one of them gets in.- Neither label: only administrators get in.
vps.health.pathwith an address of the app that answers 200 when the app works, such as/health. Without it, the status page checks the app’s front page: a public app is up when the page answers without an error, and an app behind sign-in when it sends visitors to sign in.- Optionally, the tile’s name, icon and description on the Apps tab, and the title of the status-page card.
Administrators always get in. Groups are not created by a label: they already exist in Keycloak, managed in its admin console at auth.yourdomain.com or in the People panel with Catena Pro (see Sign-in and people). The gate decides who reaches the app, not what a person can do inside it: roles and permissions inside the app are the app’s own settings.
A service that publishes a port for a protocol other than web traffic (mail, a TURN relay) also carries vps.expose.tcp or vps.expose.udp with those ports, which opens them in the server’s firewall. A label never opens a port the server itself declares (SSH, or a port kept to the server or to your private network): Catena ignores that entry and logs it.
services: relay: image: example/relay:1.2.3 ports: - target: 3478 published: 3478 protocol: udp mode: host labels: - "vps.expose.udp=3478"Another address (optional)
Section titled “Another address (optional)”When another service of the app answers on an address of its own, such as an admin page, give that service its labels too:
- On the front-door service, add
vps.route.main=true. It marks the address that the tile opens and that carries the app-wide labels. - On the other service, add
vps.route.hostwith its own address,vps.route.portwhen it is not 80, and its access:vps.auth.mode=public, orvps.auth.groupswith the groups allowed in. With neither, only administrators get in, whatever the main address allows. - Add
vps.health.pathon it when its front page is not a good check. The status page then shows one card for each address. - Join that service to
catena-networktoo (step 3).
Leave the tile, the status-page title, vps.auth.protected and the sign-in labels on the front-door service. In the file below, web is public and admin admits the staff group:
services: web: image: example/web:1.2.3 cap_drop: - NET_RAW networks: - catena-network - default labels: - "vps.route.host=my-app.yourdomain.com" - "vps.route.main=true" - "vps.auth.mode=public" - "vps.health.path=/health" - "vps.display-name=My app"
admin: image: example/admin:1.2.3 cap_drop: - NET_RAW networks: - catena-network - default labels: - "vps.route.host=my-app-admin.yourdomain.com" - "vps.route.port=8080" - "vps.auth.groups=staff"
networks: catena-network: external: trueStep 5: Set the update policy (optional)
Section titled “Step 5: Set the update policy (optional)”By default an image moves to newer patch releases only. A service that should move further, or not at all, carries vps.auto-update (see Updates).
Nothing else is labelled per service. A service whose image is a database or cache (PostgreSQL, PostGIS, TimescaleDB, MariaDB, MySQL, MongoDB, Redis, Valkey, KeyDB, Memcached, Elasticsearch, OpenSearch) is recognised from its image and keeps running when a migration pauses the rest of the app.
Step 6: Plan the data
Section titled “Step 6: Plan the data”- Named volumes live in the Docker volumes folder and are in every backup. Each volume is declared by name: anonymous volumes are backed up too, but a single-app restore cannot reach them.
- Bind mounts inside the apps area are included. A bind mount to any other absolute path makes the backup fail, unless you list that folder under Settings > Backup repository > Other folders to back up (separated by commas). See Backups and S3 storage.
- Databases are dumped automatically before each backup. The image name decides, not a label:
- a running container whose image name contains
postgresis dumped withpg_dumpall, as the user inPOSTGRES_USER; - a container whose image name contains
mariadbormysqlis dumped, and needsMARIADB_ROOT_PASSWORDorMYSQL_ROOT_PASSWORDin its environment; - a dump that fails fails the backup.
- a running container whose image name contains
- Other engines (MongoDB, SQLite and similar) get no automatic dump. You need the app’s own export, or a volume copy that is consistent on its own.
- Large file sets belong in S3-backed storage rather than in a local volume, which keeps the backup small.
A backup does not pause an app added by hand unless the file declares a backup mode (see Declare the backup mode and update steps): otherwise nothing is put in maintenance mode around the snapshot. A catalog entry can ask for it instead: Nextcloud, for one, is put in maintenance mode while the nightly maintenance takes its backup, and taken out of it afterwards, even when the backup fails.
Step 7: Publish and add
Section titled “Step 7: Publish and add”- In a git repository you can reach over
https://, put the compose file atblueprints/<app>/docker-compose.yml, where<app>is the name from step 1, with the app’s other files beside it. - Tag the version
<app>/<version>-<revision>, such asmy-app/1.2.3-1. - Under Repositories on the Apps page, save the repository’s address, with an access token when it is private (see Repositories).
- Under Add an application, choose the repository, enter
<app>, choose the version and press Add (see Add an app from a repository). - Enter passwords and keys as the app’s settings (see Declare the settings). They stay out of the file, so you can rotate them without publishing a new version.
To change the app later, publish a new tag; the app moves to it as described in Apps added from a repository.
Step 8: Check
Section titled “Step 8: Check”- Wait a few seconds: Catena picks up the deploy by itself, and checks again every hour.
- Open the address. A private app shows the sign-in page first; a public app opens directly.
- The app’s tile is on the Apps tab, and its card on the status page turns green.
When a check fails, the troubleshooting table lists the usual causes. To catch most mistakes before you deploy, check the file with the panel.
Sign in with Catena accounts (optional)
Section titled “Sign in with Catena accounts (optional)”An app that supports OpenID Connect can let people sign in with their Catena account instead of a separate password. Labels make Catena create the app’s own sign-in entry in Keycloak and hand the app its id, secret and issuer.
- Add these labels to the front-door service:
vps.auth.oidc=true.vps.auth.oidc.redirect_uriswith the app’s callback address (the app’s documentation names it), such ashttps://my-app.yourdomain.com/auth/callback. Separate several addresses with commas.vps.auth.oidc.public=trueonly for an app that cannot keep a secret, such as a phone app. The entry then has no secret and requires PKCE.
- In the compose file, pass the three values to the app under the names it expects, with an empty default so that the first deploy works:
${OIDC_CLIENT_ID:-},${OIDC_CLIENT_SECRET:-}(absent for a public app) and${OIDC_ISSUER_URL:-}. - Add the app. Within a few minutes, Catena creates the entry, writes the three values into the app’s settings and deploys the app again when they change. Keycloak generates the secret; Catena copies it.
Each return address is https:// on a domain this server serves, or a phone app’s own scheme such as app.immich:///oauth-callback. A *, plain http:// or a # is refused. When a label is wrong, Catena makes and changes nothing for that app.
Who can sign in through the app’s own button follows the app’s groups: only administrators for an app only they can open, staff and administrators when the groups go beyond admin, and every account when client is among them. A public app with no vps.auth.groups admits staff and administrators.
Catena only changes entries it made. An entry with the same name that you made by hand is never taken over. Removing the labels, or the app, deletes the entry Catena made.
To check that it worked:
- On the Apps tab, an administrator sees on the app’s tile sign-in ready, sign-in waiting or sign-in failed, with the reason, in place of the OIDC badge. Staff see the OIDC badge.
- Waiting means Catena tries again on its own. Failed means something needs fixing, and the tile says what. The Log tab also names the app and the reason.
See Single sign-on for how sign-in works across apps.
Declare the settings (optional)
Section titled “Declare the settings (optional)”The values an app’s file reads as ${VARIABLE} come from its settings file. Declare each one in the x-catena block, with schema: 2, as one of:
- a setting you change on the app’s card, with a default and a label in English and in French;
- a generated secret that Catena makes on the first deploy, with its length and its alphabet (
alnumorhex).
x-catena: schema: 2 env: APP_HOSTNAME: setting: default: my-app.yourdomain.com label: { en: "Address", fr: "Adresse" } DB_PASSWORD: generated: { length: 32, alphabet: alnum }A variable that is neither declared nor written by Catena, as the sign-in values are, reads as empty.
Declare the backup mode and update steps (optional)
Section titled “Declare the backup mode and update steps (optional)”An app can carry its own backup mode and update steps in an x-catena block at the top of its compose file. The app’s folder keeps the file as Catena deployed it, so the block travels with the app: backups and restores bring it back, and the server reads it from that file, not from the catalog.
x-catena: schema: 1 quiesce: service: web pre: - ["pkill", "-STOP", "-x", "node"] post: - ["pkill", "-CONT", "-x", "node"] timeout_seconds: 10 lifecycle: service: web migrate: - ["yarn", "db:migrate"] versioned_volumes: ["web-data"] timeout_seconds: 900Start the block with schema: 1. Each command is a list of words (["yarn", "db:migrate"]), run with docker exec inside the named service of the app, with no shell. Each section sets service and timeout_seconds; user is optional and names the account the commands run as.
quiesceis the backup mode. The nightly maintenance runs theprecommands before the backup, such as putting the app in maintenance mode, and thepostcommands after it, even when the backup fails. The nightly maintenance needs Catena Pro or Catena Business (see Schedules).postmust succeed whenprenever ran. Each command may take up to 60 seconds. A command still running when its time is up gets as long again, up to two minutes, to finish, and is stopped after that, before the next command runs; a stopped command is noted in the Log tab.lifecycleholds the update steps, each command taking up to 3600 seconds:before_updateruns in the running container just before an update deploys the new image, andafter_updateonce the update is settled, whether kept or rolled back.migrateruns when an update moves the image ofservice, and after a restore of the whole server once the databases are put back. When it fails after an update, the update is rolled back, image and database.versioned_volumeslists the app’s own named volumes that the new version upgrades in place and the previous version cannot read. The server copies each one before an update that movesserviceand puts the copy back when the update rolls back. A volume that a database or a one-shot service mounts cannot be listed.
The updates themselves run in the nightly maintenance, which needs Catena Pro or Catena Business (see Updates).
The server refuses the app’s updates when the block does not hold for its file. The rules:
- A
servicenames a service the file declares. - No value holds a
$: Docker reads it as a variable when it deploys the file. Write the commands without one. - No value names an image of the file, because an update rewrites those.
- A timeout stays within its limit.
- A format newer than the server reads is refused until the server is updated.
Check the file with the panel before you deploy: rule K17 reports each of these and lists the commands the block has the server run.
Label reference
Section titled “Label reference”Every label goes under the service’s own labels: key.
On each service with an address
Section titled “On each service with an address”On every service that carries vps.route.host: the front-door service and any other address. Each address reads its own.
| Label | Values | Default | Description |
|---|---|---|---|
vps.route.host |
An address under yourdomain.com |
none | Public address. The service that carries it has an address of the app. Without it on any service, the app has no route, no tile and no status-page card. Two addresses cannot share a host name: the one deployed first keeps it. |
vps.route.port |
A port number | 80 |
Port the service listens on inside its container. |
vps.auth.mode |
public, private, admin-only |
none | public: no gate. private: the groups in vps.auth.groups, or admin, client and staff when none are listed. admin-only: administrators only. |
vps.auth.groups |
Keycloak group names, comma-separated | none | Groups allowed in; a person in any one of them gets in. Ignored with public and admin-only. The group visitor makes the address public. |
vps.health.path |
A URL path | none | Address the status page checks every 2 minutes; the address is up when it answers 200. Without it, the check reads the front page: a public address is up when it answers without an error, an address behind sign-in when it sends visitors to sign in. |
vps.health.expect |
Text | none | Text the reply must contain. Used only with vps.health.path. |
With neither vps.auth.mode nor vps.auth.groups, only administrators get in. This holds for each address on its own: an address does not take the access of the main address. Administrators always get into a private address.
On the front-door service, once
Section titled “On the front-door service, once”On the main address’s service only. On any other service these labels are ignored.
| Label | Values | Default | Description |
|---|---|---|---|
vps.route.main |
true, false |
false |
Marks the main address. Needed only when the app has two or more addresses, on exactly one service. With none or several marked, the app has no route. |
vps.auth.protected |
true, false |
false |
true marks an app that must not be public: when its labels make it public anyway, its tile on Apps carries the badge “should require sign-in”, shown to administrators only. |
vps.auth.oidc |
true, false |
false |
Catena creates the app’s own sign-in entry in Keycloak and puts OIDC_CLIENT_ID, OIDC_CLIENT_SECRET and OIDC_ISSUER_URL in the app’s settings. Staff see an OIDC badge on the tile; administrators see the state of the sign-in. |
vps.auth.oidc.redirect_uris |
Addresses, comma-separated | none | Return addresses of the sign-in entry. Required with vps.auth.oidc. See Sign in with Catena accounts. |
vps.auth.oidc.public |
true, false |
false |
true for an app that sends no secret: the entry has no secret and requires PKCE. |
vps.display-name |
Text | the main address | Title of the main address’s card on the status page. The other addresses’ cards are titled by their address. |
vps.homepage.name |
Text | the app’s name on the server | Title of the tile on the Apps tab. |
vps.homepage.icon |
An icon name, or an image address | mdi-application |
Tile icon. An address starting with http is shown as an image. |
vps.homepage.description |
Text | none | Tile subtitle. |
vps.homepage.hidden |
true, false |
false |
Hides the tile from everyone except administrators. The app stays reachable. |
Updates
Section titled “Updates”On any service, optional, and read from whichever service carries it.
| Label | Values | Default | Description |
|---|---|---|---|
vps.auto-update |
off, patch, minor, patch+minor, major |
patch |
How far the image’s version may move. patch keeps the major and minor numbers (1.2.3 to 1.2.9). minor and patch+minor keep the major number (1.2.3 to 1.9.0). major takes any newer release (1.2.3 to 2.0.0). off never moves it. An invalid value counts as patch. |
Only images with a full version tag are updated. Services that run the same image move together, under the most restrictive of their labels; a service without the label counts as patch. The updates run in the nightly maintenance, which needs Catena Pro or Catena Business (editions); see Updates.
Published ports
Section titled “Published ports”On the service that publishes the ports.
| Label | Values | Default | Description |
|---|---|---|---|
vps.expose.tcp |
Ports or ranges, comma-separated (25,465, 7000-7010) |
none | Opens these TCP ports to the internet in the server’s firewall. The service publishes them itself, in long form with mode: host. A catalog app that is granted the host network publishes nothing: its labels open the ports it listens on directly on the server. An entry that overlaps a port the server declares itself, or an invalid entry, is ignored. |
vps.expose.udp |
Ports or ranges, comma-separated | none | The same for UDP ports. |
Worked example: Nextcloud
Section titled “Worked example: Nextcloud”The catalog entry for Nextcloud is the recommended way to run it: it carries more precautions than the simplified file below, and its sign-in client is already made. Add the catalog entry and enter its storage settings: that is the whole procedure, and Catena registers Keycloak inside Nextcloud itself. The file below shows the same design with the pieces a hand-written app needs, added under the name my-nextcloud; the proxy reaches app at my-nextcloud_app. Image tags are placeholders.
services: app: image: nextcloud:1.2.3-apache deploy: placement: constraints: - node.labels.catena.role==data entrypoint: - /bin/sh - -ec - | cat > /docker-entrypoint-hooks.d/before-starting/zz-catena.sh <<'HOOK' #!/bin/sh set -e php occ config:system:set --type int \ --value "$${NEXTCLOUD_MAX_CHUNK_SIZE}" files.chunked_upload.max_size HOOK chmod 0755 /docker-entrypoint-hooks.d/before-starting/zz-catena.sh wait_for_db() { php -r 'try { new PDO( "pgsql:host=".getenv("POSTGRES_HOST").";dbname=".getenv("POSTGRES_DB"), getenv("POSTGRES_USER"), getenv("POSTGRES_PASSWORD") ); } catch (Throwable $$e) { exit(1); } exit(0);' 2>/dev/null } waited=0 until wait_for_db; do waited=$$((waited + 2)) if [ "$$waited" -ge 300 ]; then echo "database did not answer in $${waited}s" >&2 exit 1 fi sleep 2 done exec /entrypoint.sh "$$@" - -- command: ["apache2-foreground"] environment: NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_HOSTNAME} NEXTCLOUD_ADMIN_USER: ${NEXTCLOUD_ADMIN_USER} NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASSWORD} NEXTCLOUD_MAX_CHUNK_SIZE: ${NEXTCLOUD_MAX_CHUNK_SIZE:-83886080} OVERWRITEPROTOCOL: https OVERWRITEHOST: ${NEXTCLOUD_HOSTNAME} OVERWRITECLIURL: https://${NEXTCLOUD_HOSTNAME} TRUSTED_PROXIES: 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 POSTGRES_HOST: db POSTGRES_DB: nextcloud POSTGRES_USER: nextcloud POSTGRES_PASSWORD: ${DB_PASSWORD} REDIS_HOST: redis OBJECTSTORE_S3_BUCKET: ${S3_BUCKET} OBJECTSTORE_S3_REGION: ${S3_REGION} OBJECTSTORE_S3_HOST: ${S3_HOST} OBJECTSTORE_S3_PORT: ${S3_PORT:-443} OBJECTSTORE_S3_SSL: ${S3_SSL:-true} OBJECTSTORE_S3_USEPATH_STYLE: "true" OBJECTSTORE_S3_AUTOCREATE: "false" OBJECTSTORE_S3_KEY: ${S3_ACCESS_KEY} OBJECTSTORE_S3_SECRET: ${S3_SECRET_KEY} NEXTCLOUD_OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-} NEXTCLOUD_OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-} NEXTCLOUD_OIDC_ISSUER_URL: ${OIDC_ISSUER_URL:-} volumes: - nc-html:/var/www/html - nc-config:/var/www/html/config - nc-apps:/var/www/html/custom_apps - nc-data:/var/www/html/data labels: - "vps.route.host=nextcloud.yourdomain.com" - "vps.route.port=80" - "vps.auth.mode=public" - "vps.auth.oidc=true" - "vps.auth.oidc.redirect_uris=https://nextcloud.yourdomain.com/apps/user_oidc/code" - "vps.health.path=/status.php" - "vps.auto-update=patch" cap_drop: - NET_RAW networks: - catena-network - default
db: image: postgres:1.2.3-alpine deploy: placement: constraints: - node.labels.catena.role==data environment: POSTGRES_DB: nextcloud POSTGRES_USER: nextcloud POSTGRES_PASSWORD: ${DB_PASSWORD} PGDATA: /var/lib/postgresql/data/pgdata volumes: - db-data:/var/lib/postgresql/data labels: - "vps.auto-update=patch" networks: - default
redis: image: redis:1.2.3-alpine labels: - "vps.auto-update=patch" networks: - default
cron: image: nextcloud:1.2.3-apache deploy: placement: constraints: - node.labels.catena.role==data entrypoint: - /bin/sh - -ec - | wait_for_db() { php -r 'try { new PDO( "pgsql:host=".getenv("POSTGRES_HOST").";dbname=".getenv("POSTGRES_DB"), getenv("POSTGRES_USER"), getenv("POSTGRES_PASSWORD") ); } catch (Throwable $$e) { exit(1); } exit(0);' 2>/dev/null } waited=0 until wait_for_db; do waited=$$((waited + 2)) if [ "$$waited" -ge 300 ]; then echo "database did not answer in $${waited}s" >&2 exit 1 fi sleep 2 done exec /cron.sh environment: POSTGRES_HOST: db POSTGRES_DB: nextcloud POSTGRES_USER: nextcloud POSTGRES_PASSWORD: ${DB_PASSWORD} REDIS_HOST: redis OBJECTSTORE_S3_BUCKET: ${S3_BUCKET} OBJECTSTORE_S3_REGION: ${S3_REGION} OBJECTSTORE_S3_HOST: ${S3_HOST} OBJECTSTORE_S3_PORT: ${S3_PORT:-443} OBJECTSTORE_S3_SSL: ${S3_SSL:-true} OBJECTSTORE_S3_USEPATH_STYLE: "true" OBJECTSTORE_S3_KEY: ${S3_ACCESS_KEY} OBJECTSTORE_S3_SECRET: ${S3_SECRET_KEY} volumes: - nc-html:/var/www/html - nc-config:/var/www/html/config - nc-apps:/var/www/html/custom_apps - nc-data:/var/www/html/data networks: - default
volumes: nc-html: nc-config: nc-apps: nc-data: db-data:
networks: catena-network: external: trueYou declare the variables (NEXTCLOUD_HOSTNAME, NEXTCLOUD_ADMIN_USER, NEXTCLOUD_ADMIN_PASSWORD, DB_PASSWORD and the S3_* values) as settings (see Declare the settings) and enter them on the app’s card. NEXTCLOUD_HOSTNAME is nextcloud.yourdomain.com. Catena fills the OIDC_* values itself (see below).
Why each part is there
Section titled “Why each part is there”- Storage. User files go to an S3 bucket through the
OBJECTSTORE_S3_*variables. The backup then holds only the database and the small local volumes, and the bucket is covered by its own provider-side durability.nc-datastill holds logs and application data, so it is a named volume.nc-htmlholds the code tree: with it persisted, a recreated container is recognized as an existing install and upgraded, instead of being installed again over live data. - Placement.
app,dbandcronmount volumes, so all three are pinned to the data node.rediskeeps nothing and needs no pin: a missing cache is a cache miss. - Database wait instead of
depends_on. A crash-and-restart loop until the database answers is not safe for Nextcloud: the installer gives up after ten tries, but by then the version file is already written, so the next start takes the upgrade path and the instance stays “not installed” until the volume is wiped. The entrypoint therefore opens a real database connection (not just a TCP check, because Postgres accepts connections while still starting) and only then hands over to the image’s own entrypoint. After 300 seconds it exits so swarm restarts the task. - Hook for configuration. The image fills its config folder only when that folder is empty, so a config file written before startup breaks the first run. The entrypoint writes a
before-startinghook that the image runs after populating the folder.$$is compose’s escape for a literal$. - Cron service. The same image runs
/cron.shfor background jobs. It shares the four volumes withapp, so it lands on the same node and waits for the database in the same way. - Trusted domains and proxy. TLS ends at the edge.
OVERWRITEPROTOCOL=https,OVERWRITEHOSTandOVERWRITECLIURLmake Nextcloud write https addresses,TRUSTED_PROXIEScovers the private ranges that Catena’s proxy uses, andNEXTCLOUD_TRUSTED_DOMAINSis the public hostname. - Chunk size. Cloudflare caps a request body at 100 MB on its Free and Pro plans. Nextcloud’s default upload chunk is 100 MiB, which fails at the edge and is retried without end. 80 MiB (
83886080) leaves headroom. Setting it to 0 disables chunking and breaks every upload over 100 MB. - Access.
vps.auth.mode=publicleaves Nextcloud’s own login in charge, which keeps anonymous share links (/s/...) working for people with no account. Sign-in through Keycloak is added inside the app (below). - Health.
vps.health.path=/status.phpchecks the page that says Nextcloud itself is up, beyond its front page. - Backups. The
dbservice image containspostgresand hasPOSTGRES_USERset, so it is dumped before each backup.
Sign-in with OIDC
Section titled “Sign-in with OIDC”In the catalog app, Catena makes the client in Keycloak and registers the provider inside Nextcloud itself.
For the hand-written file, the two vps.auth.oidc labels on app make Catena create the sign-in entry and write OIDC_CLIENT_ID, OIDC_CLIENT_SECRET and OIDC_ISSUER_URL into the app’s settings (see Sign in with Catena accounts). The labels’ return address is Nextcloud’s callback, https://nextcloud.yourdomain.com/apps/user_oidc/code. Once the app’s tile shows sign-in ready, you read the three values in the .env file of the app’s folder (see Restore one app on any machine), and register the provider inside Nextcloud with occ, run in the app container from the server’s shell, with the id and secret in place of the two placeholders:
CT=$(docker ps -q -f label=com.docker.swarm.service.name=my-nextcloud_app | head -1)
docker exec --user 33 "$CT" php /var/www/html/occ app:enable user_oidc
docker exec --user 33 "$CT" php /var/www/html/occ user_oidc:provider keycloak \ --no-interaction \ --clientid="<client-id>" \ --clientsecret="<client-secret>" \ --discoveryuri="https://auth.yourdomain.com/realms/vps/.well-known/openid-configuration" \ --scope="openid email profile groups" \ --mapping-uid=preferred_username \ --mapping-display-name=name \ --mapping-email=email \ --mapping-groups=groups \ --group-provisioning=1The command creates the provider or updates it, so you can repeat it. Nextcloud’s login page then shows Log in with keycloak. The local admin account keeps working as a fallback.
Check an app with the panel
Section titled “Check an app with the panel”The App check tab reads a compose file the way Docker Swarm and your server will, before you deploy it. Only administrators see the tab; staff do not.
- Open App check.
- In App name, enter the name you will give the app (step 1). Its own address and ports then do not count as taken by another app.
- Paste the compose file in Compose file and select Check.
- Read the findings. Each one carries its level (error, warning or note), its rule, the service it concerns and what to change. What this means opens the rule in the lists below.
- When automatic corrections apply, the page shows the Corrected file and the changes from your file. Copy it into the app’s file in your repository and publish a new version. A finding marked corrected below is already fixed in that file; any other is a choice you make in your file.
Nothing is deployed from this tab. The check compares your file with what your server holds: its other apps’ addresses and ports, the domains it serves, the groups you sign in with and the folders added to the backup. A part the server cannot read is named under the counts, and the checks that depend on it are left out.
Your server also checks the file of each app on the Apps page every hour and after each deploy. On the Apps tab, an administrator sees on a tile with at least one error or warning a link such as App check: errors 1, warnings 2. Notes are not counted. The link opens App check with that app’s findings. The server does not show the deployed file: paste the app’s compose file to see the corrected file. The tile has no link when nothing was found.
A finding is an error when the deploy fails, data is lost, the app cannot be reached or it gets control of the server. It is a warning when a setting does not do what the file says. A note is worth knowing.
What Docker Swarm refuses or ignores
Section titled “What Docker Swarm refuses or ignores”What Catena reads from the file
Section titled “What Catena reads from the file”| Rule | Level | What it reports |
|---|---|---|
| K1 | error | A service with an address is not on catena-network, or the file does not declare that network with external: true: the proxy cannot reach it. |
| K2 | error | A service that stores data in a volume is not pinned to the data node. Applies from two nodes. |
| K3 | error | Several addresses and not exactly one vps.route.main=true, or two services with the same address. |
| K4 | warning | An app-wide label on a service other than the main address, or an address label on a service with no address: it is ignored. |
| K5 | warning | A vps.* label under deploy.labels, where Catena does not read it. |
| K6 | note | A public address with no vps.health.path. |
| K7 | warning | An access label that has no effect: an unknown vps.auth.mode, vps.auth.groups on a public or admin-only address, visitor beside other groups, or a group that does not exist in Keycloak. |
| K8 | note | An image tag with no full version, or a vps.auto-update value that is not valid. |
| K9 | error | A bind mount outside the apps area that is not listed under Other folders to back up: the nightly backup fails. |
| K10 | warning | The file matches a catalog app but is deployed under another name: catalog actions and restores do not find it. |
| K11 | note | A vps.* label Catena does not read, such as one that names the app or the service. |
| K12 | warning | A published port that no vps.expose.tcp or vps.expose.udp label declares (open to the internet), or a declared port the service does not publish with mode: host. A catalog app granted the host network publishes nothing and draws no warning for its declared ports. |
| K13 | note | A database that is not exported before each backup: its volume is copied while it runs. |
| K14 | error | An address another app already holds. |
| K15 | error | An address that is not under a domain your server serves. |
| K16 | error | A port that another app already opens on your server. |
| K17 | error, warning | An x-catena block that does not read or does not hold for its file (see Declare the backup mode and update steps): the server refuses the app’s updates. A warning lists the commands the block has the server run in the app’s containers. |
What reaches the server
Section titled “What reaches the server”| Rule | Level | What it reports |
|---|---|---|
| X1 | error | privileged: true. |
| X2 | error | The server’s own network (network_mode: host, or the external host network). A file deployed unchanged from Catena’s catalog whose block declares it passes. |
| X3 | error | pid: host, which shows every process of the server. |
| X4 | error | ipc: host, which reaches the shared memory of other processes. |
| X5 | error | devices, which reaches the server’s hardware. |
| X6 | error | The Docker socket mounted in the service, which controls every container. A file deployed unchanged from Catena’s catalog whose block declares it passes. |
| X7 | warning | cap_add: added capabilities. Keep each only if the app needs it. |
| X8 | warning | A server folder mounted outside the apps area: the app reads the server’s files. |
| X9 | warning | A port published on the server, reachable from the internet without signing in. |
| X10 | error | Traefik labels, which route nothing. Use vps.route.host and vps.auth.mode or vps.auth.groups. |
| X11 | warning | A service on catena-network that keeps raw network access, with which it could forge traffic to Catena’s own services. The corrected file adds cap_drop: [NET_RAW]; an app that needs the access adds cap_add: [NET_RAW] (X7 then reports it). |
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause and fix |
|---|---|
| 502 or 503 on the address | The proxy cannot reach the app. The front-door service is on catena-network, and vps.route.port is the port it listens on. Read the logs of the service with docker service logs <app>_<service> on the server. |
| 403 after signing in | The person is in none of the allowed groups. The group in vps.auth.groups exists in Keycloak, matches exactly (letter case included), and the person is a member (People or Keycloak). |
| Only administrators get in | Neither vps.auth.mode nor vps.auth.groups is set, or the mode is not public, private or admin-only and no groups are listed. |
| No route, or the wrong address or access | No vps.route.host, several addresses with none or two marked vps.route.main=true, or a front-door service that is not on catena-network. An app-wide label on a service other than the main address’s is ignored. The route sync runs seconds after a deploy and every hour. |
| One address does not answer | Another address on this server already uses its host name, or its service is not on catena-network. The tile tells an administrator which address and why. Give the address a host name of its own, or join its service to catena-network, then publish a new version and move the app to it. |
| Red card on the status page | A public app whose front page answers an error, an app behind sign-in that does not send visitors to sign in, a vps.health.path that does not answer 200, or a vps.health.expect text that is absent from the reply. |
| The tile says sign-in failed or sign-in waiting | Read the reason on the tile. A return address in vps.auth.oidc.redirect_uris must be https:// on a domain this server serves, with no * and no #. Fix the label in your repository, publish a new version and move the app to it. |
| Backup failed on a bind mount | The mount is outside the apps area. Move the data to a named volume, or list the folder under Settings > Backup repository > Other folders to back up (separated by commas). |
| Backup failed on a database | The dump failed. A MariaDB or MySQL container needs MARIADB_ROOT_PASSWORD or MYSQL_ROOT_PASSWORD; a Postgres container needs a valid POSTGRES_USER. |
| Update skipped | The image tag is not a full version (latest, 1.2), vps.auto-update is off, or the move exceeds the label (a major release under patch). The newest release may also be held back by the vulnerability or waiting-period checks, or the app is held because it was changed outside Catena (see Apps changed outside Catena). |
| Adding an app is refused | The name is already a folder or an app on the server, the version is not a tag of that app in the repository, the repository is not on the list, or the compose file has an error in the check (see Check an app with the panel). |
| Service restarts in a loop at first deploy | Nothing waits for the database. The service’s entrypoint waits for it, as in the Nextcloud example. |
| Data missing after a node change | A service with a volume lacked node.labels.catena.role==data and started on another node with an empty volume. |