docker push / check
Purpose
Section titled “Purpose”Apply a Docker service definition to the host, or dry-run it first. A service definition is
a ZIP containing a dc-docker.yml; the agent renders it into a daemontools
service that runs your container.
docker/pushapplies the definition — it writes the service files to the host.docker/checkis a dry-run/diff — it computes what would change without writing anything.
The same engine backs the dc-operator console.
Agent config (config/dc-docker.json)
Section titled “Agent config (config/dc-docker.json)”Both endpoints use a single, fixed config file — config/dc-docker.json — regardless of the
service name in the URL. It carries the apiKeys accepted for all docker push/check calls:
{ "type": "DOCKER", "apiKeys": { "REPLACE_WITH_A_LONG_RANDOM_SECRET": "gitlab-ci" }}type: DOCKERapiKeys: REPLACE_WITH_A_LONG_RANDOM_SECRET: gitlab-ciBuild a ZIP whose root contains dc-docker.yml (plus any config files it references), then
dry-run before applying:
curl \ --data-binary @myservice.zip \ --fail \ -H "api-key: $DOCKER_KEY" \ https://dc-agent.example.com/dc-agent/docker/check/otelcol-1curl \ --data-binary @myservice.zip \ --fail \ -H "api-key: $DOCKER_KEY" \ https://dc-agent.example.com/dc-agent/docker/push/otelcol-1POST /dc-agent/docker/push/{name} and POST /dc-agent/docker/check/{name} — {name} is the
service name; the body is the raw ZIP. Both return a text/plain log.
The dc-docker.yml service definition
Section titled “The dc-docker.yml service definition”dc-docker.yml describes one container as a supervised service. It must sit at the root of the
ZIP, next to any config files/directories referenced by dirConfig / fileConfig /
templateFileConfig volumes.
Complete example
Section titled “Complete example”version: 0.0.1 # metadata only — not used by the enginename: otelcol-1 # daemontools service name and container --name
boundVariables: # values usable as {{ ... }} anywhere in this file - name: DOCKER_TAG value: 260602-1157
env: # container environment variables - { name: UPTRACE_DSN, type: ENV_DIR } # value injected at runtime (see below)
- { name: LANG, value: "en_US.UTF-8" } - { name: LC_ALL, value: "en_US.UTF-8" } - { name: TZ, value: "Europe/Moscow" }
image: name: "registry.static/alma9-otelcol:{{ DOCKER_TAG }}"
directories: sourceBaseDir: /opt/otelcol-1 # host base for relative volume sources destinationBaseDir: /opt/otelcol-1 # container base for relative destinations containerWorkingDir: /opt/otelcol-1 # docker run -w
volumes: - dirConfig: # copy the ZIP's ./config dir to the host, then mount it destination: ./config
- directoryOrCreate: # create the dir on the host if missing, then mount it destination: /opt/otelcol-1/var
- linkToHostFile: { destination: /etc/localtime , readonly: true } # bind-mount a host file - linkToHostFile: { destination: /etc/hosts , readonly: true }
args: # container command + arguments - --config - /opt/otelcol-1/config/otelcol.yamlHow it is processed
Section titled “How it is processed”-
The agent unpacks the ZIP to a temporary directory.
-
It reads
boundVariables(andboundVariablesMap), then runs Handlebars over the whole file text, replacing every{{ NAME }}with the variable’s value, and re-parses the result. Only bound variables feed the template —env,directories, etc. are not variables. The re-parse is strict: a key the format does not have, or a key repeated on one level, fails the push and the check with every problem listed (see Gotchas). -
It checks the config before touching the disk:
securityContext,owner/modeof volumes and — only when some volume declaresownerormode— the write-path preflight. Any error stops the push with nothing changed. -
It resolves volumes on disk (copies config from the ZIP, creates dirs, sets directory owner/mode, downloads fetch-URLs), merges
env/envMap, and defaults the owner. -
It writes a daemontools service directory under the agent’s services-definition dir (
SERVICES_DEFINITION_DIR, default/etc/service.d):/etc/service.d/otelcol-1/├── run # the docker run wrapper (below)├── env/ # empty — drop ENV_DIR secret files here└── log/run # multilog logger
The generated run script for the example above is exactly:
#!/usr/bin/env bash
exec 2>&1
docker rm otelcol-1
exec \ /usr/bin/envdir /etc/service.d/otelcol-1/env \ docker run \ --rm \ --net=host \ --log-driver none \ --name=otelcol-1 \ -e UPTRACE_DSN \ -e LANG="en_US.UTF-8" \ -e LC_ALL="en_US.UTF-8" \ -e TZ="Europe/Moscow" \ -v /opt/otelcol-1/config:"/opt/otelcol-1/config" \ -v /opt/otelcol-1/var:"/opt/otelcol-1/var" \ -v /etc/localtime:"/etc/localtime":ro \ -v /etc/hosts:"/etc/hosts":ro \ -w /opt/otelcol-1 \ registry.static/alma9-otelcol:260602-1157 \ --config /opt/otelcol-1/config/otelcol.yamlThe fixed flags (--rm, --net=host, --log-driver none) are always present; --user,
--read-only, --security-opt no-new-privileges, --privileged and --cap-add/--cap-drop come
from securityContext and follow --name. There is no --env-file (env is passed as individual -e flags wrapped in daemontools envdir). The
log/run script is exec setuidgid <owner.user> multilog s5242880 /var/log/otelcol-1.
Top-level fields
Section titled “Top-level fields”| Field | Type | Required | Purpose |
|---|---|---|---|
name |
string | yes | daemontools service name; container --name; log dir under /var/log. |
image |
{ name } |
yes | Full image reference including tag. |
version |
string | no | Metadata only — not used by the engine. |
boundVariables |
list of { name, value } |
no | Handlebars template variables. |
boundVariablesMap |
map NAME: value |
no | Same, as a map; merged into boundVariables. |
env |
list of { name, value, type } |
no | Container environment variables. |
envMap |
map NAME: value |
no | Env vars as a map (always inline VALUE). |
volumes |
list (tagged, see below) | no | Bind mounts and config files. |
args |
list of strings | no | Container command and arguments. |
directories |
object | no* | Base dirs for relative volume paths + working dir. |
owner |
{ user, group } |
no | Host user the multilog log runner runs as; defaults root/root. Not the container user, and not applied to the service files. |
securityContext |
object | no | Container user (runAsUser/runAsGroup), read-only root, no-new-privileges, privileged, Linux capabilities. |
* Required if any volume uses a relative source or destination.
boundVariables & templating
Section titled “boundVariables & templating”Every {{ NAME }} in the file is replaced before parsing. Declare the values either as a list or
a map (both are merged):
boundVariables: - name: DOCKER_TAG value: 260602-1157boundVariablesMap: APP_VERSION: 1.0-3Then reference them anywhere a string appears, e.g. image.name, a fileFetchUrl.url, or an
args entry: "registry/app:{{ DOCKER_TAG }}".
env & env types
Section titled “env & env types”Each env entry has name, and either an inline value or a type:
type |
Emitted in run |
Where the value comes from |
|---|---|---|
VALUE (or omitted) |
-e NAME="value" |
the value field, baked into the script |
ENV_DIR |
-e NAME |
at runtime from env/<NAME>, via daemontools envdir (value is ignored) |
image.name is the only field — the full reference including registry and tag, e.g.
registry.static/alma9-otelcol:{{ DOCKER_TAG }}. There are no separate registry/tag fields.
A YAML list of strings appended after the image as the container command. Each element is passed as-is (elements containing a space are quoted).
directories
Section titled “directories”| Field | Meaning |
|---|---|
sourceBaseDir |
Host base directory for relative volume source paths (the left side of -v). Created up front. |
destinationBaseDir |
Container base directory for relative volume destination paths (the right side of -v). |
containerWorkingDir |
Becomes docker run -w <dir>. |
Path resolution for each volume:
destination— absolute if it starts with/; otherwisedestinationBaseDir+ path (a leading./is stripped).source— if omitted, derived fromdestination(same as destination when it is absolute, otherwisesourceBaseDir+ destination); if given, absolute when it starts with/, otherwisesourceBaseDir+ source.configPath(config volumes) — resolved inside the unpacked ZIP; if omitted, the basename of the resolved source is used.
If a volume uses any relative path, directories (with the matching base dir) must be present, or
the push fails.
volumes
Section titled “volumes”volumes is a list; each item is exactly one of the tagged variants below. Every variant
supports source, destination, and readonly (boolean, default false → adds :ro to the
mount). The extra fields and behavior:
| Variant | Extra fields | What it does on the host |
|---|---|---|
dirConfig |
configPath |
Copies a directory from the ZIP to the host source, then mounts it. |
fileConfig |
configPath |
Copies a file from the ZIP to the host source (errors if missing), then mounts it. |
templateFileConfig |
configPath |
Like fileConfig, but Handlebars-renders the file with the bound variables while copying. |
fileFetchUrl |
url, version, signatureType |
HTTP GET url → host source (skipped if already present and non-empty; requires HTTP 200), then mounts it. |
directoryOrCreate |
owner, mode |
Creates the host source directory if missing (empty), optionally sets its owner/permissions, then mounts it. |
linkToHostDirectory |
— | Bind-mounts an existing host directory (no copy, no create). |
linkToHostFile |
— | Bind-mounts an existing host file, e.g. /etc/localtime. If destination is omitted, it mounts at the same path. |
Examples:
volumes: # ship the config/ folder from the ZIP and mount it read-only - dirConfig: configPath: ./config source: /opt/otelcol-1/config destination: /opt/otelcol-1/config readonly: true
# download a jar once and mount it - fileFetchUrl: url: https://example.com/releases/app-{{ APP_VERSION }}.jar source: /opt/app/app.jar destination: /opt/app/app.jar
# persistent data directory, created if missing - directoryOrCreate: destination: /opt/otelcol-1/var
# share a host file with the container - linkToHostFile: { destination: /etc/localtime, readonly: true }Directory owner and mode
Section titled “Directory owner and mode”A container that runs as a non-root user (see runAsUser) cannot write to a
directory the agent created as root. directoryOrCreate can hand its host directory over:
securityContext: runAsUser: 1001 runAsGroup: 1001
volumes: - directoryOrCreate: destination: ./state owner: runAs # = runAsUser:runAsGroup - directoryOrCreate: destination: ./shared owner: { user: 0, group: runAsGroup } # a number or the name of a securityContext field mode: "0770"owneris either the shorthandrunAsor{ user, group }; each field is a numeric id (0..2147483647) orrunAsUser/runAsGroup. A missing field is left unchanged. A reference to asecurityContextfield that is not set is an error — soowner: runAsneeds bothrunAsUserandrunAsGroup. Names are rejected: there may be no such user on the host.modeis three octal digits with an optional leading zero ("770","0770"); setuid, setgid and sticky bits are not supported.- Only
directoryOrCreatesupportsowner/mode— on any other variant (and on a list item that merges two variants, e.g. through a missing-) they are an error. On areadonlyvolume they are still applied on the host, with a warning:readonlyonly stops the container from writing, and a new owner/mode can take away the container user’s read access. - Only the directory itself changes, never recursively. Existing content keeps its owner: to
move an existing volume to a new user, stop the service and run
chown -Rby hand once. - Changing the owner needs the agent to run as root. Otherwise the push fails with
changing the owner needs the agent to run as root; directories created before that point stay. checkshows the change without making it, e.g.🔑 Will change uid 0 → 1001, gid 0 → 1001, mode 0755 → 0770 of /opt/app/state, or🔑 Will set …for a directory still to be created. The path is the physical one (symbolic links resolved) — the path the owner is applied to. Nothing is printed when owner and mode already match.
Write-path preflight. The agent runs as root and writes to many host paths; a directory handed
to a container must not let the container redirect those writes (for example by replacing a
subdirectory with a symbolic link to /etc). So when any volume declares owner or mode, the
whole config is checked before anything on disk changes, identically for check and push,
and every violation is reported at once:
- Volume and agent paths are absolute and contain no
..(or.in the config). - No other path the agent writes for this service — other volumes (each file of a
dirConfigcopy included),sourceBaseDir, the service directory and files, the log directory, the unpacked task — lies inside or passes through a directory withowner/mode, whether by path, by symbolic link or by bind mount; adirConfigtarget may not contain one; two volumes may not hand over the same directory; and a directory withowner/modemay not lie on its own path. - Every directory on the way to any of these paths belongs to root or the agent and is not
writable by group or others. A sticky directory such as
/tmpis accepted for an entry that belongs to root or the agent — but never as the direct parent of a directory withowner/mode. - A directory with
owner/modeis not a symbolic link (and not/).
Right before changing the owner the agent checks the chain once more, because directories created
in between get the agent’s umask. Configs without owner/mode are not checked at all and behave
exactly as before.
owner: user: otelcol group: otelcolDefaults to root/root. user is used for setuidgid <user> in the log/run script — the
host user multilog runs as. The service files (run, env/, log/run) are not chowned to it;
they belong to the user the agent runs as. The user the container runs as is
securityContext.runAsUser, and the owner of a volume directory is the volume’s
owner.
securityContext
Section titled “securityContext”securityContext: privileged: true capabilities: add: - IPC_LOCK drop: - NET_RAWprivileged: true emits --privileged; each capability becomes --cap-add/--cap-drop.
Running the container as a user, with a read-only root file system and without privilege escalation (the field names follow Kubernetes):
securityContext: runAsUser: 1001 # --user 1001:1001 runAsGroup: 1001 readOnlyRootFilesystem: true # --read-only allowPrivilegeEscalation: false # --security-opt no-new-privileges capabilities: drop: [ ALL ] --name=render-1 \ --user 1001:1001 \ --read-only \ --security-opt no-new-privileges \ --cap-drop ALL \| Field | Emits | Notes |
|---|---|---|
runAsUser |
--user U |
Numeric id 0..2147483647 only — docker --user name looks the name up in the image’s /etc/passwd, not on the host. |
runAsGroup |
--user U:G |
Requires runAsUser. |
readOnlyRootFilesystem |
--read-only when true |
|
allowPrivilegeEscalation |
--security-opt no-new-privileges when false |
--user Uwithout a group: the container gets the primary group ofUfrom the image’s/etc/passwd, or GID 0 when the image has no entry (e.g.scratch); supplementary groups come from the image’s/etc/group.--user U:G: exactly groupG, and the image’s supplementary groups ofUare not added — an application that read a directory through a supplementary group getsEACCES.- These fields are parsed strictly: ids accept digits only, the two flags accept only YAML booleans
(
true/True/TRUE/false/False/FALSE).readOnlyRootFilesystem: yesis an error naming the field, not a silentfalse. Values can come from bound variables, e.g.runAsUser: "{{ UID }}". runAsUsertogether withprivileged: trueis allowed but logged as a warning: the container still gets all capabilities and devices.- The volume directories the container writes to must belong to that user — see Directory owner and mode.
A user the image does not know: passwdEntry and tmpfs
Section titled “A user the image does not know: passwdEntry and tmpfs”When runAsUser is not in the image’s /etc/passwd (a registry UID rather than the base image’s
user), Java sees user.name=? and user.home=/ — read-only under --read-only, so
~/.java/.userPrefs, library caches and hsperfdata_<name> break. The image cannot add the user
itself (no shell, no useradd), so the agent does it at run time, the way imagekit does
(runtime.passwd_entry):
securityContext: runAsUser: 20500 runAsGroup: 20500 readOnlyRootFilesystem: true passwdEntry: "$USERNAME:*:$UID:$GID::/tmp:/sbin/nologin" tmpfs: [ "/tmp:rw,size=64m" ]| Field | Emits | Notes |
|---|---|---|
passwdEntry |
podman: --passwd-entry '<template>'; docker: -v <service dir>/container-passwd:/etc/passwd:ro |
Needs runAsUser and runAsGroup. Placeholders $USERNAME (= the UID for a numeric user, as podman does), $UID, $GID; 7 fields; home must be absolute. Only letters, digits, space and _ . , : * / + = @ - $ are accepted. |
tmpfs |
--tmpfs <path>[:<options>] per entry |
Absolute path without ./..; options like rw,size=64m (a-z 0-9 = ,). A path listed twice or also used as a volume destination is an error. |
What the container gets depends on the agent’s CONTAINER_RUNTIME (see
Installation):
- podman (default): podman’s own
--passwd-entry— a copy of the image’s/etc/passwdwith the line added, mounted separately (works with--read-only). If the UID is already in the image, the image’s line stays. - docker: docker has no such flag. The agent writes
container-passwdnext torun—rootplus the rendered line, never the host’s/etc/passwd— with mode0644and mounts it read-only over/etc/passwd. The image’s other lines are not visible in the container. The file is removed whenpasswdEntrygoes away or the runtime becomes podman.
With passwdEntry, DOCKER_CHECK and DOCKER_PUSH run docker --version first and fail if it
does not match CONTAINER_RUNTIME (--passwd-entry would stop a real Docker from starting the
container). A volume or tmpfs on /, /etc or /etc/passwd is refused: it would hide or
replace the generated /etc/passwd. Configs without these fields are not affected in either
mode.
Gotchas
Section titled “Gotchas”-
Enums are case-sensitive and must match exactly:
typeisVALUEorENV_DIR;signatureTypeisNONEorPGP. -
All scalars are read as strings, so
readonly: trueandvalue: "8051"both work — quote values that YAML might otherwise interpret (e.g. numbers with leading zeros). ThesecurityContextuser/flag fields and volumeownerare the exception: they are validated strictly and fail with the field path (e.g.volumes[0].directoryOrCreate.owner.user). -
Unknown and duplicate keys are errors. A typo would otherwise switch a setting off silently, so
DOCKER_CHECKandDOCKER_PUSHfail before anything changes and name every such key with its path and the closest known name:dc-docker.yml: unknown or duplicate keys, fix or remove them:- volumes[0].directoryOrCreate.readOnly: unknown key (did you mean 'readonly'?)- securityContext.readOnlyRootFilesysem: unknown key (did you mean 'readOnlyRootFilesystem'?)Keys inside
envMapandboundVariablesMapare free-form. Agents before this check ignored unknown keys: after upgrading, runDOCKER_CHECKover the fleet to find configs with stray keys. -
YAML anchors/aliases are not supported and will fail the parse.
-
namemust be a docker container name ([a-zA-Z0-9][a-zA-Z0-9_.-]*): it is also the daemontools service directory.nameandimageare effectively required (the engine dereferences them unconditionally);versionis ignored.
Result
Section titled “Result”pushwrites/updates the service directory (<name>/run,<name>/env/,<name>/log/run) underSERVICES_DEFINITION_DIRand returns atext/plainlog. Writes are idempotent — unchanged files are left untouched.checkwrites nothing and returns the same log withWill create …/Will write …notices,🔑 Will change …/🔑 Will set …for directory owner/mode, and a unified diff of each file that would change. Note the ZIP is still unpacked andfileFetchUrlstill downloads (see the caution above).