0%

docker 系列 - (3) Dockerfile完全指南

1. 如何選擇基礎鏡像(FROM)

1.1. 基本原則

  • 官方鏡像優非官方的鏡像,如果沒有官方鏡像,則儘量選擇Dockerfile開源的
  • 固定版本tag而不是每次都使用latest
  • 儘量選擇體積小的鏡像

準備一個index.html文件

1
<h1>Hello Docker</h1>

準備一個Dockerfile文件

1
2
FROM nginx:1.21.0-alpine
ADD index.html /usr/share/nginx/html/index.html
1
2
3
4
5
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
bitnami/nginx 1.18.0 dfe237636dde 28 minutes ago 89.3MB
nginx 1.21.0-alpine a6eb2a334a9f 2 days ago 22.6MB
nginx 1.21.0 d1a364dc548d 2 days ago 133MB

alpine 這個版本是一個非常輕量的linux 系統,在nginx、python都會看得到這樣的組合

image-20220218151904995

A minimal Docker image based on Alpine Linux with a complete package index and only 5 MB in size!

image-20220218152341367

2. 通過RUN執行指令

RUN 主要用於在image裡執行指令,比如安裝軟體、下載文件…等。

如果是了6個RUN就會建立出6個分層,通常不會這樣寫,而且體積也會比較大。

原指令

1
2
3
4
5
6
$ apt-get update
$ apt-get install wget
$ wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz
$ tar zxf 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

2.1. Dockerfile.bad

1
2
3
4
5
6
7
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 zxf 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

鏡像的大小和分層

我們可以看到下面的history 多了好多個RUN ,每一個RUN都有佔幾個MB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#建立鏡像檔 -f 參數可以指定dockerfile檔案
docker image build -f Dockerfile.bad -t ipinfo-bad .

$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ipinfo latest 97bb429363fb 4 minutes ago 138MB
ubuntu 21.04 478aa0080b60 4 days ago 74.1MB

$ docker image history 97b
IMAGE CREATED CREATED BY SIZE COMMENT
97bb429363fb 4 minutes ago RUN /bin/sh -c rm -rf ipinfo_2.0.1_linux_amd… 0B buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c mv ipinfo_2.0.1_linux_amd64 /… 9.36MB buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c tar zxf ipinfo_2.0.1_linux_am… 9.36MB buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c wget https://github.com/ipinf… 4.85MB buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c apt-get install -y wget # bui… 7.58MB buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c apt-get update # buildkit 33MB buildkit.dockerfile.v0
<missing> 4 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 4 days ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 4 days ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 4 days ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 811B
<missing> 4 days ago /bin/sh -c #(nop) ADD file:d6b6ba642344138dc… 74.1MB

2.2. Dockerfile.good

1
2
3
4
5
6
7
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 zxf 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

我們可以看到下面的結果,只有一筆RUN的分層

1
2
3
4
5
6
7
8
9
10
11
12
13
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
ipinfo-new latest fe551bc26b92 5 seconds ago 124MB
ipinfo latest 97bb429363fb 16 minutes ago 138MB
ubuntu 21.04 478aa0080b60 4 days ago 74.1MB
$ docker image history fe5
IMAGE CREATED CREATED BY SIZE COMMENT
fe551bc26b92 16 seconds ago RUN /bin/sh -c apt-get update && apt-get… 49.9MB buildkit.dockerfile.v0
<missing> 4 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 4 days ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 4 days ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 4 days ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 811B
<missing> 4 days ago /bin/sh -c #(nop) ADD file:d6b6ba642344138dc… 74.1MB

3. 文件複製和目錄操作(ADD,COPY,WORKDIR)

往鏡像裡複製文件有兩種方式COPYADD

3.1. 複製普通文件

COPYADD都可以把local的一個文件複製到鏡像裡,如果目標目錄不存在,則會自動建立

1
2
FROM python:3.9.5-alpine3.13
COPY hello.py /app/hello.py

比如把本地的hello.py複製到/app 目錄下,但app目錄不存在,所以會自動建立

3.2. 複製壓縮文件

