1. 程式人生 > 其它 >Docker 系統性入門+進階實踐-04Dockerfile完全指南

Docker 系統性入門+進階實踐-04Dockerfile完全指南

第四章-Dockerfile完全指南

  1. 如何選擇基礎映象
    基本原則:
  • 官方映象優於非官方映象,如果沒有官方映象,則儘量選擇Dockerfile開源的;
  • 固定版本tag,而不是每次都使用最新版本latest
  • 儘量選擇體積小的映象
  1. 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執行指令

  1. 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命令,都給映象增加了一層,使得映象非常的臃腫,所以我們需要優化生成映象的命令,

  1. 將多個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也變小了

檔案的複製和目錄操作

  1. 往映象裡複製檔案有兩種方式,ADD和COPY,我們來看一下兩者的不同
    COPY和ADD都可以把local的一個檔案複製到映象裡,如果目標目錄不存在,則會自動建立
  2. 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"]
  1. 如何選擇
    因此在COPY和ADD指令中選擇的時候,可以遵循這樣的原則,所有檔案複製均使用COPY指令,僅在需要自動解壓的場合使用ADD
  2. 也可以將一個目錄打包複製到映象裡面去
FROM python:3.9.13-alpine3.14
ADD app.tar.gz /
CMD ["python3", "/app/hello.py"]
  1. 目錄變更的語法命令: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)

  1. 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變數會永久的儲存在映象裡面
  1. ARG可以在構建映象的時候動態修改value,通過--build-value
    sudo docker image build -f Dockerfile-arg -t ipinfo-arg-new --build-arg VERSION=2.0.0 .
  2. 何時使用ARG和ENV
    當我們僅僅使用變數取構建映象的時候,我們就使用ARG,當我們不僅僅是用變數去構建映象,還需在容器中去使用該變數時,就需要使用ENV了

容器啟動命令 CMD

  1. CMD可以用來設定容器啟動時預設會執行的命令
  • 容器啟動時預設執行的命令
  • 如果docker container run啟動時指定了其它命令,則CMD命令會被忽略
  • 如果定義了多個CMD,只有最後一個會被執行
    第二個如果執行容器的時候指定了命令,CMD命令就會被忽略,啥意思呢,我們指定個命令看一下:
    sudo docker container run -it ipinfo-env ipinfo version
    此時建立的容器只會顯示一下ipinfo的版本,並不會進入容器了。
  1. 我們重新定義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

  1. 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格式

  1. CMD和ENTRYPOINT同時支援shell格式和exec格式
  • shell格式
    CMD echo "hello world"
    ENTRYPOINT echo "hello world"
  • Exec格式
CMD ["echo", "hello world"]
ENTRYPOINT ["echo" "hello world"]
  1. 注意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映象

  • 目錄結構
  1. 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
  1. app.py檔案
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "hello python"

3.1 requirements.txt檔案

flask
  1. 通過Dockerfile構建映象
    sudo docker image build -t flask-demo .
  2. 通過映象建立並啟動容器
    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語言

  1. 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 []
  1. 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

  1. 多階段構建前:
    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幾兆就搞定了,非常節約空間。

Dockerfile技巧-儘量使用非root使用者

參考文件