Container image
Add shellz to a container image so every exec into the container is recorded, on any platform.
When you can't install software on the host, add shellz to your container image instead. shellz runs as PID 1 in front of your app, and records every shell opened in the container by kubectl exec, docker exec, ECS Exec or a dev-container terminal. Each container that starts joins a fleet using a fleet token, and shows up in the dashboard as a fleet member.
This page covers the image and the fleet token. The platform guides add the deployment details: Kubernetes, ECS, Fargate and Cloud Run, Dev containers.
1. Create a fleet
In the dashboard, open ~/servers, find Container fleets and click Add fleet. Give it a name (for example api-prod), choose the platform, and optionally set a maximum number of members.
shellz shows the fleet token once, starting with szf_, along with setup snippets for your platform. Copy the token and store it in your secret manager. Unlike a server join token, a fleet token is reusable: every container started with it joins the fleet.
2. Add shellz to the image
Add two lines to your Dockerfile:
RUN curl -fsSL https://get.shellz.dev/container | sh
ENTRYPOINT ["/usr/local/bin/shellz", "run", "--"]
CMD ["/app/server", "--port", "8080"] # your original entrypoint and argsThe install step runs as root, needs curl (or wget) and OpenSSL 3, and:
- downloads the shellz release for the image's architecture and verifies its Ed25519 signature and SHA-256 checksum;
- installs
/usr/local/bin/shellz; - runs
shellz image-install, which puts a small recording shim in front of the image's shells (/bin/sh,/bin/bash,ash,dash,zsh,fish) and keeps the real shells alongside.
It never enrolls and never needs a token at build time. It refuses to run outside an image build or container, and refuses if you pass it a token.
Then make shellz run -- the entrypoint, with your original entrypoint and arguments as the command. If your image already has an ENTRYPOINT, move it into CMD after shellz run --.
Without curl, sh or OpenSSL in the image
For minimal and distroless images, use BuildKit's checksum-verified ADD. The dashboard's setup snippets fill in the current version and checksum for each architecture; you can also read them from https://releases.shellz.dev/stable/linux-amd64.json and linux-arm64.json.
ADD --chmod=0755 --checksum=sha256:<sha256> https://releases.shellz.dev/agent/<version>/shellz-linux-amd64 /usr/local/bin/shellz
RUN ["/usr/local/bin/shellz", "image-install"]
ENTRYPOINT ["/usr/local/bin/shellz", "run", "--"]
CMD ["/app/server", "--port", "8080"]Images with no shell at all (distroless) have nothing for the shim to wrap. shellz still runs as PID 1 and watches for exec sessions, as described in How sessions are caught.
3. Pass the fleet token at runtime
Each container reads the token when it starts, from either:
- the file
/run/secrets/shellz-fleet-token(recommended), or - the environment variable
SHELLZ_FLEET_TOKEN. If both are set, the variable wins.
Prefer the file. An environment variable set on the container is visible to anyone who can exec into it, for example with docker exec <container> env. shellz removes the variable from your app's environment and from recorded sessions, but not from other execs.
With plain Docker:
printf '%s' 'szf_<fleet token>' > shellz-fleet-token && chmod 0444 shellz-fleet-token
docker run -d --stop-timeout 30 \
-v "$PWD/shellz-fleet-token:/run/secrets/shellz-fleet-token:ro" \
ghcr.io/acme/app:1.44. Check it's recording
Start a container and open a shell in it:
docker exec -it <container> bashThe fleet page lists the container as a member with the status recording, and the session appears in the dashboard.
Stopping gracefully
When the container is stopped, shellz forwards the signal to your app, stops accepting new sessions, waits for your app to exit, and uploads the rest of the recordings before exiting. This drain takes up to 20 seconds (SHELLZ_DRAIN_SECONDS).
Give containers a stop timeout of at least 30 seconds so the drain isn't cut short: --stop-timeout 30 for docker run, stop_grace_period: 30s in Compose. Docker's default is 10 seconds. Kubernetes' default of 30 seconds is fine.
How shellz runs your app
As PID 1, shellz run -- behaves like a minimal init:
- It starts your command, forwards
SIGTERM,SIGINT,SIGHUP,SIGQUIT,SIGUSR1,SIGUSR2andSIGWINCHto it, reaps zombie processes, and exits with your app's exit code. - Your app's own shell use (
sh -cfrom scripts, process managers and so on) is never recorded and adds about 1 ms per shell started. - If the token is missing or rejected, or shellz can't reach the API, your app still starts and runs. Interactive sessions are refused until the container is enrolled, and shellz retries in the background.
If something else is already PID 1, such as docker run --init or tini, shellz runs as its child and still works.
How sessions are caught
shellz uses two layers inside the container:
- The shell shim. When a shell starts, the shim checks whether it was started by an exec into the container rather than by your app. Exec sessions go to the recorder; everything else goes straight to the real shell.
- The exec watcher. Every 500 ms, PID 1 looks for processes that came from an exec and aren't being recorded, for example someone running the moved real shell or another interpreter directly. Under fail-closed, an unrecorded session with a terminal is killed and raises
unrecorded-container-exec. Otherwise it's reported.
Commands without a terminal
Health checks and probes (kubectl exec pod -- cat /tmp/ready, sh -c 'test -f /ready') run without a terminal. shellz never blocks them, even when recording is unavailable, so a recording problem can't fail your probes or restart your pods:
- They're recorded as
commandsessions when recording is available. - When the same command produces the same output and exit status as a run in the last 10 minutes, shellz drops the duplicate recording and counts it. The dashboard shows one row with a
×countbadge instead of thousands of identical probe sessions. - If recording is unavailable, the command runs anyway, and shellz logs its command line, user, timing and exit status once it's back, with an
unrecorded-noninteractive-execwarning. - Very short non-shell commands (under 500 ms) may be missed by the watcher.
Fleet members
Each running container is a member of its fleet. The fleet page shows each member's status:
| Status | Meaning |
|---|---|
| recording | Enrolled and sending heartbeats. |
| waiting | Enrolled, waiting for the first heartbeat. |
| stopped | The container stopped cleanly and reported its exit. |
| lost | No heartbeat for 3 minutes, and no clean stop. Raises a member-lost warning. |
| tampered | The member has an open critical alert, or isn't accepting sessions. |
Members are named after the pod, task or container. On Kubernetes, the pod's namespace and node are shown too.
Managing the fleet token
From the fleet page, owners and admins can:
- Rotate token: issue a new token. The old one stops working for new containers straight away; running members keep recording. Update your secret and roll your deployment.
- Revoke: turn the fleet off. Running members stop accepting sessions and raise a
member-disabledwarning. - Delete fleet: revoke the fleet and remove it from the dashboard. Its recordings are kept.
Fleet tokens can't be used to read recordings; they only let a container join the fleet. A leaked token lets someone add members to the fleet, so rotate it if that happens. A maximum member count limits how many can be active at once; containers beyond it are refused and raise fleet-full.
Settings
See Container settings for every environment variable shellz run reads.