dockercontainersdocker-imagedocker-clioverlayfs

Scalable method to find the cause of a data leak in docker images folder (/var/lib/docker/overlay2 >> docker system df)?


After some change made to docker build scripts used in Jenkins pipelines and/or a Docker upgrade I've started experiencing serious image data leaks. They manifest themselves in an unstable, constantly growing size of the images directory /var/lib/docker/overlay2, which is always larger than the Images reachable and cleanly removable by the Docker CLI (as shown by docker system df). On a busy build server this means some 1 TB of image data leaking per day, which cannot be pruned using clean Docker CLI methods (such as docker system prune) and have to be removed manually (the entire /var/lib/docker).

How to debug and find the cause of such a data leak in a scalable way, which would be applicable in general, not just in this case [1]?

I'm talking about situations such as these (immediately after docker system prune -af):

$ sudo du -sch /var/lib/docker/overlay2; echo ""; docker system df

68G     /var/lib/docker/overlay2
68G     total

TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          0         0         0B        0B
Containers      0         0         0B        0B
Local Volumes   0         0         0B        0B
Build Cache     0         0         0B        0B

Nearly all these unreachable leaked leftovers are in the "diff" subfolders, but that would not be specific enough to distinguish them from useful layers, also stored in such subfolders:

$ find | grep diff | wc -l
989599
# vs.
$ find | grep -v diff | wc -l
792

The contents of these leaked folders also look quite non-suspicious (all file extensions, both compiled from source and shipped as pre-compiled libraries):

./268713830f68824c1f6f5b21aad89055c18e8c0e6f1751654ec82ca4caf5bba9/diff/opt/conda/lib/python3.11/site-packages/sklearn/ensemble/_weight_boosting.py
./268713830f68824c1f6f5b21aad89055c18e8c0e6f1751654ec82ca4caf5bba9/diff/opt/conda/lib/python3.11/site-packages/sklearn/ensemble/_gradient_boosting.cpython-311-x86_64-linux-gnu.so
./268713830f68824c1f6f5b21aad89055c18e8c0e6f1751654ec82ca4caf5bba9/diff/opt/conda/lib/python3.11/site-packages/sklearn/ensemble/tests/test_gradient_boosting.py
./268713830f68824c1f6f5b21aad89055c18e8c0e6f1751654ec82ca4caf5bba9/diff/opt/conda/lib/python3.11/site-packages/sklearn/ensemble/tests/__pycache__/test_gradient_boosting.cpython-311.pyc
./268713830f68824c1f6f5b21aad89055c18e8c0e6f1751654ec82ca4caf5bba9/diff/opt/conda/lib/python3.11/site-packages/sklearn/ensemble/tests/__pycache__/test_weight_boosting.cpython-311.pyc
./268713830f68824c1f6f5b21aad89055c18e8c0e6f1751654ec82ca4caf5bba9/diff/opt/conda/lib/python3.11/site-packages/sklearn/ensemble/tests/test_weight_boosting.py
./268713830f68824c1f6f5b21aad89055c18e8c0e6f1751654ec82ca4caf5bba9/diff/opt/conda/lib/python3.11/site-packages/scipy/special/tests/data/boost.npz
./268713830f68824c1f6f5b21aad89055c18e8c0e6f1751654ec82ca4caf5bba9/diff/opt/conda/lib/python3.11/site-packages/scipy/special/tests/test_boost_ufuncs.py

I also upgraded from docker build to docker buildx (Moby BuildKit builder toolkit), and at least after a few hours of test builds of one large container the situation looked more promising, because /var/lib/docker/overlay2 was stable and never grew more than the size needed for the images, as indicated by docker system df. However, because the storage could not be reclaimed after the containers were deleted (and docker system was pruned), then over time the builds will still take up all available space, it's only a question of when, not if:

$ sudo du -sch /var/lib/docker/overlay2; echo ; docker system df                                                                                                 build-srv-3-fi: Sun Jul 28 14:43:42 2024

117G    /var/lib/docker/overlay2
117G    total

TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          0         0         0B        0B
Containers      0         0         0B        0B
Local Volumes   0         0         0B        0B
Build Cache     0         0         0B        0B

(this was under docker buildx version v0.15.1 1c1dbb2)

This issue doesn't seem to be affected by the methods used for cleaning the data (e.g. removing first images and then build cache does not work any better than using the all-in method docker system prune).


[1] i.e. not restricted to my rather complex setup with dockerized Jenkins run in Docker Compose building (in parallel) dozens of pipelines with partially overlapping image layers and/or inter-connected (multi-stage) containers, requiring 4 additional service containers (such as Jenkins) to build each "business" container.



Edit: the --one-file-system switch of du, which was recommended by some, made no difference, and shows the problem of disk use is real, not just an artifact of double-counting caused by the layering file system:

$ sudo du -sch /var/lib/docker/overlay2; echo ""; sudo du -sch --one-file-system /var/lib/docker/overlay2; echo ""; docker system df

452G    /var/lib/docker/overlay2
452G    total

452G    /var/lib/docker/overlay2
452G    total

TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          0         0         0B        0B
Containers      0         0         0B        0B
Local Volumes   0         0         0B        0B
Build Cache     0         0         0B        0B


Edit: A more extreme storage usage scenario (where du no longer works (in finite time) on /var/lib/docker/overlay2, presumably due to its complexity):

