Skip to main content
A secret set is a named bundle of key/value pairs — one app’s .env. Sets come in two kinds, and a set is strictly one or the other. The boundary is enforced by the database, not by convention. Two triggers hold it: no_public_set_binding rejects binding a public set to a host, and reject_bound_set_public rejects making a bound set public. The API returns 409 with unbind all hosts before making this set public if you try the second one. So a deployable secret can never be read out of the UI, and a shared dev secret can never be shipped to a machine. Pick the kind when you create the set; flipping it later means unbinding every host first. The rest of this page is about private sets — the delivery path. The daemon holds a set in memory only, never on disk, and hands it to a local process through custosd exec.

Create a set

Creating a set needs the global set.add permission. Pass "public": true for a team-shared set; the default is private and therefore deployable. Update entries later with PUT /sets/{id}/entries for the whole map, or PUT /sets/{id}/entries/{key} for one value.

Bind it to a host

Binding exposes the set’s secrets to the host, so it requires authority over both ends: set.read on the set and host.access on the host. as_user is optional and restricts which Unix user may consume the set. DELETE /hosts/{id}/sets/{setId} unbinds it. The bind pushes a freshly sealed bundle to the host immediately if its daemon is connected, and on reconnect otherwise.

Launch the app

The important pattern is always:
custosd exec fetches the bound machine-secret set from the local daemon, injects those values into the child process environment, then replaces itself with APP_RUN_CMD.

Socket access

Run custosd exec as a user that belongs to the custos Unix group. This applies to interactive shell users and to service users used by systemd, PM2, Docker, or other process managers:
Group changes take effect in a new login session. Log out and back in before testing an interactive user; restart a service after changing its service user’s groups. The daemon creates /run/custos/secrets.sock with mode 0660 and ownership custos:custos. 0660 means the owning user and group may read from and write to the socket (6 = 4 read + 2 write), while all other users have no access (0). Root can bypass this filesystem restriction. An as_user binding is an additional restriction checked after the caller connects. Omitting as_user allows any caller with access to the socket; it does not grant socket access to users outside the custos group.

Docker

Use this when the app runs in a container, but custosd runs on the host.
custosd exec injects the set into the docker run process. Docker then copies each named variable from the Docker client’s environment into the container because the flags are passed as -e NAME instead of -e NAME=value. Writing one -e NAME per variable does the same thing, but a set with a hundred variables needs a hundred flags. {{custos.expand:TEMPLATE}} is a placeholder that custosd exec replaces, before starting the command, with TEMPLATE repeated once per name in the set, sorted. The example above expands to docker run --rm -e TEST_1 -e TEST_2 -e TEST_3 IMAGE_NAME. Only names are expanded, never values, so secrets stay out of the command line where ps would show them.

Expansion templates

A template is split on spaces into arguments. Write {} where the name belongs; if the template has no {}, the name is added as a final argument. That covers the shapes command line tools use: So the same placeholder works for anything that reads a named variable from the environment of the command that starts it, such as a Docker build:
A space in a template separates one argument from the next, so no single argument can contain a space. There is no quoting or escaping inside a template to work around this. Quote the whole placeholder whenever the template contains a space, as in the --pass env {} row above:
Without the quotes the shell splits the placeholder into separate arguments before custosd reads it, and the command fails. Quotes are also what let a template contain a comma. A single-word template such as {{custos.expand:-e}} needs no quotes. A command with no placeholder runs exactly as before. If the binding uses as_user, run the command as that exact Unix user.

PM2

Use this when PM2 starts the app process directly on the host.
The environment values are injected into the pm2 command. PM2 then records that environment for the started process. If a process is already running, restart it through custosd exec so PM2 receives the fresh values:
If the binding uses as_user, run the command as that exact Unix user.

systemd

Use this when systemd owns the app lifecycle.
As described under Socket access, add the service user to the custos group and restart the service after its group membership is active:
If the binding uses as_user, set User= to that exact Unix user.