Mounts

This section describes how to use cache mounts and bind mounts with Docker builds.

Cache mounts let you specify a persistent package cache to be used during builds. The persistent cache helps speed up build steps, especially steps that involve installing packages using a package manager. Having a persistent cache for packages means that even if you rebuild a layer, you only download new or changed packages.

Cache mounts are created using the --mount flag together with the RUN instruction in the Dockerfile. To use a cache mount, the format for the flag is --mount=type=cache,target=<path>, where <path> is the location of the cache directory that you wish to mount into the container.

Add a cache mount

The target path to use for the cache mount depends on the package manager you’re using. The application example in this guide uses Go modules. That means that the target directory for the cache mount is the directory where the Go module cache gets written to. According to the Go modules reference, the default location for the module cache is $GOPATH/pkg/mod, and the default value for $GOPATH is /go.

Update the build steps for downloading packages and compiling the program to mount the /go/pkg/mod directory as a cache mount:

  # syntax=docker/dockerfile:1
  FROM golang:1.21-alpine AS base
  WORKDIR /src
  COPY go.mod go.sum .
- RUN go mod download
+ RUN --mount=type=cache,target=/go/pkg/mod/ \
+     go mod download -x
  COPY . .

  FROM base AS build-client
- RUN go build -o /bin/client ./cmd/client
+ RUN --mount=type=cache,target=/go/pkg/mod/ \
+     go build -o /bin/client ./cmd/client

  FROM base AS build-server
- RUN go build -o /bin/server ./cmd/server
+ RUN --mount=type=cache,target=/go/pkg/mod/ \
+     go build -o /bin/server ./cmd/server

  FROM scratch AS client
  COPY --from=build-client /bin/client /bin/
  ENTRYPOINT [ "/bin/client" ]

  FROM scratch AS server
  COPY --from=build-server /bin/server /bin/
  ENTRYPOINT [ "/bin/server" ]

The -x flag added to the go mod download command prints the download executions that take place. Adding this flag lets you see how the cache mount is being used in the next step.

Rebuild the image

Before you rebuild the image, clear your build cache. This ensures that you're starting from a clean slate, making it easier to see exactly what the build is doing.

$ docker builder prune -af

Now it’s time to rebuild the image. Invoke the build command, this time together with the --progress=plain flag, while also redirecting the output to a log file.

$ docker build --target=client --progress=plain . 2> log1.txt

When the build has finished, inspect the log1.txt file. The logs show how the Go modules were downloaded as part of the build.

$ awk '/proxy.golang.org/' log1.txt
#11 0.168 # get https://proxy.golang.org/github.com/charmbracelet/lipgloss/@v/v0.6.0.mod
#11 0.168 # get https://proxy.golang.org/github.com/aymanbagabas/go-osc52/@v/v1.0.3.mod
#11 0.168 # get https://proxy.golang.org/github.com/atotto/clipboard/@v/v0.1.4.mod
#11 0.168 # get https://proxy.golang.org/github.com/charmbracelet/bubbletea/@v/v0.23.1.mod
#11 0.169 # get https://proxy.golang.org/github.com/charmbracelet/bubbles/@v/v0.14.0.mod
#11 0.218 # get https://proxy.golang.org/github.com/charmbracelet/bubbles/@v/v0.14.0.mod: 200 OK (0.049s)
#11 0.218 # get https://proxy.golang.org/github.com/aymanbagabas/go-osc52/@v/v1.0.3.mod: 200 OK (0.049s)
#11 0.218 # get https://proxy.golang.org/github.com/containerd/console/@v/v1.0.3.mod
#11 0.218 # get https://proxy.golang.org/github.com/go-chi/chi/v5/@v/v5.0.0.mod
#11 0.219 # get https://proxy.golang.org/github.com/charmbracelet/bubbletea/@v/v0.23.1.mod: 200 OK (0.050s)
#11 0.219 # get https://proxy.golang.org/github.com/atotto/clipboard/@v/v0.1.4.mod: 200 OK (0.051s)
#11 0.219 # get https://proxy.golang.org/github.com/charmbracelet/lipgloss/@v/v0.6.0.mod: 200 OK (0.051s)
...

