docker-compose

Deploy the LinuxGuard agent with docker-compose — compose.yaml example, capabilities, host paths, restart policy, and PID 1 considerations.

This page covers running the LinuxGuard agent under docker-compose. The shape is single-node, suitable for a lab host, an edge appliance, a developer workstation, or any environment where a Kubernetes DaemonSet is overkill but a long-lived agent process is desired.

Important: docker-compose is the Docker engine's native orchestrator, not a Kubernetes-compatible one. The Kubernetes Pod Security Standard does not apply. The Docker-engine equivalent — capability gating, seccomp profiles, and AppArmor profiles — is documented in § Pod Security Standard compatibility below.

compose.yaml

The recommended compose file. Pin the image to an immutable vX.Y.Z tag (never :latest) and source the enrolment token from a .env file that lives outside source control.

services:
  linuxguard-agent:
    # Pin to an immutable tag; never :latest in production.
    image: packages.linuxguard.io/linuxguard-agent:v3.0.0
    container_name: linuxguard-agent
    # Restart unless explicitly stopped — pairs well with --tls-cache so
    # restarts reuse the cached cert chain rather than re-enrolling.
    restart: unless-stopped
    # PID 1 auto-detection: the agent IS the container's main process.
    # An init shim is NOT required — the agent's pid1 build tag installs
    # its own zombie-reaper. Do NOT set `init: true` on this service.
    init: false
    # Host PID so the agent observes all processes on the node.
    pid: host
    # Add the five capabilities the file-cap transition requires.
    cap_add:
      - BPF
      - PERFMON
      - DAC_READ_SEARCH
      - SYS_PTRACE
      - SETPCAP
    # Drop everything else; defense-in-depth against image updates that
    # might add new capabilities silently.
    cap_drop:
      - ALL
    # Default Docker seccomp profile blocks perf_event_open(2). Use
    # `unconfined` for UAT/dev; for production, supply a custom profile
    # that adds perf_event_open to the allowlist.
    security_opt:
      - seccomp=unconfined
      # For production, replace the above with:
      # - seccomp=/etc/docker/seccomp-linuxguard.json
      # - apparmor=linuxguard-agent
    # Read-only image filesystem.
    read_only: true
    # tmpfs for the TLS cert cache when --tls-cache is set.
    tmpfs:
      - /run/linuxguard:rw,mode=0700,size=10m
    # tracefs bind-mount (read-only) for eBPF probe attach.
    volumes:
      - /sys/kernel/tracing:/sys/kernel/tracing:ro
      # BPF FS for pinned maps (read-write).
      - /sys/fs/bpf:/sys/fs/bpf
      # host /proc for process introspection (read-only).
      - /proc:/host/proc:ro
    environment:
      # Source the token from a .env file that is NOT checked into source
      # control. The agent reads the env var once at startup and immediately
      # unsets it to scrub /proc/<pid>/environ.
      LINUXGUARD_ENROLL_TOKEN: ${LINUXGUARD_ENROLL_TOKEN}
      LINUXGUARD_TENANT_ID: ${LINUXGUARD_TENANT_ID}
      # In a non-Kubernetes deployment the workload-id MUST be supplied
      # explicitly (no Downward API exists). Use a stable host identifier
      # plus a stable UUID per deployment.
      LINUXGUARD_NODE_NAME: ${LINUXGUARD_NODE_NAME:-${HOSTNAME}}
      LINUXGUARD_POD_UID: ${LINUXGUARD_POD_UID}
    command: ["start", "--tls-cache"]
    # Resource limits — keep memory bounded; do not cap CPU (eBPF map
    # operations spike briefly under load and CPU throttling drops events).
    deploy:
      resources:
        limits:
          memory: 512M

The companion .env file (referenced by compose at startup):

Add .env to .gitignore. Generate a stable LINUXGUARD_POD_UID once per deployment with uuidgen and persist it in the .env so restart-induced workload-id changes do not produce phantom enrolments.

Bring the agent up:

Restart policies

docker-compose exposes three restart policies that matter for the LinuxGuard agent:

Policy
When to use

unless-stopped

Recommended default. The agent restarts on container exit (crash, OOM) but stays stopped after an explicit docker compose stop. Pairs naturally with --tls-cache because restart reuses the cached cert chain without re-enrolling.