ADD比較不一樣的地方是,如果複製的是一個gzip等壓縮文件時,ADD會幫助我們自動去解壓縮文件

1
2
FROM python:3.9.5-alpine3.13
ADD hello.tar.gz /app/

3.3. 如何選擇

所有文件複製可以均使用COPY指令,僅在需要自動解壓縮的場合使用ADD

3.4. 實驗記錄 COPY

1
2
3
4
5
6
7
8
# 建立tmp資料夾
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# mkdir tmp
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# cd tmp
# 建a文件
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder/tmp# touch a
# 建b文件
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder/tmp# touch b
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder/tmp# cd ..

修改Dockerfile檔案內容如下

1
2
FROM python:3.9.5-alpine3.13
COPY tmp/ /app/tmp/

執行build動作,並進入到container查看是否有複製完整資料夾至/app/tmp/底下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#打包成docker image
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# docker build -t docker-copy .
Sending build context to Docker daemon 878.1kB
Step 1/2 : FROM python:3.9.5-alpine3.13
---> 46a196bf50ae
Step 2/2 : COPY tmp/ /app/tmp/
---> 54edba980fe5
Successfully built 54edba980fe5
Successfully tagged docker-copy:latest

#執行容器 並進入交互模式 結束後直接刪除container
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# docker run -it --rm docker-copy sh

#切換目錄至tmp底下
/ # cd app/tmp

#查看資料夾,的確有a、b兩個檔案,代表有複製到整個資料夾到container內部
/app/tmp # ls
a b

#離開container
/app/tmp # exit

3.5. 實驗記錄 ADD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#執行tar指令壓縮資料夾
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# tar cvf copy.tar tmp
tmp/
tmp/b
tmp/a

#查看清單
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# ls
copy.tar Dockerfile hello hello.c hello.py tmp
root@kite-virtual-machine:/home/kite/Desktop/genDockerIm

#先移除上一個剛產生的image
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# docker image rm docker-copy
Untagged: docker-copy:latest
Deleted: sha256:54edba980fe511c2cfce44ce4e7615fd47171deea2a8cd668404b6f59545405c
Deleted: sha256:6cff68147576a72f2121ea1da50b33ccc8328fc11683b668564baed2c3b7dbc6

修改Dockerfile檔案內容如下

1
2
FROM python:3.9.5-alpine3.13
ADD copy.tar /app/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#再進行構建
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# docker build -t docker-copy .
Sending build context to Docker daemon 888.8kB
Step 1/2 : FROM python:3.9.5-alpine3.13
---> 46a196bf50ae
Step 2/2 : ADD copy.tar /app/
---> 9f31129139ea
Successfully built 9f31129139ea
Successfully tagged docker-copy:latest

#運行容器並進入交互模式
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# docker run -it --rm docker-copy sh

#移動到tmp資料夾
/ # cd app/tmp

#查看清單,果然有自動unzip資料夾
/app/tmp # ls
a b

4. 構建參數和環境變數(ARG VS ENV)

ARGENV是經常容易被混淆的兩個Dockerfile的語法,都可以用來設置一個”變數”。但實際上兩者有很多的不同。

比方說有一個腳本

1
2
3
4
5
6
7
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 zxf 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

4.1. 使用ENV

1
2
3
4
5
6
7
8
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

4.2. 使用ARG

1
2
3
4
5
6
7
8
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

4.3. 區別

docker-image

4.3.1. ARG

  • ARG的生命週期在build 鏡像的過程

  • run container 內並不會有這組變數名稱可以使用

  • 可以藉由build 指令去指定賦與值

    1
    docker image build -f .\Dockerfile-arg -t ipinfo-arg-2.0.0 --build-arg VERSION=2.0.0 .

4.3.2. ENV

  • ENV的生命週期包含在build鏡像與run container 之後,變數仍存在。

5. CMD容器啟動命令

CMD可以用來設置容器啟動時預設會執行的命令。

我們的dockerfile檔如下

1
2
3
4
5
6
7
8
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

進行鏡像構建

1
docker image build -f dockerfile-ipinfo -t ipinfo .

