天天看點

關于Dockerfile的最佳實踐技巧

Dockerfile的文法非常簡單,然而如何加快鏡像建構速度,如何減少Docker鏡像的大小卻不是那麼直覺,需要積累實踐經驗。這篇文章可以幫助你快速掌握編寫Dockerfile的技巧。

目标

  • 更快的建構速度
  • 更小的Docker鏡像大小
  • 更少的Docker鏡像層
  • 充分利用鏡像緩存
  • 增加Dockerfile可讀性
  • 讓Docker容器使用起來更簡單

總結

  • 編寫.dockerignore檔案
  • 容器隻運作單個應用
  • 将多個RUN指令合并為一個
  • 基礎鏡像的标簽不要用latest
  • 每個RUN指令後删除多餘檔案
  • 選擇合适的基礎鏡像(alpine版本最好)
  • 設定WORKDIR和CMD
  • 使用ENTRYPOINT (可選)
  • 在entrypoint腳本中使用exec
  • COPY與ADD優先使用前者
  • 合理調整COPY與RUN的順序
  • 設定預設的環境變量,映射端口和資料卷
  • 使用LABEL設定鏡像中繼資料
  • 添加HEALTHCHECK
  • 多階段建構

示例

示例Dockerfile犯了幾乎所有的錯(當然我是故意的)。接下來,我會一步步優化它。假設我們需要使用Docker運作一個Node.js應用,下面就是它的Dockerfile(CMD指令太複雜了,是以我簡化了,它是錯誤的,僅供參考)。

FROM ubuntu
ADD . /app
RUN apt-get update 
RUN apt-get upgrade -y 
RUN apt-get install -y nodejs ssh mysql 
RUN cd /app && npm install
# this should start three processes, mysql and ssh
# in the background and node app in foreground
# isn't it beautifully terrible? <3
CMD mysql & sshd & npm start
           

建構鏡像:

docker build -t wtf .

你能發現上面Dockerfile所有的錯誤嗎? 不能? 那接下來讓我們一步一步完善它。

優化

1. 編寫.dockerignore檔案

建構鏡像時,Docker需要先準備

context

,将所有需要的檔案收集到程序中。預設的

context

包含Dockerfile目錄中的所有檔案,但是實際上,我們并不需要.git目錄,node_modules目錄等内容。

.dockerignore

的作用和文法類似于

.gitignore

,可以忽略一些不需要的檔案,這樣可以有效加快鏡像建構時間,同時減少Docker鏡像的大小。示例如下:

.git/node_modules/

2. 容器隻運作單個應用

從技術角度講,你可以在Docker容器中運作多個程序。你可以将資料庫,前端,後端,ssh,supervisor都運作在同一個Docker容器中。但是,這會讓你非常痛苦:

  • 非常長的建構時間(修改前端之後,整個後端也需要重新建構)
  • 非常大的鏡像大小
  • 多個應用的日志難以處理(不能直接使用stdout,否則多個應用的日志會混合到一起)
  • 橫向擴充時非常浪費資源(不同的應用需要運作的容器數并不相同)
  • 僵屍程序問題 - 你需要選擇合适的init程序

是以,建議大家為每個應用建構單獨的Docker鏡像,然後使用 Docker Compose 運作多個Docker容器。

現在,我從Dockerfile中删除一些不需要的安裝包,另外,SSH可以用docker exec替代。示例如下:

FROM ubuntu
ADD . /app
RUN apt-get update 
RUN apt-get upgrade -y
# we should remove ssh and mysql, and use
# separate container for database 
RUN apt-get install -y nodejs # ssh mysql 
RUN cd /app && npm install
CMD npm start
           

3. 将多個RUN指令合并為一個

Docker鏡像是分層的,下面這些知識點非常重要:

  • Dockerfile中的每個指令都會建立一個新的鏡像層。
  • 鏡像層将被緩存和複用
  • 當Dockerfile的指令修改了,複制的檔案變化了,或者建構鏡像時指定的變量不同了,對應的鏡像層緩存就會失效
  • 某一層的鏡像緩存失效之後,它之後的鏡像層緩存都會失效
  • 鏡像層是不可變的,如果我們再某一層中添加一個檔案,然後在下一層中删除它,則鏡像中依然會包含該檔案(隻是這個檔案在Docker容器中不可見了)。

