Docker
The published image (ghcr.io/sylvinus/exav) is a drop-in for the
ClamAV Docker image on
the wire: same clamd port 3310, same clamd protocol. It runs the
clamd-compatible daemon by default, so a host clamdscan (or any clamd client)
pointed at the container’s TCP 3310 just works.
It’s distroless and rootless: a static-musl binary on distroless/static
(no shell, no package manager) running as the nonroot user (uid 65532).
Bind-mounted database volumes must be writable by that uid.
Run it
Section titled “Run it”# Start the daemon; persist signatures in a named volume. Name a source, or# fill the volume yourself with `cvd`/`freshclam` first: signatures are not# bundled, and rather than serve without them exav waits for them to appear# (EXAV_STARTUP_WAIT_SECS, default 1800). Nothing answers on 3310 until they do.docker run -d -p 3310:3310 -v exav-db:/var/lib/exav \ -e EXAV_SIG_SOURCES=https://your-mirror/ ghcr.io/sylvinus/exavclamdscan --stream file.bin # once the load is logged
# One-shot scan. Paths win over the image's `EXAV_LISTEN`, so there is nothing# to unset — but the scan loads its own database, so it needs the signature# volume as well as the tree to scan.docker run --rm -v exav-db:/var/lib/exav -v "$PWD:/scan" ghcr.io/sylvinus/exav /scanNaming paths is what makes that run a scanner rather than a daemon: EXAV_LISTEN
is a default the image sets, and a default gives way to what the command line
asks for. A --listen typed on the command line alongside paths is a
different thing — two jobs in one command — and is refused rather than guessed
at.
The image’s default command is --auto-update, with EXAV_LISTEN set to
clamd://0.0.0.0:3310: serve the clamd protocol, and keep the signature volume
current — bootstrap it, wait for a sidecar to fill it if that is where signatures
come from, refresh the configured sources on a schedule, and hot-reload on every
change. Everything else comes from the environment, and any flag on the command
line overrides the matching variable, never the other way round:
# EXAV_WORKERS is overridden; every other variable still applies.docker run -d -p 3310:3310 -e EXAV_WORKERS=8 -v exav-db:/var/lib/exav \ ghcr.io/sylvinus/exav --auto-update --workers 2Health
Section titled “Health”The image carries a HEALTHCHECK that runs exav --ping: one exchange in
whatever protocol the listener speaks — PING on clamd, OPTIONS on ICAP — so
it goes red on a daemon that accepts connections and then answers nothing, which
a bare TCP probe would call healthy.
It reads the same EXAV_LISTEN the daemon did, so moving the port or serving
ICAP instead needs no change to the check. Run it yourself against any daemon:
docker exec exav /exav --ping # what this container servesexav --ping --connect clamd://scanner.internal:3310 # from anywhereA container is unhealthy while it waits for signatures — it is not serving
then, and reporting otherwise would send traffic to something that cannot
answer. If signatures arrive from a slow sidecar, raise the start period
(--health-start-period) so the orchestrator does not kill it mid-wait.
The check answers for the daemon this image defaults to. Give the container a
different job — building a database, a --connect client, a one-shot scan — and
it reports unhealthy the whole time while working perfectly, because nothing is
listening and the check cannot tell that from a daemon that died. Those runs are
short and exit on their own, so it is cosmetic under docker run; add
--no-healthcheck if something would act on it:
docker run --rm --no-healthcheck -e EXAV_LISTEN= \ -v "$PWD:/scan" ghcr.io/sylvinus/exav /scanThe signatures directory is /var/lib/exav (exav’s own). To reuse an existing
ClamAV volume, mount it there or set EXAV_SIG_DIR=/var/lib/clamav.
Four ways to populate signatures
Section titled “Four ways to populate signatures”Mount a /var/lib/exav you keep current yourself with cvd / freshclam.
No updater in the container.
Set EXAV_SIG_SOURCES to CVD mirror bases you trust (a trailing / makes a
URL a mirror, expanding to main/daily/bytecode.cvd) or to exact feed
URLs; they merge. The container fetches on start and every
EXAV_UPDATE_INTERVAL_SECS after that. Needs the http-update build (the
default image). It’s a plain HTTPS fetch with no signature verification —
trust the mirror.
A second container writes the shared volume and the scanner hot-reloads it. See the dual-container pattern below.
Set EXAV_DB_URL to an .exavdb built elsewhere; the container pulls it
(integrity-checked) and hot-reloads on change. Build once, serve many.
Environment variables
Section titled “Environment variables”Configure the container with exav’s own EXAV_* variables — the four that
matter for most deployments:
| Variable | Default | Meaning |
|---|---|---|
EXAV_SIG_DIR |
/var/lib/exav |
Signatures directory (loaded recursively). |
EXAV_SIG_SOURCES |
(unset) | Where to fetch signatures from: exact URLs, mirror bases (trailing /), or a file of either. Comma-separated for more than one. |
EXAV_DB_URL |
(unset) | URL of a prebuilt .exavdb to pull and serve; hot-reloads, and replaces the signature-file sources rather than merging with them. |
EXAV_LISTEN |
clamd://0.0.0.0:3310 |
What the container serves, and where. Comma-separated for more than one. |
Every flag has a variable spelled the same way, so
Configuration documents the rest — the startup
wait, update interval, EXAV_ALLOW_NO_DB, EXAV_ALLOW_SHUTDOWN, and where a
pulled database is stored.
Replacing a c-icap container
Section titled “Replacing a c-icap container”The same image serves ICAP (RFC 3507) on port 1344 when EXAV_LISTEN names
an icap:// address, as a drop-in for a c-icap container running the
virus_scan service against ClamAV. Adding one adds the listener, so one
container replaces the c-icap + clamav pair rather than standing in for half
of it:
docker run -d -p 3310:3310 -p 1344:1344 \ -e EXAV_LISTEN=clamd://0.0.0.0:3310,icap://0.0.0.0:1344 \ -v exav-db:/var/lib/exav ghcr.io/sylvinus/exavBoth protocols are then served by one process, over one loaded database — which is the saving, since a full signature set costs seconds and gigabytes to load and a second container mostly exists to avoid paying that twice.
For ICAP and nothing else, name the listener instead of taking the default command:
docker run -d -p 1344:1344 -v exav-db:/var/lib/exav \ ghcr.io/sylvinus/exav --listen icap://0.0.0.0:1344 --auto-updateThe port is bound only when asked for. See the ICAP guide for the verdict mapping and the full settings table.
When swapping over from the ClamAV image, translate that image’s
CLAMAV_*/FRESHCLAM_* settings to their EXAV_* equivalents — the mapping is
in Configuration.
Rootless bind mounts
Section titled “Rootless bind mounts”When you bind-mount a host directory (instead of a named volume), make it writable by uid 65532 so the daemon can write signatures and temp files:
mkdir -p ./exav-db && sudo chown 65532:65532 ./exav-dbdocker run -d -p 3310:3310 -v "$PWD/exav-db:/var/lib/exav" ghcr.io/sylvinus/exav--read-only needs a writable temp dir
Section titled “--read-only needs a writable temp dir”An object larger than --spill-threshold-bytes (16M) is written to disk while it
is scanned, so a container with --read-only and nowhere to write fails every
scan above that size — small objects keep working, which is what makes it look
intermittent. Give it a temp filesystem:
docker run -d -p 3310:3310 --read-only --tmpfs /tmp \ -v exav-db:/var/lib/exav ghcr.io/sylvinus/exavWithout one the daemon logs connection error: Permission denied and drops the
connection, and the client — clamdscan or exav’s own — reports an error and
exits 2. It is never reported as clean, but it is not a verdict either: the
scan did not happen. --spill-dir names somewhere other than TMPDIR if that
suits the mount layout better.
Dual container (shared volume)
Section titled “Dual container (shared volume)”Separate the updater from the scanner: an updater container refreshes a shared
volume and the scanner hot-reloads it — the setup in docker-compose.yml.
“Updater” is --auto-update with nothing to listen on, so what each container
does is on the line that starts it and neither can contradict itself:
services: exav: # scanner image: ghcr.io/sylvinus/exav:latest ports: ["3310:3310"] volumes: [exav-db:/var/lib/exav]
updater: # writes the shared volume, doesn't scan image: ghcr.io/sylvinus/exav:latest command: ["--auto-update", "--update-interval-secs", "43200"] environment: EXAV_LISTEN: "" # the image's default listener, switched off EXAV_SIG_SOURCES: "${EXAV_SIG_SOURCES}" volumes: [exav-db:/var/lib/exav]
volumes: exav-db:The scanner notices the updated files on the shared mount (an mtime watch, like
clamd’s SelfCheck) and re-forks its worker pool with the new signatures — no
restart, no cross-container signalling.