Skip to content

docker push / check

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/push applies the definition — it writes the service files to the host.
  • docker/check is a dry-run/diff — it computes what would change without writing anything.

The same engine backs the dc-operator console.

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:

config/dc-docker.json
{
"type": "DOCKER",
"apiKeys": {
"REPLACE_WITH_A_LONG_RANDOM_SECRET": "gitlab-ci"
}
}

Build a ZIP whose root contains dc-docker.yml (plus any config files it references), then dry-run before applying:

Check (dry-run)
curl \
--data-binary @myservice.zip \
--fail \
-H "api-key: $DOCKER_KEY" \
https://dc-agent.example.com/dc-agent/docker/check/otelcol-1
Push (apply)
curl \
--data-binary @myservice.zip \
--fail \
-H "api-key: $DOCKER_KEY" \
https://dc-agent.example.com/dc-agent/docker/push/otelcol-1

POST /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.

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.

dc-docker.yml
version: 0.0.1 # metadata only — not used by the engine
name: 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.yaml
  1. The agent unpacks the ZIP to a temporary directory.

  2. It reads boundVariables (and boundVariablesMap), 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).

  3. It checks the config before touching the disk: securityContext, owner/mode of volumes and — only when some volume declares owner or mode — the write-path preflight. Any error stops the push with nothing changed.

  4. 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.

  5. 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:

/etc/service.d/otelcol-1/run
#!/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.yaml

The 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.

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.

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-1157
boundVariablesMap:
APP_VERSION: 1.0-3

Then reference them anywhere a string appears, e.g. image.name, a fileFetchUrl.url, or an args entry: "registry/app:{{ DOCKER_TAG }}".

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).

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 /; otherwise destinationBaseDir + path (a leading ./ is stripped).
  • source — if omitted, derived from destination (same as destination when it is absolute, otherwise sourceBaseDir + destination); if given, absolute when it starts with /, otherwise sourceBaseDir + 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 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 }

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"
  • owner is either the shorthand runAs or { user, group }; each field is a numeric id (0..2147483647) or runAsUser / runAsGroup. A missing field is left unchanged. A reference to a securityContext field that is not set is an error — so owner: runAs needs both runAsUser and runAsGroup. Names are rejected: there may be no such user on the host.
  • mode is three octal digits with an optional leading zero ("770", "0770"); setuid, setgid and sticky bits are not supported.
  • Only directoryOrCreate supports owner/mode — on any other variant (and on a list item that merges two variants, e.g. through a missing -) they are an error. On a readonly volume they are still applied on the host, with a warning: readonly only 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 -R by 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.
  • check shows 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:

  1. Volume and agent paths are absolute and contain no .. (or . in the config).
  2. No other path the agent writes for this service — other volumes (each file of a dirConfig copy included), sourceBaseDir, the service directory and files, the log directory, the unpacked task — lies inside or passes through a directory with owner/mode, whether by path, by symbolic link or by bind mount; a dirConfig target may not contain one; two volumes may not hand over the same directory; and a directory with owner/mode may not lie on its own path.
  3. 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 /tmp is accepted for an entry that belongs to root or the agent — but never as the direct parent of a directory with owner/mode.
  4. A directory with owner/mode is 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: otelcol

Defaults 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:
privileged: true
capabilities:
add:
- IPC_LOCK
drop:
- NET_RAW

privileged: 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 ]
Terminal window
--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 U without a group: the container gets the primary group of U from 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 group G, and the image’s supplementary groups of U are not added — an application that read a directory through a supplementary group gets EACCES.
  • These fields are parsed strictly: ids accept digits only, the two flags accept only YAML booleans (true/True/TRUE/false/False/FALSE). readOnlyRootFilesystem: yes is an error naming the field, not a silent false. Values can come from bound variables, e.g. runAsUser: "{{ UID }}".
  • runAsUser together with privileged: true is 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/passwd with 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-passwd next to run — root plus the rendered line, never the host’s /etc/passwd — with mode 0644 and mounts it read-only over /etc/passwd. The image’s other lines are not visible in the container. The file is removed when passwdEntry goes 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.

  • Enums are case-sensitive and must match exactly: type is VALUE or ENV_DIR; signatureType is NONE or PGP.

  • All scalars are read as strings, so readonly: true and value: "8051" both work — quote values that YAML might otherwise interpret (e.g. numbers with leading zeros). The securityContext user/flag fields and volume owner are 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_CHECK and DOCKER_PUSH fail 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 envMap and boundVariablesMap are free-form. Agents before this check ignored unknown keys: after upgrading, run DOCKER_CHECK over the fleet to find configs with stray keys.

  • YAML anchors/aliases are not supported and will fail the parse.

  • name must be a docker container name ([a-zA-Z0-9][a-zA-Z0-9_.-]*): it is also the daemontools service directory. name and image are effectively required (the engine dereferences them unconditionally); version is ignored.

  • push writes/updates the service directory (<name>/run, <name>/env/, <name>/log/run) under SERVICES_DEFINITION_DIR and returns a text/plain log. Writes are idempotent — unchanged files are left untouched.
  • check writes nothing and returns the same log with Will 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 and fileFetchUrl still downloads (see the caution above).