Docker鏡像類似于洋蔥。它們都有很多層。為了修改内層,則需要将外面的層都删掉。記住這一點的話,其他内容就很好了解了。

現在,我們将所有的RUN指令合并為一個。同時把

apt-get upgrade

删除,因為它會使得鏡像建構非常不确定(我們隻需要依賴基礎鏡像的更新就好了)

FROM ubuntu
ADD . /app
RUN apt-get update \ 
 && apt-get install -y nodejs \
 && cd /app \
 && npm install
CMD npm start
           

記住一點,我們隻能将變化頻率一樣的指令合并在一起。将node.js安裝與npm子產品安裝放在一起的話,則每次修改源代碼,都需要重新安裝node.js,這顯然不合适。是以,正确的寫法是這樣的:

FROM ubuntu
RUN apt-get update && apt-get install -y nodejs 
ADD . /app 
RUN cd /app && npm install
CMD npm start
           

4. 基礎鏡像的标簽不要用latest

當鏡像沒有指定标簽時,将預設使用

latest

标簽。是以,

FROM ubuntu

指令等同于

FROM ubuntu:latest

。當時,當鏡像更新時,latest标簽會指向不同的鏡像,這時建構鏡像有可能失敗。如果你的确需要使用最新版的基礎鏡像,可以使用latest标簽,否則的話,最好指定确定的鏡像标簽。

示例Dockerfile應該使用

16.04

作為标簽。

FROM ubuntu:16.04 # it's that easy!
RUN apt-get update && apt-get install -y nodejs 
ADD . /app 
RUN cd /app && npm install
CMD npm start
           

5. 每個RUN指令後删除多餘檔案

假設我們更新了apt-get源,下載下傳,解壓并安裝了一些軟體包,它們都儲存在

/var/lib/apt/lists/

目錄中。但是,運作應用時Docker鏡像中并不需要這些檔案。我們最好将它們删除,因為它會使Docker鏡像變大。

示例Dockerfile中,我們可以删除

/var/lib/apt/lists/

目錄中的檔案(它們是由apt-get update生成的)。

FROM ubuntu:16.04
RUN apt-get update \ 
 && apt-get install -y nodejs \
