moby / buildkit

concurrent, cache-efficient, and Dockerfile-agnostic builder toolkit
https://github.com/moby/moby/issues/34227
Apache License 2.0
8.27k stars 1.17k forks source link

mount=type=cache more in-depth explanation? #1673

Open peci1 opened 4 years ago

peci1 commented 4 years ago

Hi, I'm trying to use cache mounts to speed up things like apt install, pip install or ccache when rebuilding my containers.

For each of these, I prefix each RUN command using apt/pip/ccache... with things like:

RUN --mount=type=cache,target=/var/cache/apt,rw --mount=type=cache,target=/var/lib/apt,rw  \

That seems to work sometimes, but I found out I can't understand the cache invalidation rules. The cache just from time to time disappears and I have to download all the cached packages again. I made explicit effort to remove all apt clean commands from the dockerfile, so the cache is not deleted programatically. And it also happens to CCache, which is not autodeleted.

Could somebody please explain how does it work and when exactly can the cache be reused, when (if at all) is it discarded, where is it saved...? What is the exact effect of the id parameter?

I also found out that if I want to create a cache directory for a non-root user, I can create it with cache mount option uid=1000. But if I use uid=1000,gid=1000, the folder gets ownership root:root, which is really weird. Can it be connected with me not using id and setting some caches with uid=0 and some with uid=1000? Does this create some kind of conflict?

And is there actually a good reason for setting the mode to anything else than 777? If it's only used during build, I don't get why anybody would care about permissions...

Thank you for helping.

Maybe some of the answers could be added to https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md ?

peci1 commented 4 years ago

And is it affected by the --no-cache build option?

YesYouKenSpace commented 4 years ago

If I may hijack this, I like to ask if RUN --mount=type=cache can use the cache image in --cache-from ?

morlay commented 4 years ago

confused for me too.

--cache-from and --cache-to do nothing for --mount=type=cache https://github.com/docker/buildx/issues/399

--mount=type=cache cached dir in buildkit container /var/lib/buildkit/ until gc cleanup.

FernandoMiguel commented 4 years ago

I have quite a few cache examples here https://github.com/FernandoMiguel/Buildkit

I suspect your issue is GC which by default is very small. also --no-cache will not use cache for that run

morlay commented 4 years ago

@FernandoMiguel use remote buildkit could resolve by increasing gc limits. i setup it for my private projects.

but some ci like Travis or GitHub Workflows

buildkit is always created. --mount=type=cache cached files in container, will be lost.

FernandoMiguel commented 4 years ago

Yes for any ephemeral host, cache is lost. That is expected and the correct behaviour.

You can use cache-from to pull layers from a image repo in that case

morlay commented 4 years ago

close https://github.com/docker/buildx/issues/399 and discussing here.

@FernandoMiguel

any suggestion in this case?. i need cache /go/pkg/mod for multi workflows in build stage.

but the --cache-to couldn't expose the cached files (/var/lib/buildkit) to host. and buildkit recreated when each workflow starts, all cached files in /var/lib/buildkit losts

buildx not provide way to mount host path for /var/lib/buildkit when create buildkit

FernandoMiguel commented 4 years ago

No idea what your Dockerfile looks like or what ci you are using That makes all the difference Some ci will allow you to cache host data

morlay commented 4 years ago

here example

workflow.yml https://github.com/querycap/istio/blob/master/.github/workflows/istio-pilot.yml Dockerfile https://github.com/querycap/istio/blob/master/build/istio/Dockerfile.pilot

and the logs https://pipelines.actions.githubusercontent.com/qcc22rpVKD7YzUzpABpz6Nbu6TDqQ0MFAEbSsnK9GDsCU4Hq5g/_apis/pipelines/1/runs/325/signedlogcontent/3?urlExpires=2020-09-24T13%3A32%3A15.1287086Z&urlSigningMethod=HMACV1&urlSignature=MDAD0FQhxCis3TFZLKqjsdmvB2m0iiCBWv6G9FyDOGs%3D

--cache-from --cache-to host path /tmp/.buildx/cache image

--mount=type=cache not working image

i understands how --mount=type=cache work.

i have no idea to cache files in buildkit container /var/lib/buildkit with docker buildx directly.

FernandoMiguel commented 4 years ago

Type cache won't obviously work on github actions since every run is done a new host. You can run your own host and control that, or hack a way to store the cache (github limits are too low for any real use)

morlay commented 4 years ago

@FernandoMiguel mount host path like /tmp/buildkit to buildkit to /var/lib/buildkit, then i add /tmp/buildkit to actions cache. could this be possible?

FernandoMiguel commented 4 years ago

What

morlay commented 4 years ago
docker buildx create --use --name localbuild
docker buildx inspect localbuild --bootstrap

