Docker 系統性入門+進階實踐-04Dockerfile完全指南
第四章-Dockerfile完全指南
- 如何選擇基礎映象
基本原則:
- 官方映象優於非官方映象,如果沒有官方映象,則儘量選擇Dockerfile開源的;
- 固定版本tag,而不是每次都使用最新版本latest
- 儘量選擇體積小的映象
- build一個nginx映象
Dockerfile檔案:
FROM nginx:stable
ADD index.html /usr/share/nginx/html/index.html
index.html檔案:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>哈哈哈,這是我的第一個Dockerfile建立的映象</> </body> </html>
使用Dockerfile構建映象sudo docker image build -t 1341935531/index-nginx:2.0 .
通過新建的映象建立並執行容器sudo docker run -d -p 80:80 --name index_nginx image_id
通過RUN執行指令
- RUN主要用於在image裡執行指令,比如安裝軟體、下載檔案等,例如
FROM ubuntu:21.04 RUN apt-get update RUN apt-get install -y wget RUN wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz RUN tar -xzvf ipinfo_2.0.1_linux_amd64.tar.gz RUN mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo RUN rm -rf ipinfo_2.0.1_linux_amd64.tar.gz
通過Dockerfile生成映象檔案sudo docker image build -t run_dockerfile:1.0 .
檢視映象和映象對應的分層
可以看到,每個RUN命令,都給映象增加了一層,使得映象非常的臃腫,所以我們需要優化生成映象的命令,
- 將多個RUN命令改為一個RUN命令
FROM ubuntu:21.04 RUN apt-get update && \ apt-get install -y wget && \ wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz && \ tar -xzvf ipinfo_2.0.1_linux_amd64.tar.gz && \ mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo && \ rm -rf ipinfo_2.0.1_linux_amd64.tar.gz
通過Dockerfile.good建立映象sudo docker image build -t run_dockerfile:2.0 -f Dockerfile.good .
在檢視新建立的映象的分層,明顯可以看出,比之前層數少了很多,而且size也變小了
檔案的複製和目錄操作
- 往映象裡複製檔案有兩種方式,ADD和COPY,我們來看一下兩者的不同
COPY和ADD都可以把local的一個檔案複製到映象裡,如果目標目錄不存在,則會自動建立 - COPY複製普通檔案
FROM python:3.9.13-alpine3.14
COPY hello.py /app/
CMD ["python", "/app/hello.py"]
比如把本地的hello.py複製到/app/目錄下,如果/app/這個目錄不存在,則會自動建立
注意: 不能這樣寫/app, 而是要這樣寫/app/,否則會報錯,找不到
3. ADD複製壓縮檔案
ADD比COPY高階一點的地方就是,如果複製的是一個gzip等壓縮檔案時,ADD會幫助我們自動解壓縮檔案
FROM python:3.9.13-alpine3.14
ADD hello.tar.gz /app/
CMD ["python", "/app/hello.py"]
- 如何選擇
因此在COPY和ADD指令中選擇的時候,可以遵循這樣的原則,所有檔案複製均使用COPY指令,僅在需要自動解壓的場合使用ADD - 也可以將一個目錄打包複製到映象裡面去
FROM python:3.9.13-alpine3.14
ADD app.tar.gz /
CMD ["python3", "/app/hello.py"]
- 目錄變更的語法命令:WORKDIR
WORKDIR比cd的好處是,如果沒有這個目錄,WORKDIR會幫助我們自動建立這個目錄
FROM python:3.9.13-alpine3.14
WORKDIR /app/
ADD app.tar.gz ./
CMD ["python3", "./app/hello.py"]
通過Dockerfile構建機映象sudo docker image build -t python_test:7.0 .
通過構建好的映象建立並啟動容器測試sudo docker container run --rm image_id
構建引數和環境變數(ARG VS ENV)
- ARG和ENV是經常容易被混淆的兩個Dockerfile的語法,都可以用來設定一個變數,但實際上兩者有很多的不同
FROM ubuntu:21.04
ARG VERSION=2.0.1
RUN apt-get update && \
apt-get install -y wget && \
wget https://github.com/ipinfo/cli/releases/download/ipinfo-${VERSION}/ipinfo_${VERSION}_linux_amd64.tar.gz && \
tar zxf ipinfo_${VERSION}_linux_amd64.tar.gz && \
mv ipinfo_${VERSION}_linux_amd64 /usr/bin/ipinfo && \
rm -rf ipinfo_${VERSION}_linux_amd64.tar.gz
通過dockerfile建立映象sudo docker image build -f Dockerfile-arg -t ipinfo-arg .
通過構建的映象建立容器並進入容器sudo docker container -it image_id
檢視ipinfo的版本ipinfo version
Dockerfile-env檔案:
FROM ubuntu:21.04
ENV VERSION=2.0.1
RUN apt-get update && \
apt-get install -y wget && \
wget https://github.com/ipinfo/cli/releases/download/ipinfo-${VERSION}/ipinfo_${VERSION}_linux_amd64.tar.gz && \
tar zxf ipinfo_${VERSION}_linux_amd64.tar.gz && \
mv ipinfo_${VERSION}_linux_amd64 /usr/bin/ipinfo && \
rm -rf ipinfo_${VERSION}_linux_amd64.tar.gz
操作步驟同Dockerfile-arg
2. ARG和ENV的區別?
- ARG只用於構建映象的時候使用,構建完成之後,建立容器的時候,容器裡面是無法使用ARG的變數的
- ENV不僅可用於構建映象的時候使用,建立容器的時候也可以使用ENV變數,ENV變數會永久的儲存在映象裡面
- ARG可以在構建映象的時候動態修改value,通過--build-value
sudo docker image build -f Dockerfile-arg -t ipinfo-arg-new --build-arg VERSION=2.0.0 .
- 何時使用ARG和ENV
當我們僅僅使用變數取構建映象的時候,我們就使用ARG,當我們不僅僅是用變數去構建映象,還需在容器中去使用該變數時,就需要使用ENV了
容器啟動命令 CMD
- CMD可以用來設定容器啟動時預設會執行的命令
- 容器啟動時預設執行的命令
- 如果docker container run啟動時指定了其它命令,則CMD命令會被忽略
- 如果定義了多個CMD,只有最後一個會被執行
第二個如果執行容器的時候指定了命令,CMD命令就會被忽略,啥意思呢,我們指定個命令看一下:sudo docker container run -it ipinfo-env ipinfo version
此時建立的容器只會顯示一下ipinfo的版本,並不會進入容器了。
- 我們重新定義Dockerfile, 自定義CMD覆蓋ubuntu:21.04原始映象中自帶的CMD
FROM ubuntu:21.04
ENV VERSION=2.0.1
RUN apt-get update && \
apt-get install -y wget && \
wget https://github.com/ipinfo/cli/releases/download/ipinfo-${VERSION}/ipinfo_${VERSION}_linux_amd64.tar.gz && \
tar zxf ipinfo_${VERSION}_linux_amd64.tar.gz && \
mv ipinfo_${VERSION}_linux_amd64 /usr/bin/ipinfo && \
rm -rf ipinfo_${VERSION}_linux_amd64.tar.gz
CMD ["ipinfo", "v"]
通過Dockerfile構建映象sudo docker image build -t ipinfo-cmd .
通過映象構建容器sudo docker container run --rm -it image_id
此時不會進入容器,只會列印ipinfo的版本,因為我們自定的CMD覆蓋了原有的
容器啟動命令 ENTRYPOINT
- ENTRYPOINT也可以設定容器啟動時要執行的命令,但是和CMD是有區別的
CMD設定的命令,可以在docker container run啟動時傳入其它命令,覆蓋掉CMD命令,
但是ENTRYPOINT所設定的命令是一定會被執行的,
ENTRYPOINT和CMD可以聯合使用,ENTRYPOINT設定執行的命令,CMD傳遞引數
Dockerfile-cmd檔案:
FROM ubuntu:21.04
CMD ["echo", "hello docker"]
Dockerfile-entrypoint檔案:
FROM ubuntu:21.04
ENTRYPOINT ["echo", "hello docker"]
用這兩個Dockerfile分別構建映象,執行容器時額外指定命令看效果:
可以看出,執行容器時如果不指定命令,兩者沒啥區別,如果指定了命令,CMD命令會被覆蓋,而ENTRYPOINT命令不會被覆蓋。
- ENTRYPOINT可以和CMD聯合使用,如下
FROM ubuntu:21.04
ENTRYPOINT ["echo"]
CMD ["hello", "world"]
建立映象sudo docker image build -f Dockerfile-entrypoint-cmd -t ubuntu-entrypoint-cmd .
執行容器,通過ENTRYPOINT執行命令,通過CMD指定引數:sudo docker container run --rm -it image_id [hello docker]
注意:如果不傳入CMD引數,那麼會列印hello world, 如果傳入了CMD引數,那麼會把預設的CMD命令覆蓋掉,列印hello docker
shell格式和exec格式
- CMD和ENTRYPOINT同時支援shell格式和exec格式
- shell格式
CMD echo "hello world"
ENTRYPOINT echo "hello world"
- Exec格式
CMD ["echo", "hello world"]
ENTRYPOINT ["echo" "hello world"]
- 注意shell指令碼的問題
FROM ubuntu:21.04
ENV NAME=docker
CMD echo "hello $NAME"
上面這些是沒問題的,加入我們把CMD改成Exec格式,是不行的,結果列印:hello $NAME
那麼Exec格式需要怎麼寫才能使用ENV設定的環境變數呢,我們需要以shell指令碼的方式去執行
FROM ubuntu:21.04
ENV NAME=docker
CMD ["/bin/sh", "-c", "echo hello $NAME"]
或這樣
FROM ubuntu:21.04
ENV NAME=docker
CMD ["sh", "-c", "echo hello $NAME"]
建立映象,執行容器,執行成功。
Dockerfile中的RUN命令升級版
由於之前RUN命令後面可能會跟有很多的命令,幾十行甚至上百行,所以我們把這些命令寫到一個指令碼檔案中,
我們直接 RUN bash xxx.sh檔案即可。例如:
Dockerfile檔案:
FROM ubuntu:21.04
WORKDIR /usr/src/app/
COPY run.sh run.sh
RUN bash run.sh
CMD ["ipinfo", "v"]
run.sh檔案:
#!/usr/bin/env bash
apt-get update
apt-get install -y wget
wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz
tar -xzvf ipinfo_2.0.1_linux_amd64.tar.gz
mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo
rm -rf ipinfo_2.0.1_linux_amd64.tar.gz
建立映象sudo docker image build -t image_name .
執行容器sudo docker run --rm -it image_id
一起構建一個python flask映象
- 目錄結構
- Dockerfile檔案:
FROM python:3.9-slim
WORKDIR /usr/src/app
COPY app /usr/src/app
RUN bash run.sh
ENTRYPOINT ["flask", "run"]
CMD ["-h", "0.0.0.0", "-p", "5000"]
注意:這裡使用了ENTRYPOINT+CMD是因為,我們可以自己指定ip+埠來覆蓋CMD命令,ENTRYPOINT是覆蓋不了的。
2. run.sh檔案
#!/usr/bin/env bash
pip install --no-cache-dir -U -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
- app.py檔案
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "hello python"
3.1 requirements.txt檔案
flask
- 通過Dockerfile構建映象
sudo docker image build -t flask-demo .
- 通過映象建立並啟動容器
sudo docker container run -it -p 8008:5000 flask-demo
Dockerfile使用技巧,合理使用快取
在檔案沒有任何改動的同時去重新構建映象的話,就會完全使用快取sudo docker image build -t flask-demo .
結果:
我們的Dockerfile是這樣的:
FROM python:3.9-slim
WORKDIR /usr/src/app
COPY app /usr/src/app
RUN bash run.sh
ENTRYPOINT ["flask", "run"]
CMD ["-h", "0.0.0.0", "-p", "5000"]
當app目錄中有任何一點改動的COPY層以及後面所有的層都會重新構建,浪費時間,
為了解決這個問題,我們可以把經常需要改動的放在映象層的後面,當然我們要注意移動的層後面的沒有依賴前面的層,這樣移動才能正常。
總結:我們要合理使用Dockerfile的快取,我們要把經常要改變的檔案目錄往Dockerfile後面放。
Dockerfile技巧-dockerignore
我們的程式碼僅僅有幾kb,但是Dockerfile同級目錄有其它檔案目錄較大,所以構建會特別慢,我們需要把這樣的目錄忽略掉,
那麼就需要使用到.dockerignore檔案,在Dockerfile同級目錄下建立.dockerignore檔案
.vscode/
env/
test/
這樣構建映象時就會把這三個目錄都忽略掉。
兩個作用:一個是減小我們映象的體積,另一個是保護我們的私密檔案
Dockerfile技巧-多階段構建-C語言
- Dockerfile檔案:
FROM gcc:9.4 AS builder
COPY hello.c /usr/src/app/hello.c
WORKDIR /usr/src/app
RUN gcc --static -o hello hello.c
FROM alpine:3.13.5
COPY --from=builder /usr/src/app/hello /usr/src/app/hello
ENTRYPOINT ["/usr/src/app/hello"]
CMD []
- hello.c檔案
#include <stdio.h>
void main(int argc, char *argv[])
{
printf("hello %s\n", argv[argc - 1]);
}
這樣構建映象分為兩個階段的目的是:第一個階段需要gcc環境編譯c程式碼為可執行檔案,
第二個階段是因為gcc映象太大了,而我們執行編譯好的檔案又不需要gcc環境,僅需要一個小點的linux環境即可,
使得映象變得非常的小,同時也不影響編譯好的程式碼的執行。
Dockerfile技巧-多階段構建-golang
- 多階段構建前:
Dockerfile檔案
FROM golang:1.18
COPY main.go /usr/src/app/
WORKDIR /usr/src/app
RUN go build -o httpserver main.go
ENTRYPOINT ["/usr/src/app/httpserver"]
main.go檔案:
package main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/index", func(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write([]byte("hello index"))
})
err := http.ListenAndServe("0.0.0.0:8008", mux)
if err != nil {
log.Fatalln(err.Error())
}
}
構建映象:sudo docker image build -t go-demo .
構建好的映象大小:佔了971MB,
REPOSITORY TAG IMAGE ID CREATED SIZE
go-demo latest 85a4e8f7dcc9 4 minutes ago 971MB
2. 多階段構建後,main.go檔案不變
Dockerfile檔案:
FROM golang:1.18 AS builder
COPY . /usr/src/app/
WORKDIR /usr/src/app
RUN bash run.sh
FROM alpine:3.16.0
COPY --from=builder /usr/src/app/httpserver /usr/src/app/httpserver
ENTRYPOINT ["/usr/src/app/httpserver"]
run.sh檔案
#!/usr/bin/env bash
go env -w CGO_ENABLED=0
go build -o httpserver main.go
構建映象:sudo docker image build -t go-alpine .
啟動容器:sudo docker container run --rm -it -p 8008:8008 go-alpine
檢視映象大小,可以看到映象10幾兆就搞定了,非常節約空間。