It has been a very long time since my last post here.
Anyway, I had to dig into a topic I never really explored and I thought sharing these results may help someone, one day, maybe.
The Requirement
One of my customers delivers a solution for its on-premises customers as a set of Docker images and Helm charts. The security team of one of these cutomers has asked for an antivirus scan report for every version. We are not talking about CVEs (with tools like Trivy or Grype). No, the focus here is about antivirus scans.
This is something we never do for Docker images.
Most of images are based on Linux filesystems and run on Linux systems, for which very few virus exist. It is not that it is not feasible, but Linux virus are rare (such systems face other threats). And in the Docker ecosystem, if such an infected file had to be spotted, the image itself would quickly be removed and fixed.
So, I have been looking for an antivirus for Linux that we can use in an automated pipeline to generate such reports. ClamAV quickly emerged as the best (to not say the only) choice, even though this tool appears in some comparisons as not as efficient as more well-known antivirus solutions. In fact, these other solutions provide additional services (such as real-time system monitoring). Therefore, the comparison delta is not really on antivirus scans but about features that ClamAV does not provide. It fact, ClamAV is often used on Linux systems to scan e-mail attachments. This tool comes with a CLI, which is very convenient for a CI integration.
Now, the question is… how do we scan a Docker image with ClamAV?
Unlike Trivy as an example, there is no “docker mode”. Sure, ClamAV is available as a Docker image, but its CLI has no dedicated option to scan a Docker image or to interact with a container registry. The real interrogation was: if we save the Docker image as a tar, is the scan as efficient as if we had mounted the ClamAV binary directly in the file system? Let’s compare the two approaches.
Scan a Docker image as a TAR file
First, let’s consider this Dockerfile:
FROM alpine:3.23ADD https://secure.eicar.org/eicar.com.txt /opt/ops/eicar.com.txtADD https://secure.eicar.org/eicar.com.txt /home/lambda/to.tmp
This is a basic Alpine image where we copy the EICAR file twice in two different locations.
The EICAR file is a special file whose signature is detected by antivirus Software and allows to perform some tests.
Now, let’s build the image and export as a TAR.
With this format, you only have blobs (or layers), in the same way you can see them in the file system of a container registry. Then, let’s scan it with ClamAV.
# Update the ClamAV imagedocker pull clamav/clamav:stable# Cache the virus databasedocker volume create clam_db# Create the image to scandocker build -t test-av .# Save it as a single filedocker save -o /tmp/test-av.tar test-av# The file system is a set of blobs, with a directory# /blobs/sha256 and various files.# Update the databasedocker run -ti --rm \ --name "clamav" \ -v /tmp/test-av.tar:/scan/test-av.tar \ --mount source=clam_db,target=/var/lib/clamav \ clamav/clamav:stable \ freshclam# Mount everything and launch the scandocker run -ti --rm \ --name "clamav" \ -v /tmp/test-av.tar:/scan/test-av.tar \ --mount source=clam_db,target=/var/lib/clamav \ clamav/clamav:stable \ clamscan /scan
Here is the result:
/scan/test-av.tar: Eicar-Test-Signature FOUND----------- SCAN SUMMARY -----------Known viruses: 3627885Engine version: 1.5.2Scanned directories: 1Scanned files: 1Infected files: 1Data scanned: 68 BData read: 8.31 MiB (ratio 0.00:1)Time: 11.504 sec (0 m 11 s)Start Date: 2026:06:27 13:49:40End Date: 2026:06:27 13:49:51
The signature is recognized.
However, you can notice that it does not say which file in the TAR file contains the signature, nor how many times it was found. The scan result will always contain a single line. Maybe not exactly the kind of details expected by the security team of our customer.
Scan a Docker image as an exported file system
Originally, I thought the best approach was mounting the ClamAV binary into a running container created from the image to scan and then run it. It appeared to not cover all the cases. Indeed, ClamAV is not available as a single binary. Instead, it has dependencies to libraries. And some libraries may change depending on the base image (consider Debian VS. Alpine images with musl / libc). Let’s not even mention distroless or scratch images.
So, the best solution is to extract the file system of the image, no matter what it is, launch ClamAV as previously, with the extracted file system in a mount point.
# Update the ClamAV imagedocker pull clamav/clamav:stable# Cache the virus databasedocker volume create clam_db# Create a container from the image but do not start itdocker create --name temporary_scan test-av# Export the file system of the container# (and fix permission issues for deletion)mkdir /tmp/test-av-fsdocker export temporary_scan | tar \ --no-same-owner \ --no-same-permissions \ --exclude='dev/*' \ -xf - \ -C /tmp/dg-fstar# Here, we really have a Linux file system# /bin, /dev, /etc, /home...# Delete the containerdocker rm temporary_scan# Update the databasedocker run -ti --rm \ --name "clamav" \ -v /tmp/test-av.tar:/scan/test-av.tar \ --mount source=clam_db,target=/var/lib/clamav \ clamav/clamav:stable \ freshclam# Mount everything and launch the scan# -r: recursive scan# -i: only show infected filesdocker run -ti --rm \ --name "clamav" \ -v /tmp/test-av-fs:/scan \ --mount source=clam_db,target=/var/lib/clamav \ clamav/clamav:stable \ clamscan -r -i /scan
Notice I used the “-i” option to only display the infected files.
Without this option, you get the whole list of scanned files (which may be good for a complete report).
Here is the result:
/scan/home/lambda/to.tmp: Eicar-Test-Signature FOUND/scan/opt/ops/eicar.com.txt: Eicar-Test-Signature FOUND----------- SCAN SUMMARY -----------Known viruses: 3627885Engine version: 1.5.2Scanned directories: 102Scanned files: 84Infected files: 2Data scanned: 8.73 MiBData read: 8.02 MiB (ratio 1.09:1)Time: 11.924 sec (0 m 11 s)Start Date: 2026:06:27 14:01:27End Date: 2026:06:27 14:01:39
The good part here is that it shows exactly which files are infected (and thus how many times). This information is maybe not useful for the report reader but it is for the development team if it wants to understand the source and fix it.
I found few comments indicating the second approach could be more efficient from a memory point of view, as files are already extracted, while in the first method, they are extracted on the fly before being analyzed. The scan time is almost the same here, so this is not relevant in this precise case. But there may be cases where it makes a difference.
Tricky situation
Let’s now consider this Dockerfile:
FROM alpine:3.23ADD https://secure.eicar.org/eicar.com.txt /opt/ops/eicar.com.txtRUN rm /opt/ops/eicar.com.txt
Almost the same file, except the EICAR file will appear in the Docker layers but not in the resulting file system mounted in the container. Let’s verify our scan process. By exporting the image (first method), we get…
/scan/test-av.tar: Eicar-Test-Signature FOUND----------- SCAN SUMMARY -----------Known viruses: 3627917Engine version: 1.5.3Scanned directories: 1Scanned files: 1Infected files: 1Data scanned: 17.56 MiBData read: 8.31 MiB (ratio 2.11:1)Time: 12.330 sec (0 m 12 s)Start Date: 2026:07:17 14:43:42End Date: 2026:07:17 14:43:54
This is not surprising, since the file is present in one of the layers.
Let’s see with the second method, that scans the resulting file system.
----------- SCAN SUMMARY -----------Known viruses: 3627917Engine version: 1.5.3Scanned directories: 100Scanned files: 82Infected files: 0Data scanned: 8.73 MiBData read: 8.02 MiB (ratio 1.09:1)Time: 11.545 sec (0 m 11 s)Start Date: 2026:07:17 14:44:44End Date: 2026:07:17 14:44:55
As expected, the file does not appear in the resulting file system and is thus not detected.
Conclusion
To be honest, I was not sure the first method would work.
An exported Docker image is a set of layers: when you open the first TAR, it does not have the same file structure than in the second attempt. So, I was curious to see if it could detect infected files among the Docker layers. And it did.
First, I had a preference for the second option.
I then thought about the case of a virus somewhere in a layer but not in the resulting file system. And what you mind is: will my servers and image repository host something that contain a virus? The first method is definitely more reliable. The second one should be reserved to obtain more precise reports and identify which files are affected (some sort of debug or deeper analysis).
Running ClamAV in a Docker container is also the most simple solution.
It updates the database on the first startup but needs to be updated manually then. To save time, in particular in a CI system, it is better to cache its database in a separate volume. You can just reuse the commands given above.



