always

The agent restarts on every exit, including after docker compose stop. Appropriate for fully-unattended edge appliances where the agent must come back up after host reboot regardless of operator intent.

on-failure

Restart only on non-zero exit code. NOT recommended — the agent exits 143 (SIGTERM) on graceful shutdown and 130 (SIGINT) on Ctrl-C, which on-failure correctly treats as failure restarts. But on-failure interferes with operator stop semantics (a docker compose stop sends SIGTERM; the policy sees exit 143 and restarts).

no

The agent never restarts. Use only for one-shot agents (e.g., CI runners that enrol, ship one telemetry snapshot, and exit). For one-shots, the Ephemeral mode docker run example is more idiomatic than docker-compose.

PID 1 considerations

The agent IS the container's main process — it runs as PID 1 inside the container. Two compose-specific behaviors follow from this:

Do NOT set init: true

docker-compose's init: true option injects a tini or docker-init shim as PID 1 to reap zombie children. The agent is built with the pid1 build tag, which installs its own PID-1 init shim with zombie-reaping. Adding tini on top produces two layers of init machinery; the agent's auto-detection of PID 1 (os.Getpid() == 1) reports 1 only when it's actually PID 1 — under init: true, the agent runs as PID 2 and PID-1 auto-detection silently fails, so ephemeral semantics do NOT activate.

If init: true is required for some reason (e.g., a multi-process container), set the LINUXGUARD_PID1_CHILD=1 env var so the agent treats itself as ephemeral despite not being PID 1. This is the same sentinel the agent's own init shim sets when re-execing; setting it from compose is acceptable when init: true is mandatory.

SIGTERM exit code

The agent re-raises caught signals as os.Exit(128 + signum):

  • SIGTERM (which docker compose stop sends) → exit code 143.

  • SIGINT (which Ctrl-C in docker compose up sends) → exit code 130.

docker wait reports these exit codes correctly. Operators who write health-check scripts that check docker inspect linuxguard-agent --format '{{ .State.ExitCode }}' must expect 143/130 on graceful shutdown, not 0 — this is the same behavior documented in the start CLI reference § Exit codes.

Volume mounts

The compose example mounts four paths. Each is justified and minimal:

Mount
Type
Direction
Justification

/sys/kernel/tracing:/sys/kernel/tracing:ro

bind

host → container, read-only

Required for link.Tracepoint() to resolve tracepoint IDs at probe-attach time. Without this, probe attach trips "neither debugfs nor tracefs are mounted" and the agent degrades to DEGRADED_PROBES_PARTIAL.

/sys/fs/bpf:/sys/fs/bpf

bind

host → container, read-write

Required when the in-process loader pins maps for cross-process sharing. Read-write because pinned-map creation requires write access.

/proc:/host/proc:ro

bind

host → container, read-only

Required for process introspection when pid: host is set. The agent walks the host's /proc for the workload-discovery pass on startup.

/run/linuxguard:rw,mode=0700,size=10m

tmpfs

container-only

TLS cert cache when --tls-cache is set. Tmpfs (not a hostPath) so cert material never reaches durable storage.

The image filesystem is read_only: true. The agent writes ONLY to the tmpfs and to /tmp (writable in the distroless base).

Environment variables

Source every value from outside the compose file itself. The compose file is a manifest you commit; the .env file is a manifest you do NOT commit. Together they reproduce the deployment without leaking secrets.

Variable
Source
Notes

LINUXGUARD_ENROLL_TOKEN

Secret store (Vault, AWS Secrets Manager, etc.) → shell → .env

The token. NEVER hardcode in compose.yaml.

LINUXGUARD_TENANT_ID

Same secret store as the token

Required for the TOTP enrolment path.

LINUXGUARD_NODE_NAME

$HOSTNAME of the host (typically)

Identifies the host; can be a stable name in your asset registry.

LINUXGUARD_POD_UID

Generated once per deployment with uuidgen

A stable UUID. Persist in .env so restart-induced workload-id changes do not produce phantom enrolments.

The compose file references the .env values via ${VAR} interpolation. Compose loads .env from the same directory as compose.yaml automatically.