# recreate buildkit with host path
docker rm -f buildx_buildkit_localbuild0
docker run --privileged -d --name=buildx_buildkit_localbuild0 -v=/tmp/buildkit:/var/lib/buildkit moby/buildkit:buildx-stable-1         

now /tmp/buildkit in host contains buildkit files /var/lib/buildkit i think i could cache this folder /tmp/buildkit

@FernandoMiguel it's not work with lots of permission error for whole path.

hope --cache-from --cache-to could support --mount=cache.

or add a option to path to assign the snapshots host path --mount=type=cache,path=/tmp/gomod,target=/go/pkg/mod

or replace the snapshot id of dirname with the id of --mount=type=cache,id=gomod,target=/go/pkg/mod /var/lib/buildkit/runc-overlayfs/snapshots/snapshots/2/go/pkg/mod /var/lib/buildkit/runc-overlayfs/snapshots/snapshots/gomod/go/pkg/mod

FernandoMiguel commented 4 years ago

/tmp is a special folder and may have weird permissions

morlay commented 4 years ago

github actions use a non-root user, but /var/lib/buildkit/runc-overlayfs only for root user.

and /var/lib/buildkit/runc-overlayfs always in changing. i give up to use this way.
need find other hacks.

Aposhian commented 2 years ago

Since this thread has been hijacked for a different issue, @peci1's questions are still unanswered. What exactly does id do? What are the cache invalidation rules?

ringerc commented 2 years ago

It's notable that the docs in https://docs.docker.com/engine/reference/commandline/buildx_build/ don't mention the cache, too.

spacedub commented 2 years ago

@Aposhian

I just spent some time testing cache sharing modes (private, locked, shared) in buildkitd/buildctl - I do assume the behavior is the same with buildx.

Here is my understanding:

Note that the cache object is the same even if you change the mount path... only the ID+mode matters...

So, same ID and mode, the cache object should not "disappear" on its own... ... that is, unless: a. you do a buildctl prune against your buildkitd, which destroys the cache objects b. or you run your build with --no-cache, in which case all cache objects used by that build will be reset (including for other concurrent builds that are still in-flight) c. or garbage collection decided to evict that cache entry d. or you are using a "private" mount and starting your build while another build is already accessing the mount e. OR... the apt-get configuration itself, inside your build is purposefully deleting the cache entries after a run

Furthermore, in the OP question, it looks to me like the sharing mode is shared (which is the default behavior). For use with apt, this is probably wrong. According to Docker documentation "apt needs exclusive access to its data" and their documentation suggests using locked instead https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#run---mounttypecache

Of course, if you are running a lot (a few?) concurrent builds on that buildkitd, locked will definitely slow them all down and make you loose some / most of the caching benefits in the first place...

Whether or not locked (or private) is enough in the apt world... If you have multiple different apt versions in different images using the same mount (id+sharingmode) for your apt, you might be in for trouble...

So, given OP used "shared": f. maybe concurrent apt access to the cache mount makes apt drop all the data in some cases?

Furthermore, the OP mounts /var/cache/apt. But if you look at cat /etc/apt/apt.conf.d/docker-clean inside the Debian official image, it is clear that this actually prevents any caching of the packages. So, solely mounting into /var/cache/apt is useless.

Finally, mounting and reusing /var/lib/apt also seems useless to me. The cache benefit is about 3 seconds on Debian (3.7s on empty cold start, vs. 0.7s once /var/lib/apt is populated), in the rare case where the build instruction itself is not cached. And since you cannot trust the content of the cache folder... you cannot save yourself the apt-get update operation anyway in further instructions...

In a shell, if you want to use cache properly for apt:

Assuming you get the above right, the only remaining reasons for cache to disappear are a. b. or c from above (pretty much explicit prune or cache bust, or garbage collection).

Hope that helps.

spacedub commented 2 years ago

Thought I would clarify what happens when you do NOT use an id.

It will default to the target (eg: mount path). So, in OP case, the cache for apt is shared, meaning:

peci1 commented 2 years ago

Thanks for the analysis.

But if you look at cat /etc/apt/apt.conf.d/docker-clean inside the Debian official image, it is clear that this actually prevents any caching of the packages.

I have this at the beginning of the dockerfile:

RUN sudo rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' | sudo tee /etc/apt/apt.conf.d/keep-cache

So this should not interfere with the cache.

peci1 commented 2 years ago

And I do not build concurrently, so private/shared is not a problem for me.

Aposhian commented 2 years ago

Thanks @spacedub!

That is really good to know that:

I think it would be interesting to experiment to see if sharing /var/cache/apt mounts between OS versions would cause problems. My first guess is not since the package downloads are specified by version and codename: for example: docker-ce_5%3a20.10.18~3-0~ubuntu-focal_amd64.deb