Now, in order to see that the cache mount is being used, change the version of one of the Go modules that your program imports. By changing the module version, you're forcing Go to download the new version of the dependency the next time you build. If you weren’t using cache mounts, your system would re-download all modules. But because you've added a cache mount, Go can reuse most of the modules and only download the package versions that doesn't already exist in the /go/pkg/mod directory.

Update the version of the chi package that the server component of the application uses:

$ docker run -v $PWD:$PWD -w $PWD golang:1.21-alpine \
    go get github.com/go-chi/chi/v5@v5.0.8

Now, run another build, and again redirect the build logs to a log file:

$ docker build --target=client --progress=plain . 2> log2.txt

Now if you inspect the log2.txt file, you’ll find that only the chi package that was changed has been downloaded:

$ awk '/proxy.golang.org/' log2.txt
#10 0.143 # get https://proxy.golang.org/github.com/go-chi/chi/v5/@v/v5.0.8.mod
#10 0.190 # get https://proxy.golang.org/github.com/go-chi/chi/v5/@v/v5.0.8.mod: 200 OK (0.047s)
#10 0.190 # get https://proxy.golang.org/github.com/go-chi/chi/v5/@v/v5.0.8.info
#10 0.199 # get https://proxy.golang.org/github.com/go-chi/chi/v5/@v/v5.0.8.info: 200 OK (0.008s)
#10 0.201 # get https://proxy.golang.org/github.com/go-chi/chi/v5/@v/v5.0.8.zip
#10 0.209 # get https://proxy.golang.org/github.com/go-chi/chi/v5/@v/v5.0.8.zip: 200 OK (0.008s)

Add bind mounts

There are a few more small optimizations that you can implement to improve the Dockerfile. Currently, it's using the COPY instruction to pull in the go.mod and go.sum files before downloading modules. Instead of copying those files over to the container’s filesystem, you can use a bind mount. A bind mount makes the files available to the container directly from the host. This change removes the need for the additional COPY instruction (and layer) entirely.

  # syntax=docker/dockerfile:1
  FROM golang:1.21-alpine AS base
  WORKDIR /src
- COPY go.mod go.sum .
  RUN --mount=type=cache,target=/go/pkg/mod/ \
+     --mount=type=bind,source=go.sum,target=go.sum \
+     --mount=type=bind,source=go.mod,target=go.mod \
      go mod download -x
  COPY . .

  FROM base AS build-client
  RUN --mount=type=cache,target=/go/pkg/mod/ \
      go build -o /bin/client ./cmd/client

  FROM base AS build-server
  RUN --mount=type=cache,target=/go/pkg/mod/ \
      go build -o /bin/server ./cmd/server

  FROM scratch AS client
  COPY --from=build-client /bin/client /bin/
  ENTRYPOINT [ "/bin/client" ]

  FROM scratch AS server
  COPY --from=build-server /bin/server /bin/
  ENTRYPOINT [ "/bin/server" ]

Similarly, you can use the same technique to remove the need for the second COPY instruction as well. Specify bind mounts in the build-client and build-server stages for mounting the current working directory.

  # syntax=docker/dockerfile:1
  FROM golang:1.21-alpine AS base
  WORKDIR /src
  RUN --mount=type=cache,target=/go/pkg/mod/ \
      --mount=type=bind,source=go.sum,target=go.sum \
      --mount=type=bind,source=go.mod,target=go.mod \
      go mod download -x
- COPY . .

  FROM base AS build-client
  RUN --mount=type=cache,target=/go/pkg/mod/ \
+     --mount=type=bind,target=. \
      go build -o /bin/client ./cmd/client

  FROM base AS build-server
  RUN --mount=type=cache,target=/go/pkg/mod/ \
+     --mount=type=bind,target=. \
      go build -o /bin/server ./cmd/server

  FROM scratch AS client
  COPY --from=build-client /bin/client /bin/
  ENTRYPOINT [ "/bin/client" ]

  FROM scratch AS server
  COPY --from=build-server /bin/server /bin/
  ENTRYPOINT [ "/bin/server" ]

Summary

This section has shown how you can improve your build speed using cache and bind mounts.

Related information:

Next steps

The next section of this guide is an introduction to making your builds configurable, using build arguments.