# syntax=docker/dockerfile:1 FROM golang:1.20-alpine WORKDIR /src COPY go.mod go.sum . RUN go mod download COPY . . RUN go build -o /bin/client ./cmd/client RUN go build -o /bin/server ./cmd/server ENTRYPOINT [ "/bin/server" ]
1 2 3 4
$ docker build --tag=buildme . $ docker images buildme REPOSITORY TAG IMAGE ID CREATED SIZE buildme latest c021c8a7051f 5 seconds ago 150MB
# syntax=docker/dockerfile:1 FROM golang:1.20-alpine WORKDIR /src COPY go.mod go.sum . RUN go mod download COPY . . RUN go build -o /bin/client ./cmd/client RUN go build -o /bin/server ./cmd/server + + FROM scratch + COPY --from=0 /bin/client /bin/server /bin/ ENTRYPOINT [ "/bin/server" ]
1 2 3 4
$ docker build --tag=buildme . $ docker images buildme REPOSITORY TAG IMAGE ID CREATED SIZE buildme latest 436032454dd8 7 seconds ago 8.45MB
每一个 FROM 代表了一个stage,上面加入的 FROM scratch 将其分为了两个stage,第一个stage负责编译 client 和 server ,第二个stage将产出从第一个stage拷贝过来,最终构建的image会保留最后一个的stage。从 docker images buildme 的输出可以看到,image的体积从 150MB 降到了 8.45MB。
并行构建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
# syntax=docker/dockerfile:1 - FROM golang:1.20-alpine + FROM golang:1.20-alpine AS base WORKDIR /src COPY go.mod go.sum . RUN go mod download COPY . . + + FROM base AS build-client RUN go build -o /bin/client ./cmd/client + + FROM base AS build-server RUN go build -o /bin/server ./cmd/server
# syntax=docker/dockerfile:1 - FROM golang:1.20-alpine + FROM golang:1.20-alpine AS base WORKDIR /src COPY go.mod go.sum . RUN go mod download COPY . . + + FROM base AS build-client RUN go build -o /bin/client ./cmd/client + + FROM base AS build-server RUN 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" ]
# syntax=docker/dockerfile:1 FROM golang:1.20-alpine WORKDIR /src COPY go.mod go.sum . - RUN go mod download + RUN --mount=type=cache,target=/go/pkg/mod/ \ + go mod download -x COPY . . RUN go build -o /bin/client ./cmd/client RUN go build -o /bin/server ./cmd/server ENTRYPOINT [ "/bin/server" ]
golang通过 go get 下载的 mod 默认位于 $GOPATH/pkg/mod 中(其中 $GOPATH 默认为 /go),上面将该目录使用 Cache Mount 挂载,要观察缓存是否生效的话,可以参考guide中以下的做法:
# 2. 重新执行构建,并将构建过程打印出来 $ docker build --progress=plain . 2> log1.txt $ 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) ...
# 3. 引入新的依赖包(以下命令将当前目录挂载进了容器并指定了workdir,所以在golang的容器内执行go get之后,新加入的依赖包的信息也会更新到宿主机的go.mod和go.sum中) $ docker run -v $PWD:$PWD -w $PWD golang:1.20-alpine \ go get github.com/go-chi/chi/[email protected]
# 4. 重新执行构建(可以看到仅下载了新加入的chi包) $ docker build --progress=plain . 2> log2.txt 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)
利用Bind Mount减少构造时的文件拷贝
对于仅在 Build 阶段使用,且已存在于宿主机的代码或配置等文件,可以通过 Bind Mount 来减少构建时文件拷贝的开销,这将直接使用宿主机上的文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# syntax=docker/dockerfile:1 FROM golang:1.20-alpine WORKDIR /src - COPY go.mod go.sum . - RUN go mod download + RUN --mount=type=bind,source=go.mod,target=go.mod \ --mount=type=bind,source=go.sum,target=go.sum \ go mod download -x - COPY . .
- RUN go build -o /bin/client ./cmd/client + RUN --mount=type=bind,target=. \ + go build -o /bin/client ./cmd/client
- RUN go build -o /bin/server ./cmd/server + RUN --mount=type=bind,target=. \ go build -o /bin/server ./cmd/server
以上是一个Golang项目的Dockerfile文件的部分内容,第一行是将项目代码拷贝进工作空间,第二行是处理项目的依赖。如果项目代码发生了变化,那么该Layer需要重新构建,不管项目的依赖有没有发生变化,之后的 RUN go mod download Layer都会重新执行,这样就产生了没有必要的开销。