Dockerfile 技巧——镜像的多阶段构建

这一节来聊聊多阶段构建,以及为什么要使用它。

C语言例子

假如有一个C的程序,我们想用Docker去做编译,然后执行可执行文件。

  1. #include <stdio.h>
  2. void main(int argc, char *argv[])
  3. {
  4. printf("hello %s\n", argv[argc - 1]);
  5. }

本地测试(如果你本地有C环境)

  1. $ gcc --static -o hello hello.c
  2. $ ls
  3. hello hello.c
  4. $ ./hello docker
  5. hello docker
  6. $ ./hello world
  7. hello world
  8. $ ./hello friends
  9. hello friends
  10. $

构建一个Docker镜像,因为要有C的环境,所以我们选择gcc这个image

  1. FROM gcc:9.4
  2. COPY hello.c /src/hello.c
  3. WORKDIR /src
  4. RUN gcc --static -o hello hello.c
  5. ENTRYPOINT [ "/src/hello" ]
  6. CMD []

build和测试

  1. $ docker build -t hello .
  2. Sending build context to Docker daemon 5.12kB
  3. Step 1/6 : FROM gcc:9.4
  4. ---> be1d0d9ce039
  5. Step 2/6 : COPY hello.c /src/hello.c
  6. ---> Using cache
  7. ---> 70a624e3749b
  8. Step 3/6 : WORKDIR /src
  9. ---> Using cache
  10. ---> 24e248c6b27c
  11. Step 4/6 : RUN gcc --static -o hello hello.c
  12. ---> Using cache
  13. ---> db8ae7b42aff
  14. Step 5/6 : ENTRYPOINT [ "/src/hello" ]
  15. ---> Using cache
  16. ---> 7f307354ee45
  17. Step 6/6 : CMD []
  18. ---> Using cache
  19. ---> 7cfa0cbe4e2a
  20. Successfully built 7cfa0cbe4e2a
  21. Successfully tagged hello:latest
  22. $ docker image ls
  23. REPOSITORY TAG IMAGE ID CREATED SIZE
  24. hello latest 7cfa0cbe4e2a 2 hours ago 1.14GB
  25. gcc 9.4 be1d0d9ce039 9 days ago 1.14GB
  26. $ docker run --rm -it hello docker
  27. hello docker
  28. $ docker run --rm -it hello world
  29. hello world
  30. $ docker run --rm -it hello friends
  31. hello friends
  32. $

可以看到镜像非常的大,1.14GB

实际上当我们把hello.c编译完以后,并不需要这样一个大的GCC环境,一个小的alpine镜像就可以了。

这时候我们就可以使用多阶段构建了。

  1. FROM gcc:9.4 AS builder
  2. COPY hello.c /src/hello.c
  3. WORKDIR /src
  4. RUN gcc --static -o hello hello.c
  5. FROM alpine:3.13.5
  6. COPY --from=builder /src/hello /src/hello
  7. ENTRYPOINT [ "/src/hello" ]
  8. CMD []

测试

  1. $ docker build -t hello-alpine -f Dockerfile-new .
  2. Sending build context to Docker daemon 5.12kB
  3. Step 1/8 : FROM gcc:9.4 AS builder
  4. ---> be1d0d9ce039
  5. Step 2/8 : COPY hello.c /src/hello.c
  6. ---> Using cache
  7. ---> 70a624e3749b
  8. Step 3/8 : WORKDIR /src
  9. ---> Using cache
  10. ---> 24e248c6b27c
  11. Step 4/8 : RUN gcc --static -o hello hello.c
  12. ---> Using cache
  13. ---> db8ae7b42aff
  14. Step 5/8 : FROM alpine:3.13.5
  15. ---> 6dbb9cc54074
  16. Step 6/8 : COPY --from=builder /src/hello /src/hello
  17. ---> Using cache
  18. ---> 18c2bce629fb
  19. Step 7/8 : ENTRYPOINT [ "/src/hello" ]
  20. ---> Using cache
  21. ---> 8dfb9d9d6010
  22. Step 8/8 : CMD []
  23. ---> Using cache
  24. ---> 446baf852214
  25. Successfully built 446baf852214
  26. Successfully tagged hello-alpine:latest
  27. $ docker image ls
  28. REPOSITORY TAG IMAGE ID CREATED SIZE
  29. hello-alpine latest 446baf852214 2 hours ago 6.55MB
  30. hello latest 7cfa0cbe4e2a 2 hours ago 1.14GB
  31. demo latest 079bae887a47 2 hours ago 125MB
  32. gcc 9.4 be1d0d9ce039 9 days ago 1.14GB
  33. $ docker run --rm -it hello-alpine docker
  34. hello docker
  35. $ docker run --rm -it hello-alpine world
  36. hello world
  37. $ docker run --rm -it hello-alpine friends
  38. hello friends
  39. $

可以看到这个镜像非常小,只有6.55MB

Go语言例子

同样的,假如有一个Go的程序,我们想用Docker去做编译,然后执行可执行文件。

  1. package main
  2. import (
  3. "log"
  4. "net/http"
  5. )
  6. func test(w http.ResponseWriter, r *http.Request) {
  7. w.Write([]byte("Hello golang"))
  8. }
  9. func main() {
  10. log.SetFlags(log.LstdFlags | log.Lshortfile)
  11. log.Println("start server on [localhost:8080] ...")
  12. http.HandleFunc("/", test)
  13. err := http.ListenAndServe(":8080", nil)
  14. if err != nil {
  15. log.Fatal(err)
  16. }
  17. }