Why would you not cache /var/lib/apt, even if the gain is smaller relative to /var/cache/apt? Specifically /var/lib/apt/lists? Isn't that mainly caching work from apt-get update, fetching package lists from repositories? I personally like just making it a cache mount, and then I can skip the step of doing something like rm -rf /var/lib/apt/lists in my Dockerfile.

but be sure to ALSO configure apt to use it, as it is disabled in apt-conf.d in the official images (eg: that is option Dir::Cache)

I believe this is also circumvented by the one liner that @peci1 shared

spacedub commented 2 years ago

UID/GID I have not tested so I do not know (I will check later today) That being said I would be weary of using the same id and sharing mode with different uids in different places - seems quite error prone / confusing to me.

About the "lists" part, if your purpose is to avoid having to rm, a tmpfs should give you the same benefits without the downsides of locking / waiting. But then, if you are mostly building one thing at a time and mostly the same thing, "shared" is probably acceptable.

About the package cache - what happens if a different package with that same name has been downloaded in that location? Will apt verify the checksum of that (and then what happens next) or is apt verifying the checksum before, at download time? Will different Debian based distributions (or different versions of the same distribution) use different package names and versions for the same things, or can they conflict? Short of having clarity on these... I would be careful using the same share across unrelated Dockerfiles...

fkhantsi commented 1 year ago

hey, back to the first question, reusing cache among different ci/cd hosts.

I am using AWS CodeBuild to build an image, which allows me to store specific directories between builds, even though the host will go away. I just need to know which directory from the host I should persist.

Is it /var/lib/buildkit? is it /var/lib/docker/buildkit/cache.db as per #1474 ?

spacedub commented 1 year ago

To share cache between hosts, I would suggest using registry stored cache - see https://github.com/moby/buildkit#export-cache

Trying to share host folders used by buildkit between machines does not sound like a good idea...

fkhantsi commented 1 year ago

is this specifically the mount=type=run cache, or layer cache?

spacedub commented 1 year ago

Layer cache AFAIK. I would not use mount cache as a form of persistent storage... and I would not try to share them across hosts.

https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#run---mounttypecache

Does spell it out:

Cache mounts should only be used for better performance. Your build should work with any contents of the cache directory as another build may overwrite the files or GC may clean it if more storage space is needed.

If you want something to persist, put it in a scratch image and push it - or leverage layer cache.

Hope that helps.

fkhantsi commented 1 year ago

Thanks, but this doesn't help me. I do want to use cache for better performance -- specifically for local maven repository.

Here is what I am trying to do: use a multi stage Dockerfile to first build my .war in a maven container, and then deploy it to a tomcat container. The build process should be identical on my dev machine, and in my CodeBuild CI/CD environment.

Without any optimization, I would be rebuilding all of the images, and downloading all of my maven dependencies on every build.

First optimization, which is not relevant to this discussion, is to publish the build image to a container registry, and build using cache-from. This will cache the layers, however if I change my pom.xml (maven's dependency file for those who don't know), the layer will be invalidated, and it will have to rebuild it, and redownload all the dependencies from maven central, instead of just one.

The solution for this is using --mount=type=cache target=/root/.m2 Unfortunately, since the host is ephemeral, the cache will be destroyed. I want to persist the cache between builds by saving it to s3, something that codebuild supports, by asking only which directory in the host to store between builds.

So which directory do I give it? /var/lib/buildkit? /var/lib/docker/buildkit/cache.db or something else?

spacedub commented 1 year ago

Thanks for clarifying. Why not use --mount=type=bind then (instead of --mount=type=cache)?

Pretty much gives you what you are asking for, and then you can just save that specific folder anyway you want.

fkhantsi commented 1 year ago

because it's for reading from host, not writing. Even if you make it read-write, the writes are discarded

spacedub commented 1 year ago

Yep, you are right. Mixing things up with LLB...

To your question, in my case here using buildkit + the containerd worker without docker, the state is probably kept in /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs or more likely I would have to copy the entire /var/lib/containerd and /var/lib/buildkit folder...

Location will clearly depend on how you are launching buildkitd, and obviously which worker you are using (eg: you might want to look into buildkitd root argument: --root value path to state directory (default: "/var/lib/buildkit")).

Hope that helps.

creative-resort commented 1 year ago

For which duration is a (cache) mount valid and available in a Dockerfile?

Is it possible to have it mounted just for one RUN command, or to unmount it a couple of layers later?

Aposhian commented 1 year ago

For which duration is a (cache) mount valid and available in a Dockerfile?

A cache mount is only mounted for the RUN command that you specify it for. Subsequent RUN commands without the cache mount specified will just have an empty folder where it was mounted, if I remember correctly.

codethief commented 1 year ago