5.1. 容器啟動時預設執行的命令

啟動一個ipinfo container

1
2
3
4
5
#啟動一個新容器
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# docker container run -it ipinfo

#自動進入到容器內部了
root@ecfe534f0f60:/# ls

我們發現我們沒有在ipinfo 後面下command 為什麼會自動進入到shell模式呢?我們使用docker image history ipinfo 看一下

1
2
3
4
5
6
7
#查看ipinfo
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# docker image history ipinfo
IMAGE CREATED CREATED BY SIZE COMMENT
f68f17d7369f 27 minutes ago /bin/sh -c apt-get update && apt-get ins… 47MB
53a4d53f91b3 29 minutes ago /bin/sh -c #(nop) ENV VERSION=2.0.1 0B
7cc39f89fa58 2 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ADD file:4cb90f4b06e581fee… 80MB

發現docker 的基礎鏡像中有CMD["bash"],所以就會變成預設是在這個模式下。

5.2. 如果docker container run啟動容器時指定了其它命令,則CMD命令會被忽略。

1
2
3
4
5
6
7
8
9
10
11
12
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# docker run -it --rm ipinfo ipinfo 8.8.8.8
Core
- IP 8.8.8.8
- Anycast true
- Hostname dns.google
- City Mountain View
- Region California
- Country United States (US)
- Location 37.4056,-122.0775
- Organization AS15169 Google LLC
- Postal 94043
- Timezone America/Los_Angeles

5.3. 如果定義了多個CMD,只有最後一個會被執行。

以下腳本為例,參考了ubuntu的鏡像,ubuntu本身內部就有使用其它CMD,這時我們如果在最後多加了一個CMD的指令。那優先權最大就會變成

CMD ["ipinfo"]

1
2
3
4
5
6
7
8
9
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"]

迅速清理那些已經停止的container

1
docker system prune -f

遽速清理沒有使用的image

1
docker image prune -a

5.4. 小技巧

1
2
3
4
5
6
7
8
9
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 []

CMD[]設為空之後,再重新build一個新的鏡像,這樣子在輸入docker run --it ipinfo 時,就不會通過,要求使用者一定要在後面再接一個參數,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#創建容器,出現錯誤提示
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# docker run -it ipinfo
docker: Error response from daemon: No command specified.
See 'docker run --help'.

#帶參數
root@kite-virtual-machine:/home/kite/Desktop/genDockerImagesFolder# docker run -it ipinfo ipinfo 8.8.8.8
Core
- IP 8.8.8.8
- Anycast true
- Hostname dns.google
- City Mountain View
- Region California
- Country United States (US)
- Location 37.4056,-122.0775
- Organization AS15169 Google LLC
- Postal 94043
- Timezone America/Los_Angeles

6. 容器啟動命令ENTRYPOINT

ENTRYPOINT也可以設置容器啟動時要執行的命令,但是和CMD是有區別的。

  • CMD設置的命令,可以在docker container run 時傳入其它命令,覆蓋掉CMD的命令,但是ENTRYPOINT所設置的命令是一定會被執行的。
  • ENTRYPOINTCMD可以聯合使用,ENTRYPOINT設置執行的命令,CMD傳遞參數

7. SHELL格式和EXEC格式

CMDENTRYPOINT同時支持SHELL格式和EXEC格式。

7.1. shell格式

1
CMD echo "hello docker"
1
ENTRYPOINT echo "hello docker"

7.2. exec格式

1
ENTRYPOINT ["echo", "hello docker"]
1
CMD ["echo", "hello docker"]

7.3. 注意事項舉例

比方說要print 出hello docker

shell版本

1
2
3
FROM ubuntu:21.04
ENV NAME=docker
CMD echo "hello $NAME"

我們試著改成exec格式,如下面這樣,其實是不行的喔。

1
2
3
FROM ubuntu:21.04
ENV NAME=docker
CMD ["echo", "hello $NAME"]

具體方式要以shell腳本的方式去執行

1
2
3
FROM ubuntu:21.04
ENV NAME=docker
CMD ["sh", "-c", "echo hello $NAME"]

8. 一起構建一個python Flask鏡像

