.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
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
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
Runcustosd 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:
/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, butcustosd 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:
--pass env {} row
above:
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.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:
as_user, run the command as that exact Unix user.
systemd
Use this when systemd owns the app lifecycle.custos group and restart the service
after its group membership is active:
as_user, set User= to that exact Unix user.