@Aposhian Do you happen to know in how far the contents of the mount cache affect the caching of the RUN command's layer? Are they used as a cache "key", i.e. whenever the mount cache changes, the image layer cache will be busted and the RUN command will be executed anew?

Aposhian commented 1 year ago

I don't think so. I don't think that checksums for the contents of cache mounts are computed. They are more just like volume mounts for building, for caching common things like package/asset downloading or compiler caching, in my experience.

ye7iaserag commented 1 year ago

Why isn't it possible to have a build bind mount that persists and keeping the persist defaulting to false like the rw option? This would be so helpful especially for pip packages like torch that can reach a couple of GBs with cuda also it would solve a long living issue with all WSL2 users where the virtual disk size can only grow bigger and at some point are forced to purge and rebuild images which then will not find any caches to use.

alex-statsig commented 1 year ago

+1, Layer caching does not work very well for optimizing small changes to a build process. Ex. if one new package must be installed, layer caching is entirely invalidated; mount=type=cache allows persisting the package store so the incremental install is much cheaper. Or similarly, with nextjs there is a build artifact ".next/cache" which is supposed to be persisted in future runs to reduce client bundle thrash and improve build speed.

It sounds like most people expected --cache-from type=regsitry to allow loading these mounts from a cache image. I'm not sure why that wouldn't be supported.

ye7iaserag commented 1 year ago

Yes, but then you can't keep that cache between purges, it's maintained by docker, you can't back it up or move it... I explained things more in here https://github.com/moby/moby/issues/15771#issuecomment-1762287334

homerjam commented 1 year ago

Switching from docker build to docker buildx build and it seems like caching is completely absent now 🤷 ie. every time I build it's downloading the FROM image and executing every RUN - previously a build would be cached and finish in seconds

brianbraunstein commented 1 year ago

It looks like there's at least 1 more dimension included in the "true" cache ID, the from field.

The value given appears to effectively be translated to the underlying stage/image's hash before being used in the ID tuple. This means if you give different names pointing to the same stage/image, the cache will be shared.

I've attached my playground directory which demonstrates the result. One could probably tweak this to dig around and reverse engineer more relevant dimensions. true_cache_id.tar.gz

It would be great if this was documented rather than requiring reverse engineering the behavior. It would feel less dodgy depending on documented behavior rather than discovered behavior.

@spacedub Did you get around to checking uid and gid?

Gripe: I find this behavior a little bit of a pity. If you want a different cache, then just provide a different ID, right? This behavior is more confusing, particularly when not documented. Plus I already have a use case where 2 separate images could properly seed the same cache. It's easy enough to work around though.

mfittko commented 1 year ago

Mounting folders from the host to the builder (just like using --volume when running a container) would be the killer feature, no? Guess that's at least what some people here expected to find but were disappointed that RUN --mount doesn't exactly provide this.

Being able to use a persistent cache that could be stored on EFS or similar would be so cool! I mean most people build their production images on a CI service. Unfortunately the only slightly comparable option is to add a cache image for the build stages which makes things overly complex and which also needs to be re-built from time to time 🤷‍♂️

Anyone having experiences on using a dedicated builder container running on ECS or similar that could be used for caching the RUN --mounts for a longer period of time so it could be re-used for subsequent builds in ephemeral CI environments?

Really looking for some build file caching option in addition to the regular layer cache option!

FeryET commented 11 months ago

How does --mount=type=cache affect the final image size? Will the mounted cache target get removed from the created container once the image is built?

jedevc commented 11 months ago

How does --mount=type=cache affect the final image size? Will the mounted cache target get removed from the created container once the image is built?

@FeryET yup, that's correct, the cache is not part of any layer in the final image - it's just a local helper to speed up builds.

yukinakanaka commented 8 months ago

FYI about sharing

The buildkit document says

One of shared, private, or locked. Defaults to shared. A shared cache mount can be used concurrently by multiple writers. private creates a new mount if there are multiple writers. locked pauses the second writer until the first one releases the mount.

And, to understand the above specific behaviors, I just tested three cache sharing modes (shared, locked, private).

I hope those are useful to someone.

kuchaguangjie commented 8 months ago

I think this short answer from SO explains it clearly: https://stackoverflow.com/a/76351422/1568658

ryanotella commented 3 months ago

On Docker for Desktop on macOS, I found id and mode ineffective, but uid worked correctly.

# pip example
RUN useradd -u 1001 -m app -d /app
...
USER app
RUN --mount=type=cache,target=/app/.cache/pip,uid=1001 pip install -r requirements.txt
rkarp commented 3 months ago

I've just tested changing the uid, gid and mode parameters to --mount=type=cache, and modifying either of them causes the cache to be empty, indicating that you get a new cache. So they do seem to be part of the effective ID.