8.1. 環境準備

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#建立一個資料夾名稱為pythonworkspace
kite@kite-virtual-machine:~/Desktop$ mkdir pythonworkspace

#進入pythonworkspace資料夾
kite@kite-virtual-machine:~/Desktop$ cd pythonworkspace/

#安裝python3-virtualenv套件
kite@kite-virtual-machine:~/Desktop/pythonworkspace$ sudo apt install python3-virtualenv
Reading package lists... Done
Building dependency tree
Reading state information... Done
python3-virtualenv is already the newest version (20.0.17-1ubuntu0.4).
The following packages were automatically installed and are no longer required:
libc++1 libc++1-10 libc++abi1-10 libsss-nss-idmap0
Use 'sudo apt autoremove' to remove them.
0 upgraded, 0 newly installed, 0 to remove and 36 not upgraded.

#建立一個獨立空間的python環境,名稱為flaskwebsite
kite@kite-virtual-machine:~/Desktop/pythonworkspace$ python3 -m venv flaskwebsite

#執行進入到flaskwebsite空間
kite@kite-virtual-machine:~/Desktop/pythonworkspace$ source ./flaskwebsite/bin/activate

#發現前面就會帶(flaskwebsite)空間名稱囉,再用pip list看目前套件清單
(flaskwebsite) kite@kite-virtual-machine:~/Desktop/pythonworkspace$ pip list
Package Version
------------- -------
pip 20.0.2
pkg-resources 0.0.0
setuptools 44.0.0

#安裝flask套件
(flaskwebsite) kite@kite-virtual-machine:~/Desktop/pythonworkspace$ pip install Flask

Collecting Flask
Downloading Flask-2.0.3-py3-none-any.whl (95 kB)
|████████████████████████████████| 95 kB 393 kB/s
Collecting Jinja2>=3.0
Downloading Jinja2-3.0.3-py3-none-any.whl (133 kB)
|████████████████████████████████| 133 kB 1.2 MB/s
Collecting Werkzeug>=2.0
Downloading Werkzeug-2.0.3-py3-none-any.whl (289 kB)
|████████████████████████████████| 289 kB 2.6 MB/s
Collecting itsdangerous>=2.0
Downloading itsdangerous-2.1.0-py3-none-any.whl (15 kB)
Collecting click>=7.1.2
Downloading click-8.0.4-py3-none-any.whl (97 kB)
|████████████████████████████████| 97 kB 3.4 MB/s
Collecting MarkupSafe>=2.0
Downloading MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (25 kB)
Installing collected packages: MarkupSafe, Jinja2, Werkzeug, itsdangerous, click, Flask
Successfully installed Flask-2.0.3 Jinja2-3.0.3 MarkupSafe-2.1.0 Werkzeug-2.0.3 click-8.0.4 itsdangerous-2.1.0

#再次確認套件清單
(flaskwebsite) kite@kite-virtual-machine:~/Desktop/pythonworkspace$ pip list
Package Version
------------- -------
click 8.0.4
Flask 2.0.3
itsdangerous 2.1.0
Jinja2 3.0.3
MarkupSafe 2.1.0
pip 20.0.2
pkg-resources 0.0.0
setuptools 44.0.0
Werkzeug 2.0.3

#執行vscode 打開當前目錄
(flaskwebsite) kite@kite-virtual-machine:~/Desktop/pythonworkspace$code .

#設定環境變數,這步驟設定後,之後下flask run 會自動找這個設定值的內容
(flaskwebsite) kite@kite-virtual-machine:~/Desktop/pythonworkspace$ export FLASK_APP=app.py

8.2. 腳本與程式碼

網站程式碼:app.py

1
2
3
4
5
6
7
8
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
return 'Hello, World!'

鏡像構建腳本:Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#參考 python tag 為 slim 版的輕量 image
FROM python:slim

#將本機檔 app.py copy到image內部指定路徑
COPY app.py /src/app.py

#執行安裝flask套件指令
RUN pip install flask

#設定預設工作目錄
WORKDIR /src

#設定image內部環境變數
ENV FLASK=app.py