Security context

docker-compose does not enforce Kubernetes-style Pod Security Standards. The Docker-engine equivalent is the combination of cap_add / cap_drop, security_opt, read_only, tmpfs, and pid. The compose example above sets all five:

Surface
Setting
Rationale

cap_add

[BPF, PERFMON, DAC_READ_SEARCH, SYS_PTRACE, SETPCAP]

The five capabilities the file-cap transition requires.

cap_drop

[ALL]

Defense-in-depth — drop everything not explicitly added.

security_opt

seccomp=unconfined (UAT/dev) OR a custom profile (production)

The default Docker seccomp profile blocks perf_event_open(2).

security_opt (production)

apparmor=linuxguard-agent

Install the AppArmor profile at host level first (see Distroless image reference § Security context).

read_only

true

The agent does not write to the image filesystem.

pid

host

Required for the node-level coverage shape (analogous to K8s hostPID).

tmpfs

/run/linuxguard

TLS cert cache; never persists to disk.

User: the distroless image runs as nonroot (UID 65532) by default. docker-compose has no user: override needed; the image's USER directive applies.

Host paths

Identical to the Kubernetes DaemonSet host-path requirements — the four paths above (tracefs, BPF FS, /proc, tmpfs at /run/linuxguard). See Kubernetes DaemonSet § Host paths for the canonical table including removability notes.

Security Note: All host-path mounts are read-only except /sys/fs/bpf (which requires write for pinned-map creation) and /run/linuxguard (which is a tmpfs). Do NOT mount the host root (/) — see the anti-patterns hub section.

Pod Security Standard compatibility

PSS is a Kubernetes admission-control concept; it does not apply to docker-compose. The Docker-engine equivalent — capability gating, seccomp, and AppArmor — is documented in § Security context above.

For deployments that need PSS classification (e.g., a multi-environment fleet where some hosts run docker-compose and others run Kubernetes), use the PSS classification from the Kubernetes DaemonSet § Pod Security Standard compatibility table as the closest analogue. The compose example above corresponds to PSS privileged (because pid: host and the added capabilities exceed the baseline allowlist).

RBAC

RBAC is a Kubernetes concept; it does not apply to docker-compose. The Docker-engine equivalent is the Docker daemon's access control: any local user in the docker group can run docker compose up against the daemon, and the daemon honors the container's security context as configured.

For deployments where Docker daemon access is restricted (e.g., a host where dockerd runs under a non-root user with userns-remap), the configuration is host-side, not compose-side. Verify that the user running docker compose has permission to map the required capabilities and bind-mount the host paths above.

Verification

After docker compose up -d, verify the agent is producing heartbeats:

A healthy agent produces a heartbeat log line within 20 seconds. A degraded probe report points to a missing prerequisite — most commonly kernel.perf_event_paranoid > 2 on the host, or a missing seccomp override. See Kubernetes DaemonSet § Prerequisites for the host-level fixes; they apply identically under docker-compose.

Troubleshooting

OCI runtime create failed: ... operation not permitted

The most common cause: the Docker daemon's default seccomp profile is blocking perf_event_open(2). Add security_opt: [seccomp=unconfined] for UAT/dev, or supply a custom profile for production.

Agent exits immediately with bootstrap ephemeral: ... none of the three were present

The Downward API env vars (LINUXGUARD_NODE_NAME, LINUXGUARD_POD_UID) are not populated, and --workload-id is not set. Verify the .env file is being loaded (docker compose config prints the resolved env block) and that both variables have non-empty values.

Agent logs 400 tenantId required for TOTP enrollment

LINUXGUARD_TENANT_ID is missing or empty. The TOTP path requires it; long-lived API-key enrollments do not. Verify the tenant ID is set in the .env file and that the compose environment: block references it.

init: true causes the agent to skip ephemeral semantics

docker-compose's init: true makes the agent run as PID 2, not PID 1. Either remove init: true (recommended — the agent has its own PID-1 init shim) or set LINUXGUARD_PID1_CHILD=1 to force ephemeral semantics.


Next Step: Podman →

Related: Container deployment hub | Ephemeral mode | Distroless image reference | Kubernetes DaemonSet | start CLI reference

Last updated

Was this helpful?