Image-Warden can run as an on-demand job container while the registry remains a separate long-running service. This keeps scheduling, registry lifecycle, and release policy explicit.
The container deployment supports Docker, Podman Compose, and Podman Quadlet examples. It does not replace the native host install path.
One practical host layout:
/opt/image-warden/
compose.yaml
config/
image-warden.conf
secrets
registry/
config.yml
data/
registry/
state/
cache/
The Image-Warden container uses:
/config/image-warden config and secrets
/state/image-warden digests, candidate queue, events.log, lock
/cache scanner reports and scanner databases
The same state mount must be shared by all Image-Warden job containers. The global flock lock is stored in the mounted state directory, so parallel jobs using the same state mount contend correctly. Jobs using different state mounts do not protect each other.
From the repository root:
docker build -t localhost/image-warden:local .Podman:
podman build -t localhost/image-warden:local .The image is Debian-based and includes skopeo, jq, curl, flock, Trivy, and Grype. Scanner major versions are checked at build time. Grype is downloaded from a pinned release tarball, controlled by the GRYPE_VERSION build argument.
Copy the example and edit paths:
cp deploy/compose.yaml.example /opt/image-warden/compose.yaml
mkdir -p /opt/image-warden/registry
cp deploy/registry/config.yml /opt/image-warden/registry/config.ymlIf the compose file is no longer inside the repository deploy/ directory, set the build context to the directory containing the Dockerfile:
export IMAGE_WARDEN_BUILD_CONTEXT=/path/to/image-wardenUse this registry address in the config when Image-Warden runs inside the compose network:
LOCAL_REGISTRY="registry:5000"
LOCAL_REGISTRY_TLS_VERIFY=false
REGISTRY_CONTAINER_NAME="staging-registry"
CONTAINER_RUNTIME="docker"For Podman Compose, change CONTAINER_RUNTIME="docker" to CONTAINER_RUNTIME="podman".
The compose example expects this host layout beside the compose file:
config/image-warden.conf
config/secrets
registry/config.yml
data/registry/
data/state/
data/cache/
Start the registry:
docker compose -f /opt/image-warden/compose.yaml up -d registryRun jobs on demand:
docker compose -f /opt/image-warden/compose.yaml run --rm image-warden iw-stage
docker compose -f /opt/image-warden/compose.yaml run --rm image-warden iw-release
docker compose -f /opt/image-warden/compose.yaml run --rm image-warden iw-cleanup
docker compose -f /opt/image-warden/compose.yaml run --rm image-warden iw-history --image nginxPodman Compose uses the same file:
podman compose -f /opt/image-warden/compose.yaml up -d registry
podman compose -f /opt/image-warden/compose.yaml run --rm image-warden iw-stageScheduling is kept outside of the container on purpose. Use host cron or systemd timers that call docker compose run --rm ... or podman compose run --rm ....
If IMAGE_RELEASE_HOOK is configured, the path must exist inside the Image-Warden container. Mount the hook script into the container and point the config at that container path. The hook runs once per promoted image. Hook failures are recorded in events.log but do not undo the already-promoted registry tag.
Podman users can use the examples in deploy/podman/ instead of Compose.
Build the image:
podman build -t localhost/image-warden:local .Copy and edit the job files:
mkdir -p ~/.config/containers/systemd
cp deploy/podman/image-warden-*.container ~/.config/containers/systemd/
cp deploy/podman/image-warden-*.timer ~/.config/containers/systemd/Then reload and enable timers:
systemctl --user daemon-reload
systemctl --user enable --now image-warden-stage.timer
systemctl --user enable --now image-warden-release.timer
systemctl --user enable --now image-warden-cleanup.timerThe existing systemd/staging-registry.container remains the registry Quadlet example. Use the same registry name in LOCAL_REGISTRY that the Image-Warden job containers can reach.
The job examples use Network=host for the simple localhost registry case. If you use a dedicated Podman network, replace that with the shared network used by the registry container.
Registry names are namespace-sensitive.
Inside Compose, registry:5000 usually resolves to the registry service. On the host, consumers often pull from 127.0.0.1:5000. These are not the same reference.
Image-Warden promotes whatever LOCAL_REGISTRY names. Make sure consumers can pull that same reference, or publish/retag accordingly.
For rootless Podman, localhost:5000 inside a container is the container's own network namespace. Use a Podman network alias, host.containers.internal, or host networking when appropriate.
Trivy and Grype download vulnerability databases. If /cache is not persistent, every --rm job container may download fresh databases.
The example sets:
XDG_CACHE_HOME=/cache
TRIVY_CACHE_DIR=/cache/trivy
GRYPE_DB_CACHE_DIR=/cache/grype/dbMount /cache read-write and include it in backup/restore decisions if you want fast warm starts. It does not need the same retention guarantees as state or registry data.
iw-cleanup deletes old timestamp tags through the registry HTTP API and then runs registry garbage collection with:
${CONTAINER_RUNTIME} exec ${REGISTRY_CONTAINER_NAME} /bin/registry garbage-collect /etc/distribution/config.yml --delete-untagged=falseInside an Image-Warden job container, GC only works if the selected container runtime client and socket are available. The first container deployment path does not hide that requirement.
Compose may prefix container names unless container_name is set. The example sets:
container_name: staging-registryso this config works:
REGISTRY_CONTAINER_NAME="staging-registry"For registry v3, the config path is /etc/distribution/config.yml. Older
registry v2 examples often used /etc/docker/registry/config.yml.
events.log lives in the mounted state directory. iw-history reads one JSONL file into memory, so rotate long-running logs.
Example:
/opt/image-warden/data/state/events.log {
monthly
rotate 24
compress
missingok
notifempty
copytruncate
}Compressed rotated logs are not searched automatically. Decompress older logs or pass an uncompressed file with:
iw-history --event-log /path/to/events.log.1Back up:
- registry data
- Image-Warden config and secrets
- Image-Warden state, especially candidates and
events.log
Cache is optional. It only affects scanner DB warm-up and retained reports.
After building:
docker run --rm localhost/image-warden:local iw-stage --help
docker run --rm localhost/image-warden:local iw-config --help
docker run --rm localhost/image-warden:local iw-history --helpPodman:
podman run --rm localhost/image-warden:local iw-stage --help
podman run --rm localhost/image-warden:local iw-config --help
podman run --rm localhost/image-warden:local iw-history --helpThese smoke tests only check that the packaged scripts start and can read their help output. Use the same pattern for other commands, for example iw-stage, iw-release, iw-cleanup, iw-scan, and iw-history.