#對外公開5000 port
EXPOSE 5000

#執行網站啟動指令
CMD ["flask","run","-h","0.0.0.0"]

透過dockerhub官網去尋找python的docker image檔,這次特別找輕量的python,可以在tag頁籤底下查詢slim關鍵字。

image-20220222111053601

9. 合理使用緩存技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#參考 python tag 為 slim 版的輕量 image
FROM python:slim

#將本機檔 app.py copy到image內部指定路徑 << 我原本在這個位置,程式異動後重build,後面的步驟都會重新再來,不會找緩存
COPY app.py /src/app.py

#執行安裝flask套件指令
RUN pip install flask

#設定預設工作目錄
WORKDIR /src

#設定image內部環境變數
ENV FLASK=app.py

#對外公開5000 port
EXPOSE 5000

#執行網站啟動指令
CMD ["flask","run","-h","0.0.0.0"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#參考 python tag 為 slim 版的輕量 image
FROM python:slim

#執行安裝flask套件指令
RUN pip install flask

#設定預設工作目錄
WORKDIR /src

#設定image內部環境變數
ENV FLASK=app.py

#將本機檔 app.py copy到image內部指定路徑 << 我換到這個位置,程式異後動後重build。前面那些指令就會預設去找緩存的資料囉
COPY app.py /src/app.py

#對外公開5000 port
EXPOSE 5000

#執行網站啟動指令
CMD ["flask","run","-h","0.0.0.0"]

在固定且不經常變動的指令放到最前面,常會變的比如app.py檔案的程式碼更新。就儘量放到最後面。

這樣在build的過程中,就可以使用到緩存的機制。

10. dockerignore

我們可以透過tree -L 1來看一下結構目錄

1
2
3
4
5
6
7
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# tree -L 1
.
├── app.py
├── Dockerfile
├── flaskwebsite
└── __pycache__

我們發現其實flaskwebsite資料夾也包含在內,因此,指令docker image build -t flask-demo . 中的點也就是指當前路徑。

我們可以觀查在Sending build context to Docker damon 11.12MB這行顯示需要傳輸到11.12MB,但實際上我們的app.py程式碼才幾kb而已。原因就是因為包含了flaskwebsite資料夾

1
2
3
4
5
6
7
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# docker image build -t flask-demo .

Sending build context to Docker daemon 11.12MB
Step 1/7 : FROM python:slim
---> e4f3ae64bf20
Step 2/7 : RUN pip install flask
---> Running in 2ad3e866c913

我們來試著進行資料夾的排除,跟.giteignore概念一樣

.dockerignore檔案

1
./flaskwebsite

這個檔案我們寫了一行./flaskwebsite,也就是請在build過程中忽略flaskwebsite資料夾。

資料結構加了一個檔案後如下

1
2
3
4
5
6
7
8
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# tree -L 1 -a
.
├── app.py
├── Dockerfile
├── .dockerignore
├── flaskwebsite
└── __pycache__

我們再重build一下

1
2
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# docker image build -t flask-demo .
Sending build context to Docker daemon 5.632kB

就發現變成5.632kb囉

11. dockerfile技術之多段構建

有時候我們只需要一個可編譯的系統環境,也不必在host主機上安裝完整的編譯環境。就可以透過docker的環境來幫我們做到這件事情。但我們會發現可編譯程式的gcc鏡像檔(以編譯c語言為例),容量是很擁腫的,有1.44GB以上。

查看鏡像大容量,gcc1.14GBhello-apline6.55MB

1
2
3
4
5
root@kite-virtual-machine:/home/kite/Desktop/cworkspace# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-apline latest aed1c277cad0 About a minute ago 6.55MB
gcc 9.4 426442df2c0a 3 weeks ago 1.14GB
alpine 3.13.5 6dbb9cc54074 10 months ago 5.61MB

我們可以透過多段構建技巧,分段完成每一階段的任務。再最後階段再使用可執行c語言的鏡像檔幫我們build出小體積的鏡像檔。以下以這個為想法進行實驗與說明:

資料夾結構

1
2
3
4
root@kite-virtual-machine:/home/kite/Desktop/cworkspace# tree
.
├── Dockerfile
└── hello.c

hello

1
2
3
4
5
6
#include <stdio.h>

void main(int argc, char *argv[])
{
printf("hello %s\n", argv[argc - 1]);
}

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
## 第一階段編譯
#拉取一個可編譯c語言的鏡像(image比較肥大),別名為builder
FROM gcc:9.4 AS builder

#將檔案copy至內部容器之指定路徑
COPY hello.c /src/hello.c

#預設當前目錄
WORKDIR /src

#執行編輯 hello
RUN gcc --static -o hello hello.c


## 第二階段編譯(image比較輕量)
#拉取一個可執行c語言的鏡像
FROM alpine:3.13.5

#從builder鏡像取出 src/hello 程式copy之當前鏡像容器內部
COPY --from=builder /src/hello /src/hello

#預設執行該指令碼
ENTRYPOINT [ "/src/hello" ]

CMD []

玩玩看

1
2
root@kite-virtual-machine:/home/kite/Desktop/cworkspace# docker container run  --rm -it hello-apline kite
hello kite

12. dockerfile技巧之儘量使用非root用戶

dockerfile-root

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM python:slim

RUN pip install flask

WORKDIR /src

ENV FLASK=app.py

COPY app.py /src/app.py

EXPOSE 5000

CMD ["flask","run","-h","0.0.0.0"]

dockerfile-no-root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM python:3.9.5-slim

RUN pip install flask && \
groupadd -r flask && useradd -r -g flask flask && \
mkdir /src && \
chown -R flask:flask /src

#使用flask user帳號,避免使用root發生資安問題
USER flask

COPY app.py /src/app.py

WORKDIR /src
ENV FLASK_APP=app.py

EXPOSE 5000

CMD ["flask", "run", "-h", "0.0.0.0"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#構建flask-no-root鏡像
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# docker image build -f dockerfile-no-root -t flask-no-root .
Sending build context to Docker daemon 7.68kB
Step 1/8 : FROM python:3.9.5-slim
...
...
Successfully built b65dbef09eed
Successfully tagged flask-no-root:latest

#構建flask-root鏡像
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# docker image build -f docker-root -t docker-root .
unable to prepare context: unable to evaluate symlinks in Dockerfile path: lstat /home/kite/Desktop/pythonworkspace/docker-root: no such file or directory
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# docker image build -f dockerfile-root -t flask-root .
Sending build context to Docker daemon 7.68kB
Step 1/7 : FROM python:slim
...
...
Successfully built 50a0190d1105
Successfully tagged flask-root:latest

#查看鏡像
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
flask-no-root latest b65dbef09eed 3 minutes ago 126MB
flask-root latest 50a0190d1105 2 hours ago 134MB

#創建啟動flask-no-root
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# docker container run -d --rm --name flask-no-root flask-no-root
8699d07e199436126092b6310ecf1a1fe7d68300b4a430d1209e94af1ff1073c

#創建啟動flask-root
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# docker container run -d --rm --name flask-root flask-root
bb46a94bf7b5746ef0d3189fec59ae527531389955287c6df718cdb67c4a49a3

#使用container top 指令查看權限
##root權限的container
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# docker container top flask-root
UID PID PPID C STIME TTY TIME CMD
root 54109 54086 0 15:30 ? 00:00:00 /usr/local/bin/python /usr/local/bin/flask run -h 0.0.0.0

#使用container top 指令查看權限
##非root權限的container
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# docker container top flask-no-root
UID PID PPID C STIME TTY TIME CMD
systemd+ 54216 54196 0 15:30 ? 00:00:00 /usr/local/bin/python /usr/local/bin/flask run -h 0.0.0.0

#進入到flask-root container容器內,以井字號開頭就說明是root權限
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# docker run -it flask-root sh
# exit

#進入到flask-no-root container容器內,以錢字號開頭就說明是非root權限
root@kite-virtual-machine:/home/kite/Desktop/pythonworkspace# docker run -it flask-no-root sh
$ exit

13. 小總結

dockerfile可以多參考相關連結