# added lines
 && rm -rf /var/lib/apt/lists/*
ADD . /app 
RUN cd /app && npm install
CMD npm start
           

6. 選擇合适的基礎鏡像(alpine版本最好)

在示例中,我們選擇了

ubuntu

作為基礎鏡像。但是我們隻需要運作node程式,有必要使用一個通用的基礎鏡像嗎?

node

鏡像應該是更好的選擇。

FROM node
ADD . /app 
# we don't need to install node 
# anymore and use apt-get
RUN cd /app && npm install
CMD npm start
           

更好的選擇是alpine版本的

node

鏡像。alpine是一個極小化的Linux發行版,隻有4MB,這讓它非常适合作為基礎鏡像。

FROM node:7-alpine
ADD . /app 
RUN cd /app && npm install
CMD npm start
           

apk是Alpine的包管理工具。它與

apt-get

有些不同,但是非常容易上手。另外,它還有一些非常有用的特性,比如

no-cache

--virtual

選項,它們都可以幫助我們減少鏡像的大小。

7. 設定WORKDIR和 CMD

WORKDIR指令可以設定預設目錄,也就是運作

RUN

/

CMD

/

ENTRYPOINT

指令的地方。

CMD指令可以設定容器建立是執行的預設指令。另外,你應該講指令寫在一個數組中,數組中每個元素為指令的每個單詞(參考官方文檔)。

FROM node:7-alpine
WORKDIR /app 
ADD . /app 
RUN npm install
CMD ["npm", "start"]
           

8. 使用ENTRYPOINT (可選)

ENTRYPOINT指令并不是必須的,因為它會增加複雜度。

ENTRYPOINT

是一個腳本,它會預設執行,并且将指定的指令當成參數接收。它通常用于建構可執行的Docker鏡像。entrypoint.sh如下:

#!/usr/bin/env sh_# $0 is a script name, #

2, $3 etc are passed arguments#

1case "$CMD" in "dev" ) npm install export NODE_ENV=development exec npm run dev ;; "start" ) _# we can modify files here, using ENV variables passed in _ # "docker create" command. It can't be done during build process. echo "db: $DATABASE_ADDRESS" >> /app/config.yml export NODE_ENV=production exec npm start ;; * ) _# Run custom command. Thanks to this line we can still use _ # "docker run our_image /bin/bash" and it will work exec

{@:2} ;;esac

示例Dockerfile:

FROM node:7-alpine
WORKDIR /app 
ADD . /app 
RUN npm install
ENTRYPOINT ["./entrypoint.sh"] 
CMD ["start"]
           

可以使用如下指令運作該鏡像:

_# 運作開發版本_docker run our-app dev _# 運作生産版本_docker run our-app start _# 運作bash_docker run -it our-app /bin/bash

9. 在entrypoint腳本中使用exec

在前文的entrypoint腳本中,我使用了

exec

指令運作node應用。不使用

exec

的話,我們則不能順利地關閉容器,因為SIGTERM信号會被bash腳本程序吞沒。

exec

指令啟動的程序可以取代腳本程序,是以所有的信号都會正常工作。

這裡擴充介紹一下docker容器的停止過程:

(1). 對于容器來說,

init

系統不是必須的,當你通過指令

docker stop mycontainer

來停止容器時,docker CLI 會将

TERM

信号發送給 mycontainer 的

PID

為 1 的程序。

  • 如果 PID 1 是 init 程序 - 那麼 PID 1 會将 TERM 信号轉發給子程序,然後子程序開始關閉,最後容器終止。
  • 如果沒有 init 程序- 那麼容器中的應用程序(Dockerfile 中的

    ENTRYPOINT

    CMD

    指定的應用)就是 PID 1,應用程序直接負責響應

    TERM

    信号。這時又分為兩種情況:
    • 應用不處理 SIGTERM - 如果應用沒有監聽

      SIGTERM

      信号,或者應用中沒有實作處理

      SIGTERM

      信号的邏輯,應用就不會停止,容器也不會終止。
    • 容器停止時間很長 - 運作指令

      docker stop mycontainer

      之後,Docker 會等待

      10s

      ,如果

      10s

      後容器還沒有終止,Docker 就會繞過容器應用直接向核心發送

      SIGKILL

      ,核心會強行殺死應用,進而終止容器。

(2).如果容器中的程序沒有收到

SIGTERM

信号,很有可能是因為應用程序不是

PID 1

,PID 1 是

shell

,而應用程序隻是

shell

的子程序。而 shell 不具備

init

系統的功能,也就不會将作業系統的信号轉發到子程序上,這也是容器中的應用沒有收到

SIGTERM

信号的常見原因。

問題的根源就來自

Dockerfile

,例如:

FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
ENTRYPOINT ./popcorn.sh
CMD ["start"]
           

ENTRYPOINT

指令使用的是 **shell 模式**,這樣 Docker 就會把應用放到

shell

中運作,是以

shell

是 PID 1。

解決方案有以下幾種:

方案 1:使用 exec 模式的 ENTRYPOINT 指令

與其使用 shell 模式,不如使用 exec 模式,例如:

FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
ENTRYPOINT ["./popcorn.sh"]
           

這樣 PID 1 就是

./popcorn.sh

,它将負責響應所有發送到容器的信号,至于

./popcorn.sh

是否真的能捕捉到系統信号,那是另一回事。

舉個例子,假設使用上面的 Dockerfile 來建構鏡像,

popcorn.sh

腳本每過一秒列印一次日期:

#!/bin/sh

while true
do
 date
 sleep 1
done
           

建構鏡像并建立容器:

docker build -t truek8s/popcorn .
docker run -it --name corny --rm truek8s/popcorn
           

打開另外一個終端執行停止容器的指令,并計時:

time docker stop corny
           

因為

popcorn.sh

并沒有實作捕獲和處理

SIGTERM

信号的邏輯,是以需要 10s 左右才能停止容器。要想解決這個問題,就要往腳本中添加信号處理代碼,讓它捕獲到

SIGTERM

信号時就終止程序:

#!/bin/sh
# catch the TERM signal and then exit
trap "exit" TERM
while true
do
 date
 sleep 1
done
           

注意:下面這條指令與 shell 模式的 ENTRYPOINT 指令是等效的:

ENTRYPOINT ["/bin/sh", "./popcorn.sh"]
           

方案 2:直接使用 exec 指令

如果你就想使用

shell

模式的 ENTRYPOINT 指令,也不是不可以,隻需将啟動指令追加到

exec

後面即可,例如:

FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
ENTRYPOINT exec ./popcorn.sh
           

這樣

exec

就會将 shell 程序替換為

./popcorn.sh

程序,PID 1 仍然是

./popcorn.sh

方案 3:使用 init 系統

如果容器中的應用預設無法處理

SIGTERM

信号,又不能修改代碼,這時候方案 1 和 2 都行不通了,隻能在容器中添加一個

init

系統。init 系統有很多種,這裡推薦使用 tini,它是專用于容器的輕量級 init 系統,使用方法也很簡單:

  1. 安裝

    tini

  2. tini

    設為容器的預設應用
  3. popcorn.sh

    作為

    tini

    的參數

具體的 Dockerfile 如下:

FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--", "./popcorn.sh"]
           

現在 ``` tini

就是 PID 1,它會将收到的系統信号轉發給子程序 ```
popcorn.sh
           

10. COPY與ADD優先使用前者

COPY指令非常簡單,僅用于将檔案拷貝到鏡像中。ADD相對來講複雜一些,可以用于下載下傳遠端檔案以及解壓壓縮包(參考官方文檔)。

FROM node:7-alpine
WORKDIR /app
COPY . /app 
RUN npm install
ENTRYPOINT ["./entrypoint.sh"] 
CMD ["start"]
           

11. 合理調整COPY與RUN的順序

我們應該把變化最少的部分放在Dockerfile的前面,這樣可以充分利用鏡像緩存。

在建構鏡像的時候,docker 會按照

dockerfile

中的指令順序來一次執行。每一個指令被執行的時候 docker 都會去緩存中檢查是否有已經存在的鏡像可以複用,而不是去建立一個新的鏡像複制。

如果不想使用建構緩存,可以使用

docker build

參數選項

--no-cache=true

來禁用建構緩存。在使用鏡像緩存時,要弄清楚緩存合适生效,何時失效。建構緩存最基本規則如下:

  • 如果引用的父鏡像在建構緩存中,下一個指令将會和所有從該父程序派生的子鏡像做比較,如果有子鏡像使用相同的指令,那麼緩存命中,否則緩存失效。
  • 在大部分情況下,通過比較

    Dockerfile

    中的指令和子鏡像已經足夠了。但是有些指令需要進一步的檢查。
  • 對于

    ADD

    COPY

    指令, 檔案的内容會被檢查,并且會計算每一個檔案的校驗碼。但是檔案最近一次的修改和通路時間不在校驗碼的考慮範圍内。在建構過程中,docker 會比對已經存在的鏡像,隻要有檔案内容和中繼資料發生變動,那麼緩存就會失效。
  • 除了

    ADD

    COPY

    指令,鏡像緩存不會檢查容器中檔案來判斷是否命中緩存。例如,在處理

    RUN apt-get -y update

    指令時,不會檢查容器中的更新檔案以确定是否命中緩存,這種情況下隻會檢查指令字元串是否相同。

示例中,源代碼會經常變化,則每次建構鏡像時都需要重新安裝NPM子產品,這顯然不是我們希望看到的。是以我們可以先拷貝

package.json

,然後安裝NPM子產品,最後才拷貝其餘的源代碼。這樣的話,即使源代碼變化,也不需要重新安裝NPM子產品。

FROM node:7-alpine
WORKDIR /app
COPY package.json /app 
RUN npm install 
COPY . /app
ENTRYPOINT ["./entrypoint.sh"] 
CMD ["start"]
           

同樣舉一反三,Python項目的時候,我們同樣可以先拷貝requerements.txt,然後進行pip install requerements.txt,最後再進行COPY 代碼。

ROM python:3.6
# 建立 app 目錄
WORKDIR /app
# 安裝 app 依賴
COPY src/requirements.txt ./
RUN pip install -r requirements.txt
# 打包 app 源碼
COPY src /app
EXPOSE 8080
CMD [ "python", "server.py" ]
           

## 12. 設定預設的環境變量,映射端口和資料卷 運作Docker容器時很可能需要一些環境變量。在Dockerfile設定預設的環境變量是一種很好的方式。另外,我們應該在Dockerfile中設定映射端口和資料卷。示例如下: ```dockerfile FROM node:7-alpine ENV PROJECT_DIR=/app WORKDIR

PROJECT_DIR RUN npm install COPY .

MEDIA_DIR EXPOSE $APP_PORT ENTRYPOINT ["./entrypoint.sh"] CMD ["start"] ``` [ENV](https://docs.docker.com/engine/reference/builder/#env)指令指定的環境變量在容器中可以使用。如果你隻是需要指定建構鏡像時的變量,你可以使用[ARG](https://docs.docker.com/engine/reference/builder/#arg)指令。

13. 使用LABEL設定鏡像中繼資料

使用LABEL指令,可以為鏡像設定中繼資料,例如鏡像建立者或者鏡像說明。舊版的Dockerfile文法使用MAINTAINER指令指定鏡像建立者,但是它已經被棄用了。有時,一些外部程式需要用到鏡像的中繼資料,例如nvidia-docker需要用到

com.nvidia.volumes.needed

。示例如下:

FROM node:7-alpine 
LABEL maintainer "[email protected]" 
...
           

14. 添加HEALTHCHECK

運作容器時,可以指定

--restart always

選項。這樣的話,容器崩潰時,Docker守護程序(docker daemon)會重新開機容器。對于需要長時間運作的容器,這個選項非常有用。但是,如果容器的确在運作,但是不可(陷入死循環,配置錯誤)用怎麼辦?使用HEALTHCHECK指令可以讓Docker周期性的檢查容器的健康狀況。我們隻需要指定一個指令,如果一切正常的話傳回0,否則傳回1。對HEALTHCHECK感興趣的話,可以參考這篇部落格。示例如下:

FROM node:7-alpine 
LABEL maintainer "[email protected]"
ENV PROJECT_DIR=/app 
WORKDIR $PROJECT_DIR
COPY package.json $PROJECT_DIR 
RUN npm install 
COPY . $PROJECT_DIR
ENV MEDIA_DIR=/media \ 
 NODE_ENV=production \
 APP_PORT=3000
VOLUME $MEDIA_DIR 
EXPOSE $APP_PORT 
HEALTHCHECK CMD curl --fail http://localhost:$APP_PORT || exit 1
ENTRYPOINT ["./entrypoint.sh"] 
CMD ["start"]
           

當請求失敗時,

curl --fail

指令傳回非0狀态。

15. 多階段建構

參考文檔《https://docs.docker.com/develop/develop-images/multistage-build/》

在docker不支援多階段建構的年代,我們建構docker鏡像時通常會采用如下兩種方法:

方法A.将所有的建構過程編寫在同一個Dockerfile中,包括項目及其依賴庫的編譯、測試、打包等流程,可能會有如下問題:

  • - Dockerfile可能會特别臃腫
  • - 鏡像層次特别深
  • - 存在源碼洩露的風險

方法B.事先在外部将項目及其依賴庫編譯測試打包好後,再将其拷貝到建構目錄中執行建構鏡像。

方法B較方法A略顯優雅一些,而且可以很好地規避方法A存在的風險點,但仍需要我們編寫兩套或多套Dockerfile或者一些腳本才能将其兩個階段自動整合起來,例如有多個項目彼此關聯和依賴,就需要我們維護多個Dockerfile,或者需要編寫更複雜的腳本,導緻後期維護成本很高。

為解決以上問題,**Docker v17.05 開始支援多階段建構 (multistage builds)**。使用多階段建構我們就可以很容易解決前面提到的問題,并且隻需要編寫一個 Dockerfile。

你可以在一個 Dockerfile 中使用多個 FROM 語句。每個 FROM 指令都可以使用不同的基礎鏡像,并表示開始一個新的建構階段。你可以很友善的将一個階段的檔案複制到另外一個階段,在最終的鏡像中保留下你需要的内容即可。

預設情況下,建構階段是沒有指令的,我們可以通過它們的索引來引用它們,第一個 FROM 指令從0開始,我們也可以用AS指令為建構階段命名。

案例1

FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]
           

通過

docker build

建構後,最終結果是産生與之前相同大小的 Image,但複雜性顯著降低。您不需要建立任何中間 Image,也不需要将任何編譯結果臨時提取到本地系統。

哪它是如何工作的呢?關鍵就在

COPY --from=0

這個指令上。Dockerfile 中第二個 FROM 指令以 alpine:latest 為基礎鏡像開始了一個新的建構階段,并通過

COPY --from=0

僅将前一階段的建構檔案複制到此階段。前一建構階段中産生的 Go SDK 和任何中間層都會在此階段中被舍棄,而不是儲存在最終 Image 中。

使用多階段建構一個python應用。

案例2

預設情況下,建構階段是未命名的。您可以通過一個整數值來引用它們,預設是從第 0 個 FROM 指令開始的。 為了友善管理,您也可以通過向 FROM 指令添加 as NAME 來命名您的各個建構階段。下面的示例就通過命名各個建構階段并在 COPY 指令中使用名稱來通路指定的建構階段。

這樣做的好處就是即使稍後重新排序 Dockerfile 中的指令,COPY 指令一樣能找到對應的建構階段。

FROM golang:1.7.3 as builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]
           

案例3

停在特定的建構階段

建構鏡像時,不一定需要建構整個 Dockerfile 中每個階段,您也可以指定需要建構的階段。比如:您隻建構 Dockerfile 中名為 builder 的階段

$ docker build --target builder -t alexellis2/href-counter:latest .
           

此功能适合以下場景:

  • 調試特定的建構階段。
  • 在 Debug 階段,啟用所有程式調試模式或調試工具,而在生産階段盡量精簡。
  • 在 Testing 階段,您的應用程式使用測試資料,但在生産階段則使用生産資料。

案例4

使用外部鏡像作為建構階段

使用多階段建構時,您不僅可以從 Dockerfile 中建立的鏡像中進行複制。您還可以使用

COPY --from

指令從單獨的 Image 中複制,支援使用本地 Image 名稱、本地或 Docker 注冊中心可用的标記或标記 ID。

COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
           

案例5

把前一個階段作為一個新的階段

在使用 FROM 指令時,您可以通過引用前一階段停止的地方來繼續。同樣,采用此方式也可以友善一個團隊中的不同角色,如何使用類似流水線的方式,一級一級提供基礎鏡像,同樣更友善快速的複用團隊其他人的基礎鏡像。例如:

FROM alpine:latest as builder
RUN apk --no-cache add build-base
FROM builder as build1
COPY source1.cpp source.cpp
RUN g++ -o /binary source.cpp
FROM builder as build2
COPY source2.cpp source.cpp
RUN g++ -o /binary source.cpp
           
# ---- 基礎 python 鏡像 ----
FROM python:3.6 AS base
# 建立 app 目錄
WORKDIR /app
# ---- 依賴 ----
FROM base AS dependencies 
COPY gunicorn_app/requirements.txt ./
# 安裝 app 依賴
RUN pip install -r requirements.txt
# ---- 複制檔案并 build ----
FROM dependencies AS build 
WORKDIR /app
COPY . /app
# 在需要時進行 Build 或 Compile
# --- 使用 Alpine 釋出 ----
FROM python:3.6-alpine3.7 AS release 
# 建立 app 目錄
WORKDIR /app
COPY --from=dependencies /app/requirements.txt ./
COPY --from=dependencies /root/.cache /root/.cache
# 安裝 app 依賴
RUN pip install -r requirements.txt
COPY --from=build /app/ ./
CMD ["gunicorn", "--config", "./gunicorn_app/conf/gunicorn_config.py", "gunicorn_app:app"]

           

公衆号:運維開發故事

github:https://github.com/orgs/sunsharing-note/dashboard

部落格:https://www.devopstory.cn

愛生活,愛運維

我是冬子先生,《運維開發故事》公衆号團隊中的一員,一線運維農民工,雲原生實踐者,這裡不僅有硬核的技術幹貨,還有我們對技術的思考和感悟,歡迎關注我們的公衆号,期待和你一起成長!

關注我,不定期維護優質内容

溫馨提示

如果我的文章對你有所幫助,還請幫忙一下,你的支援會激勵我輸出更高品質的文章,非常感謝!

你還可以把我的公衆号設為「星标」,這樣當公衆号文章更新時,你會在第一時間收到推送消息,避免錯過我的文章更新。

........................