本地测试(如果你本地有Golang环境)

  1. $ go build
  2. $ ls
  3. app df dockerfile go.mod main.go
  4. $ ./app
  5. 2023/02/15 02:28:18 main.go:14: start server on [localhost:8080] ...

另一个终端

  1. $ curl localhost:8080
  2. Hello golang

构建一个Docker镜像,因为要有Go的环境,所以我们选择golang这个image

  1. FROM golang:alpine3.17 AS builder
  2. COPY main.go /src/app.go
  3. WORKDIR /src
  4. RUN go build app.go
  5. EXPOSE 8080
  6. ENTRYPOINT [ "/src/app" ]

build和测试

  1. $ docker build -t hello-go .
  2. Sending build context to Docker daemon 6.512MB
  3. Step 1/6 : FROM golang:alpine3.17 AS builder
  4. ---> 3257bc8ee9f7
  5. Step 2/6 : COPY main.go /src/app.go
  6. ---> b0156e003e2d
  7. Step 3/6 : WORKDIR /src
  8. ---> Running in 7976422fe214
  9. Removing intermediate container 7976422fe214
  10. ---> 122042396c76
  11. Step 4/6 : RUN go build app.go
  12. ---> Running in f321f6a73147
  13. Removing intermediate container f321f6a73147
  14. ---> 21236778ceee
  15. Step 5/6 : EXPOSE 8080
  16. ---> Running in d47b6e2fb836
  17. Removing intermediate container d47b6e2fb836
  18. ---> 133988261356
  19. Step 6/6 : ENTRYPOINT [ "/src/app" ]
  20. ---> Running in 7f19bd8952b4
  21. Removing intermediate container 7f19bd8952b4
  22. ---> 2ccb4f220a22
  23. Successfully built 2ccb4f220a22
  24. Successfully tagged hello-go:latest
  25. $ docker image ls
  26. REPOSITORY TAG IMAGE ID CREATED SIZE
  27. hello-go latest 2ccb4f220a22 19 minutes ago 321MB
  28. golang alpine3.17 3257bc8ee9f7 3 days ago 254MB
  29. $ docker run -p 8080:8080 -it hello-go
  30. 2023/02/14 18:16:11 app.go:14: start server on [localhost:8080] ...
  1. $ curl localhost:8080
  2. Hello golang

可以看到镜像也很大,321MB,同样的,我们使用多阶段构建。

  1. FROM golang:alpine3.17 AS builder
  2. COPY main.go /src/app.go
  3. WORKDIR /src
  4. RUN go build app.go
  5. FROM alpine:3.17.0
  6. COPY --from=builder /src/app /src/app
  7. EXPOSE 8080
  8. ENTRYPOINT [ "/src/app" ]

测试

  1. $ docker build -t hello-go-alpine -f ./df .
  2. Sending build context to Docker daemon 6.512MB
  3. Step 1/8 : FROM golang:alpine3.17 AS builder
  4. ---> 3257bc8ee9f7
  5. Step 2/8 : COPY main.go /src/app.go
  6. ---> 167672dc57ce
  7. Step 3/8 : WORKDIR /src
  8. ---> Running in a53f0f84c92d
  9. Removing intermediate container a53f0f84c92d
  10. ---> cc8ee771cdbd
  11. Step 4/8 : RUN go build app.go
  12. ---> Running in 9e8e575af675
  13. Removing intermediate container 9e8e575af675
  14. ---> e8e7c7219cd5
  15. Step 5/8 : FROM alpine:3.17.0
  16. 3.17.0: Pulling from library/alpine
  17. c158987b0551: Pull complete
  18. Digest: sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4
  19. Status: Downloaded newer image for alpine:3.17.0
  20. ---> 49176f190c7e
  21. Step 6/8 : COPY --from=builder /src/app /src/app
  22. ---> 8121bedd9a21
  23. Step 7/8 : EXPOSE 8080
  24. ---> Running in 93a02551712d
  25. Removing intermediate container 93a02551712d
  26. ---> e91f0c467511
  27. Step 8/8 : ENTRYPOINT [ "/src/app" ]
  28. ---> Running in aef94175c85d
  29. Removing intermediate container aef94175c85d
  30. ---> f3ee197cba4f
  31. Successfully built f3ee197cba4f
  32. Successfully tagged hello-go-alpine:latest
  33. $ docker image ls
  34. REPOSITORY TAG IMAGE ID CREATED SIZE
  35. hello-go-alpine latest f3ee197cba4f 31 seconds ago 13.6MB
  36. <none> <none> e8e7c7219cd5 46 seconds ago 321MB
  37. hello-go latest 2ccb4f220a22 24 minutes ago 321MB
  38. golang alpine3.17 3257bc8ee9f7 3 days ago 254MB
  39. $ docker run --rm -p 8080:8080 -it hello-go-alpine
  40. 2023/02/14 18:42:29 app.go:14: start server on [localhost:8080] ...

现在镜像只有13.6MB

  1. $ curl localhost:8080
  2. Hello golang

Angular例子