$ df -haT
Filesystem     Type         Size  Used Avail Use% Mounted on
[..]
/dev/md2       ext4         3.4T  3.2T  5.3G 100% /


$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          333       0         2.873TB   2.873TB (100%)
Containers      0         0         0B        0B
Local Volumes   0         0         0B        0B
Build Cache     2532      0         55.14GB   55.14GB

$ docker system prune -af
Total reclaimed space: 812.8GB

$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          0         0         0B        0B
Containers      0         0         0B        0B
Local Volumes   0         0         0B        0B
Build Cache     0         0         0B        0B

...vs. the reality: the amount of space that docker system prune claimed to have reclaimed was underestimated by half - 0.4T rather than 0.8T and we still have a few times more leaked images data unreachable to docker stored in /var/lib/docker/overlay2). Notice also that even the prediction of what would be reclaimable made by docker system df turned out to be off by some 2.5T (when confronted with the reality). Only the build cache limit from docker buildx settings (set at 50 GB) was more or less working - it was exceeded by just 10%.

$ df -haT
Filesystem     Type         Size  Used Avail Use% Mounted on
[..]
/dev/md2       ext4         3.4T  2.8T  400G  88% /

# restarting the docker daemon and socket does not help either:
$ sudo systemctl restart docker docker.socket

$ df -haT
Filesystem     Type         Size  Used Avail Use% Mounted on
[..]
/dev/md2       ext4         3.4T  2.8T  400G  88% /



Solution

  • The issue in docker system prune -af not being able to clean all container images data seems to have been resolved by Docker Engine v27.2.1. I looked into the release notes, but could not identify any relevant ones (related to pruning) by this version.

    # before:
    $ df -haT
    Filesystem     Type         Size  Used Avail Use% Mounted on
    [..]
    /dev/md2       ext4         3.4T  3.2T   20G 100% /
    [..]
    
    # the cleanup (taking just as long as `rm -rf /var/lib/docker` would)
    $ docker system prune -af
    [..]
    
    # results:
    $ sudo du -sch /var/lib/docker/overlay2; echo ""; sudo du -sch --one-file-system /var/lib/docker/overlay2; echo ""; docker system df
    1.1M    /var/lib/docker/overlay2
    1.1M    total
    
    1.1M    /var/lib/docker/overlay2
    1.1M    total
    
    TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
    Images          0         0         0B        0B
    Containers      0         0         0B        0B
    Local Volumes   1         0         914.2kB   914.2kB (100%)
    Build Cache     0         0         0B        0B
    
    # after:
    $ df -haT
    Filesystem     Type         Size  Used Avail Use% Mounted on
    [..]
    /dev/md2       ext4         3.4T  158G  3.0T   5% /
    [..]
    
    # versions:
    $ docker version
    Client: Docker Engine - Community
     Version:           27.2.1
     API version:       1.47
     Go version:        go1.22.7
     Git commit:        9e34c9b
     Built:             Fri Sep  6 12:08:10 2024
     OS/Arch:           linux/amd64
     Context:           default
    
    Server: Docker Engine - Community
     Engine:
      Version:          27.2.1
      API version:      1.47 (minimum version 1.24)
      Go version:       go1.22.7
      Git commit:       8b539b8
      Built:            Fri Sep  6 12:08:10 2024
      OS/Arch:          linux/amd64
      Experimental:     false
     containerd:
      Version:          1.7.22
      GitCommit:        7f7fdf5fed64eb6a7caf99b3e12efcf9d60e311c
     runc:
      Version:          1.1.14
      GitCommit:        v1.1.14-0-g2c9f560
     docker-init:
      Version:          0.19.0
      GitCommit:        de40ad0
    

    Update: It continues to work as expected in new versions of Docker Engine, as seen from this typical use case of freeing some space for new containers by removing a selection of old ones:

    # BEFORE:
    $ df -haT
    Filesystem     Type         Size  Used Avail Use% Mounted on
    [..]
    /dev/md2       ext4         3.4T  3.0T  196G  94% /
     
    $ IMG_TO_RM=$(docker images | grep gpu-py39 | awk '{print $3}')
    
    $ docker rmi -f $IMG_TO_RM
    [..]
    Total reclaimed space: 437GB
    
    # AFTER:
    $ df -haT
    Filesystem     Type         Size  Used Avail Use% Mounted on
    [..]
    /dev/md2       ext4         3.4T  2.6T  573G  83% /
    
    $ docker version
    Client: Docker Engine - Community
     Version:           27.3.1
     API version:       1.47
     Go version:        go1.22.7
     Git commit:        ce12230
     Built:             Fri Sep 20 11:41:00 2024
     OS/Arch:           linux/amd64
     Context:           default
    
    Server: Docker Engine - Community
     Engine:
      Version:          27.3.1
      API version:      1.47 (minimum version 1.24)
      Go version:       go1.22.7
      Git commit:       41ca978
      Built:            Fri Sep 20 11:41:00 2024
      OS/Arch:          linux/amd64
      Experimental:     false
     containerd:
      Version:          1.7.23
      GitCommit:        57f17b0a6295a39009d861b89e3b3b87b005ca27
     runc:
      Version:          1.1.14
      GitCommit:        v1.1.14-0-g2c9f560
     docker-init:
      Version:          0.19.0
      GitCommit:        de40ad0