Docker是一種輕量級的作業系統虛擬化解決方案,相較於傳統在Host作業系統上安裝Guest作業系統的硬體虛擬化方式,Docker可以直接在同一個Host作業系統核心上,以「容器」來區分應用程式的執行環境,也就是直接在系統層上完成虛擬化。因此Docker執行程式的效率通常會比傳統虛擬化的方式還要來得好,可以節省許多硬體資源。在實務上,Docker常被用來部署資料庫、Web應用程式等伺服器相關的程式,因為只要設定好執行環境,再將映像檔保存下來之後,就可以一直重複使用。對於程式開發人員來說,Docker也可以用來模擬不同環境下,程式是否能正常編譯和執行。



在本篇文章開始前,如果您還沒設定好您的Docker執行環境,可以參考這篇文章來設定:

容器(Container)和映像(Image)

Docker容器內的環境是由映像產生出來的,當一個容器在被建立時,會把映像內儲存的環境複製進來使用。在容器內發生的任何改變,只會影響到該容器,不會去更動到當初建立容器時使用的映像,更不會去更動到使用到相同映像的其它容器。

從 Docker Hub 取得映像

Docker Hub是Docker官方提供的Docker映像檔的線上存放空間,網址如下:

任何人都可以將自己製作的映像發佈到Docker Hub上,給自己或是給其他人使用。

當我們要從Docker Hub上把映像「pull」下來時,需要指定映像的名稱以及標籤(tag),如果沒有指定標籤,則預設會使用latest這個標籤。例如:

docker run hello-world

以上指令,會從Docker Hub上把名稱為hello-world的映像「pull」到本地端,並且建立出一個容器,這個容器會複製hello-world映像的環境,接著容器會被啟動,在螢幕上顯示出Hello from Docker!之類的訊息。這個指令其實就等同以下指令:

docker run hello-world:latest

在映像名稱後面使用冒號:來連接標籤名稱,相同名稱、不同標籤的映像可以是不同的映像。

撰寫 Dockerfile 來建立映像

Docker映像的建立是從撰寫Dockerfile檔案開始的。我們可以新增一個目錄,並在這個目錄下新增Dockerfile純文字檔案,並輸入以下內容:

FROM scratch

CMD echo "magiclen.org"

FROM命令可以指定一個Docker Hub上的映像為目前這個映像的基礎映像(Base Image),scratch是Docker最小的映像,沒有任何東西。FROM命令通常寫在Dockerfile檔案的第1行,如果有用到多行的FROM,或是ARG命令的話,FROM命令就不一定寫在第1行。

CMD命令可以設定使用這個映像所建立出來的容器在啟動的時候預設要執行的指令,筆者想讓容器在啟動的時候印出magiclen.org,所以此處CMD命令設定的指令為echo "magiclen.org"CMD命令寫在Dockerfile檔案的最後一行,如果寫了多行的CMD命令,只會用最後的那行。

scratch的環境下,只能去跑一些無動態連結函式庫的執行檔,連一般作業系統會有的echo指令都沒有。為了要在容器中使用echo指令,我們可以改用alpine作為基礎映像。

Alpine Linux是非常輕量化的Linux發行版,基於musl libc,很適合作為Docker容器的基礎映像來使用,以製作出輕巧高效的映像。當然如果您並不在乎映像的大小和效能,可以也是可以直接使用ubuntu作為基礎映像,比較不會有函式庫或是套件缺東缺西還得要自己去寫腳本來編譯原始碼的情況。

修改後的Dockerfile如下:

FROM alpine

CMD echo "magiclen.org"

上面的Dockerfile,可以使用docker bulid指令來建立映像。將工作目錄移動到Dockerfile所在的目錄,然後執行以下指令:

docker bulid .

docker

以上指令的.是指目前的工作目錄,就是要把目前的工作目錄當作是Docker在建立映像時所參考的目錄(build context),在這個目錄底下的所有沒被忽略的檔案都會在建立映像的時期被傳給Docker的守護行程(daemon)處理。至於要如何忽略檔案,之後會說明。

docker bulid指令執行成功後,上面的Dockerfile所產生出來的映像就建立完成了。我們可以用以下指令來查看本地端擁有的映像清單:

docker image ls

docker

如上圖,可以看到現在的映像清單上只有一個項目。這個便是我們剛才建立出來的映像。您可能會注意到那古怪的建立時間(應該是跟著基礎映像),明明才剛建出來,怎麼就過了四週了?這是Docker新出的「BuildKit」這個映像建立工具的問題,筆者也不知道這個要怎麼解決。

如果要使用舊版的建立工具(不建議這麼做,因為它被棄用了),可以在指令前加上DOCKER_BUILDKIT=0環境變數設定。如此一來建立出來的映像檔除了可以得到正確的映像建立時間之外,還可以在映像清單上看到建立映像時所使用到的基礎映像,可一同管理,如下圖:

docker

如果要用我們剛才建立出來的映像啟動一個容器的話,可以將這個映像的ID (Image ID)記錄下來,套用在下面的docker run指令中:

docker run <IMAGE_ID_or_NAME>

執行結果如下圖:

docker

如果想要自訂映像的名稱和標籤,可以在使用docker build指令時加上-t參數,後面再傳入要使用的名稱和標籤(標籤不設定的話預設為latest)。

docker

Dockerfile更深入的撰寫方式會融入到之後的小節中來講解。

容器的建立、啟動與停止、管理

首先建立出以下的Dockerfile

FROM alpine

RUN apk add --no-cache bash

CMD bash -c 'for((i=1;;i+=1)); do sleep 1 && echo "Counter: $i"; done'

RUN命令可以讓Docker映像在被建立的時候,去執行Docker環境中的某個指令。RUN命令可以被使用多次。

apk指令是Alpine Linux提供的套件管理工具,apk add --no-cache bash指令可以安裝bash套件且不快取套件索引。特別加上--no-cacheapk不快取套件索引資訊的目的是要讓最終產生的映像更輕量,讓映像愈輕巧愈好是製作映像的大原則。

使用以下指令建立映像:

docker build -t counter .

docker

如上圖,觀察一下docker build命令可以發現,Docker在建立映像的時候會自動把它做過的步驟做快取,下一次建立就不用再花大量時間執行一次。在執行Dockerfile中的命令時,會去比對Dockerfile中的命令是否有被修改,如果沒有的話,就會直接使用先前的快取。倘若現在我們修改了上面Dockerfile中的第5行,然後再重新建立映像,Docker也不會再重新pull alpine的映像和重新執行apk add --no-cache bash指令。

在撰寫Dockerfile的時候,要考慮到映像建立時的快取機制,把愈容易發生變化的命令放在愈後面,這樣才不會浪費太多時間在重跑建立映像時要執行的命令上。如果不想要在建立映像時使用快取的話,可以在使用docker build命令時加上--no-cache參數。

上面Dockerfile中的第5行會執行一個簡單的Bash迴圈,每秒進行計數並顯示在螢幕上。建立出counter映像後就可以用以下指令來建立並啟動容器看看:

docker run counter

執行結果如下圖:

docker

現在問題來了,我們該如何停止這個容器呢?Ctrl + c似乎不起作用(關閉終端機也沒用)?Shell居然就這樣卡住了!?

開啟新的終端機執行以下指令來查看運行中的容器狀態:

docker ps

docker

有個比較需要注意到的地方是,Docker會替沒有被明確命名容器的隨機配上一個名稱。由於我們剛才在使用docker run指令的時候並未使用--name參數來設定容器的名稱,所以如上圖所示,這個新容器被自動配上了exciting_gates這個名稱。

若要停止運行中的容器,可以使用docker stop指令,格式如下:

docker stop <CONTAINER_ID_or_NAME>

若要啟動停止中的容器,可以使用docker start指令,格式如下:

docker start <CONTAINER_ID_or_NAME>

這邊要注意的是,docker start指令若沒有加上-a(attach)參數,啟動容器後,容器只會在背景執行,而無法直接看到它的輸出結果。

若要重新啟動運行中的容器,可以使用docker restart指令,格式如下:

docker restart <CONTAINER_ID_or_NAME>

這邊要注意的是,docker restart指令在重新啟動容器後,容器只會在背景執行,而無法直接看到它的輸出結果。

若要刪除容器,可以使用docker rm指令,格式如下:

docker rm <CONTAINER_ID_or_NAME>

預設情況下,容器需要處在停止狀態才能夠被刪除。docker rm指令還可以加上-f參數來強制刪除運行中的容器。

docker run指令可以加上-d參數,使容器在被建立後於背景執行。

docker

在背景或前景執行的容器,可以使用docker ps指令看到。如果想要將容器移到前景執行,可以使用docker attach指令,用法如下:

docker attach <CONTAINER_ID_or_NAME>

docker

我們其實很少有機會去用到docker attach指令,比較多是用docker logs指令。docker logs指令可以查看容器的輸出日誌,不會讓Shell卡住,用法如下:

docker logs <CONTAINER_ID_or_NAME>

docker

把全部的日誌都顯示出來可能會太長,docker logs指令可以加上-n參數再接上一個數字來設定要顯示最新的幾行日誌。

docker

如果想要持續監看容器的日誌,還可以替docker logs指令加上-f參數。

docker

如果想要停止監看,只要按Ctrl + c就好了。停止監看日誌並不會導致容器的運行被中止。

docker

難道我們就只能監看容器而無法從外面對裡面的環境進行操作嗎?不,只要使用docker exec指令就可以在容器裡面執行指令。通常我們會直接執行容器裡面的Shell,在這個Shell環境中去管理容器,指令格式如下:

docker exec -it <CONTAINER_ID_or_NAME> bash

由於並不是所有映像都內建Bash,如有需要請自行代換其它的Shell。上面指令中的bash,就是要在容器中執行的指令。

docker

如果要離開Shell,請執行exit指令,或是按下Ctrl + d

當我們改變容器內的資料後,有一點要注意,那就是這個改變在刪除容器後是不會被保存下來的。原因在上面有提到,就是在容器內執行的任何操作都不會去影響到建立容器時所使用的映像。

作為指令工具來運行的容器

首先建立出以下的Dockerfile

ARG EXECUTABLE_NAME=s2tw
	 
FROM rust:slim-bookworm AS builder
 
WORKDIR /build
 
RUN cargo search --limit 0
 
RUN apt update && apt install -y doxygen clang curl cmake pkg-config \
    && curl -fL https://github.com/BYVoid/OpenCC/archive/refs/tags/ver.1.1.6.tar.gz -O \
    && tar xf ver.1.1.6.tar.gz \
    && cd OpenCC-* \
    && make -j$(nproc) PREFIX=/tmp/OpenCC install
 
RUN cp -r -f /tmp/OpenCC/include /usr && cp -r -f /tmp/OpenCC/lib /usr && cp -r -f /tmp/OpenCC/share /usr && ldconfig
 
RUN apt install -y git
 
ARG EXECUTABLE_NAME
 
RUN git clone --depth 1 https://github.com/magiclen/s2tw.git \
    && cd s2tw \
    && cargo build --release
 
 
FROM debian:bookworm-slim
 
COPY --from=builder /tmp/OpenCC/lib /usr/lib/
COPY --from=builder /tmp/OpenCC/share /tmp/OpenCC/share
 
RUN ldconfig
 
RUN adduser --disabled-password \
    --gecos "" \
    --no-create-home \
    user
 
WORKDIR /app
 
RUN chown user:user /app
 
USER user
 
ARG EXECUTABLE_NAME
ENV EXECUTABLE_NAME=${EXECUTABLE_NAME}
 
COPY --chown=user:user --from=builder /build/s2tw/target/release/${EXECUTABLE_NAME}  ./
 
CMD /app/${EXECUTABLE_NAME}

以上的Dockerfile檔案,在建立映像時,會先在編譯環境中編譯用來將簡體中文轉成繁體中文的s2tw指令工具以及其會需要用到的OpenCC函式庫,接著再把編譯好的檔案從編譯環境中複製到乾淨的運行環境中,最終建立好的映像就不會保留編譯原始碼所需要的程式專案和編譯工具,以及編譯過程中產生出來的垃圾(執行階段用不到的檔案)。

第1行使用了ARG命令來定義能被使用在這個Dockerfile檔案的EXECUTABLE_NAME變數,預設值為s2tw。用ARG命令定義的變數要怎麼指派非預設的值呢?使用docker build指令時可以加上一個或是多個--build-arg參數來指定ARG變數的值,如下:

docker build --build-arg EXECUTABLE_NAME=s2tw -t s2tw .

這邊如果不加--build-arg EXECUTABLE_NAME=s2tw參數,EXECUTABLE_NAME變數也還是會被預設為s2tw

docker

第3行使用了FROM命令來設定基礎映像為Rust編譯環境的映像。習慣上,Docker映像名稱後面的標籤名稱,除了會用版本號碼外,也有可能會用到該映像的基礎映像相關的分辨名稱。例如這邊使用的slim-bookworm,就是要使用Debian bookworm,也就是Debian 12,而且是輕量(slim)化過的,作為其所使用的基礎映像。而FROM命令的最後加上AS再接上一個名稱,是為了設定這個階段(從這個FROM命令到下一個FROM命令之前)所建立出來的映像的階段名稱,如此才能在接下來的階段使用COPY命令時,能去存取之前的階段所建立出來的映像中的檔案。

如果沒有什麼特殊需求的話,要使用以Debian為基礎的映像,建議都選擇「slim」版本。如果有遇到缺套件或函式庫的情況的話,再使用apt指令來安裝,或者區分階段來編譯原始碼。再次強調,我們要讓建立出來的映像愈輕巧愈好。

第5行的WORKDIR命令是要新增並設定之後的Dockerfile命令預設的工作目錄。如果沒有用WORKDIR命令來設定的話,預設就是用基礎映像所設定的工作目錄,或者是/

第7行的cargo指令是要更新Rust的套件索引,之所以特別寫出來,是因為這個指令需要花費的時間比較久,可以做個映像快取來加速。

第9行到第13行是用來安裝編譯OpenCC函式庫時所需要的套件、下載OpenCC的程式專案,以及執行編譯程式專案所需的指令。在此將OpenCC函式庫安裝到/tmp/OpenCC目錄,方便後續的複製。在RUN命令切換工作目錄,並不會影響到其它命令的工作目錄。

第15行是將/tmp/OpenCC目錄中,我們有需要的OpenCC的相關檔案複製到/usr目錄中,因為等等編譯s2tw時會去看這個目錄。並且執行ldconfig指令來重新搜尋可用的動態函式庫。

第17行用來安裝git指令工具,因為要透過git指令來下載s2tw的程式專案。

第19行用ARG命令宣告要在這個階段的此處開始套用EXECUTABLE_NAME變數,如此一來便能在Dockerfile的命令中使用${EXECUTABLE_NAME}來表示EXECUTABLE_NAME變數的值。ARG命令會使得後面命令的映像快取不容易被採用,所以ARG命令寫在愈後面愈好。

第21行到第23行是用來下載s2tw的程式專案,以及執行編譯程式專案所需的指令。

第26行又使用了一個FROM命令來進入下一階段,在這個階段使用的基礎映像為debian:bookworm-slim,要來打造執行s2tw的環境。

第28行和第29行用了COPY命令,從先前階段(builder)中複製出OpenCC函式庫。--from參數可以指定檔案來源的階段名稱,如果不設定--from參數,就會使用外面的檔案系統(host)作為檔案來源。COPY命令的檔案路徑格式如下:

COPY <src>... <dest> # 用空格隔開路徑,路徑可以用雙引號框起來
COPY ["<src>",... "<dest>"] # 路徑用雙引號框起來,用逗號隔開路徑

<src>可以是絕對路徑也可以是相對路徑。<src>的路徑支援Wildcard匹配,即*能匹配任意數量的任意字元;?能匹配一個任意字元。

<dest>可以是絕對路徑也可以是相對路徑。若是相對路徑則是相對於WORKDIR命令所設定的路徑。

第31行執行ldconfig指令來重新搜尋可用的動態函式庫。

第33行到第36行透過adduser指令建立出一般使用者,使用者名稱為user。在容器內也是有分使用者權限的,我們偏好使用一般使用者(或者說非root的使用者,non-root user)來運行我們的程式,降低安全相關的風險。

第38行使用WORKDIR命令來新增並設定之後的Dockerfile命令預設的工作目錄。

由於WORKDIR命令新增出來的目錄是屬於root使用者的,所以在第40行利用chmod指令來將工作目錄的擁有者更改為剛才建立的user使用者。

第42行使用USER命令來設定之後要用哪個使用者身份來執行指令。

第44行用ARG命令宣告要在這個階段的此處開始套用EXECUTABLE_NAME變數。第45行能使EXECUTABLE_NAME變數被當作環境變數來使用。ENV命令可以設定這個映像環境的環境變數預設值。在使用docker run指令建立並啟動容器時,可以加上-e參數,來設定環境變數的值。

第47行複製s2tw工具的執行檔到工作目錄下。

第49行能讓這個映像所建立出來容器在啟動時執行s2tw工具。注意這邊的${EXECUTABLE_NAME}是屬於指令的部份,是吃映像環境的環境變數,而不是ARG命令所設定的變數。

由於這個s2tw工具在執行後需要透過標準輸入與使用者互動,所以在使用docker run指令建立並啟動容器時,還要加上-i參數,執行結果如下圖:

docker run -i啟動的容器,可以直接按下Ctrl + d來停止。

docker

掛載容器外的檔案目錄

FROM rust:slim-bookworm AS builder

WORKDIR /build

RUN cargo search --limit 0

RUN apt update && apt remove -y imagemagick-6-common && apt autoremove -y && apt install -y clang curl make libglib2.0-dev zlib1g-dev libjpeg-dev libtiff-dev libpng-dev libwebp-dev libheif-dev libxml2-dev \
    && curl -fL https://imagemagick.org/archive/releases/ImageMagick-7.1.1-15.tar.gz -O \
    && tar xf ImageMagick-7.1.1-15.tar.gz \
    && mkdir /tmp/ImageMagick-lib \
    && cd ImageMagick-* \
    && ./configure --disable-static --disable-installed --disable-docs --enable-hdri --without-utilities --prefix /tmp/ImageMagick-lib \
    && make -j$(nproc) \
    && make install

RUN cp -r -f /tmp/ImageMagick-lib/include /usr/local && cp -r -f /tmp/ImageMagick-lib/lib /usr/local && ldconfig

RUN apt install -y git

RUN git clone --depth 1 https://github.com/magiclen/image-resizer.git \
    && cd image-resizer \
    && cargo build --release


FROM debian:bookworm-slim

RUN apt update && apt remove -y imagemagick-6-common && apt autoremove -y \
  && apt install -y libgomp1 libjbig0 libtiff6 libpng16-16 libwebp7 libwebpmux3 libwebpdemux2 libheif1 libxml2 \
  && apt clean && rm -rf /var/lib/apt/lists/*

COPY --from=builder /tmp/ImageMagick-lib/lib /usr/local/lib/

RUN ldconfig

RUN adduser --disabled-password \
    --gecos "" \
    --no-create-home \
    user

WORKDIR /app

RUN chown user:user /app

USER user

COPY --chown=user:user --from=builder /build/image-resizer/target/release/image-resizer  ./

ENTRYPOINT ["/app/image-resizer"]

CMD ["--help"]

以上的Dockerfile檔案,在建立映像時,會先在編譯環境中編譯用來處理圖片縮放的image-resizer指令工具以及其會需要用到的ImageMagick函式庫,接著再把編譯好的檔案從編譯環境中複製到乾淨的運行環境中,最終建立好的映像就不會保留編譯原始碼所需要的程式專案和編譯工具,以及編譯過程中產生出來的垃圾(執行階段用不到的檔案)。

第29行的apt clean指令可以清除已下載的套件的快取(apt install等的快取),rm -rf /var/lib/apt/lists/*則可以清除apt update的快取。清除快取以確保運行環境是乾淨的。

第48行使用ENTRYPOINT命令來設定Docker環境的進入點,在此是直接以image-resizer指令作為進入點。

ENTRYPOINT命令有兩種撰寫方式,如下:

ENTRYPOINT ["<executable_file_path>", "<arg1>", ... , "<arg2>"] # 第一種。執行檔路徑名稱和每個參數用雙引號框起來,並用逗號隔開。
ENTRYPOINT <command> # 第二種。一行指令。

第一種撰寫方式稱為「exec form」,不能直接支援變數(環境變數和ARG變數)。這種形式可以與CMD命令共同使用,容器在執行的時候,會執行ENTRYPOINT命令所設定的指令,再串接上CMD命令所設定的指令。

第二種撰寫方式稱為「shell form」,可以支援環境變數。這種形式與CMD命令共同使用的話,CMD命令似乎就會不起效果,且也無法透過docker run指令來串接指令。

CMD命令其實也有這兩種撰寫方式,如下:

CMD ["<arg0>", "<arg1>", ... , "<arg2>"] # 第一種。
CMD <command> # 第二種。

第一種撰寫方式就是「exec form」,不能直接支援變數(環境變數和ARG變數)。如果ENTRYPOINT命令也是「exec form」的話,容器在執行的時候,會執行ENTRYPOINT命令所設定的指令,再串接上CMD命令所設定的指令。例如上面的Dockerfile檔案,容器在執行的時候預設執行的完整指令就會是/app/image-resizer --help。我們可以在使用docker run指令的時候,於指令最尾端加上要替換掉的用CMD命令所預設的指令(exec form)。

第二種撰寫方式就是「shell form」,可以支援環境變數。如果ENTRYPOINT命令是「exec form」的話。容器在執行的時候,會執行ENTRYPOINT命令所設定的指令,再串接上sh -c,再串接上CMD命令所設定的指令。例如若將上面的Dockerfile檔案的CMD ["--help"]改為CMD "--help",容器在執行的時候預設執行的完整指令就會是/app/image-resizer sh -c "--help"

用以上的Dockerfile檔案建立出映像後,可以先用docker run指令來執行看看,如下圖:

docker

由於image-resizer指令工具是用來處理圖片檔案,且Docker容器的檔案系統無法直接與外面Host的檔案系統直接溝通,因此我們會需要將外面的檔案系統掛載到容器內的環境中,才能合理地使用這個容器。在使用docker run指令時,可以加上一個或多個-v參數來設定新建立出來的容器要掛上的捲軸(Volume)路徑,格式如下:

-v <src>:<dest>

<src>是外面的檔案系統的目錄的絕對路徑;<dest>是Docker容器的檔案系統的目錄的絕對路徑。

例如:

docker run -v $(pwd)/workspace:/workspace resizer /workspace -m 256

以上指令,會將外面的工作目錄下的workspace目錄掛載到容器的/workspace路徑,然後指定/workspace目錄給image-resizer指令工具,使它對這個目錄底下所有支援的圖片檔案進行最大尺寸為256像素的縮圖工作。

docker

-v參數也可以設定掛載進Docker容器的目錄在容器中是否可以寫入,預設是可以寫入,如果要用唯讀的方式掛載,可以在<src>:<dest>後接上:ro,變成<src>:<dest>:ro

docker

編譯本地的程式專案,並在容器內運行HTTP伺服器

這個小節會以TypeScript程式語言製作的HTTP伺服器來當作例子。如果您不太熟悉TypeScript程式語言,有興趣了解更多的話,可以參考《TypeScript學習之路》系列文章

首先新增一個目錄,並將終端機工作目錄移動到這個新目錄下,接著在這個目錄下新增一個app.ts檔案,檔案內容如下:

import { createServer } from "http";

const PORT = process.env.PORT ?? 3000;

const server = createServer((req, res) => {
    res.writeHead(200, { "Content-Type": "text/plain" });
    res.write("Hello world!");
    res.end();
});

server.listen(PORT, () => {
    console.log(`HTTP server started at http://localhost:${PORT}`);
});

也可以寫個README.md

Hello HTTP Server
=================

## How to Build?

```bash
docker build -t hello-http .
```

## How to Run?

```bash
docker run -d -p 3000:3000 --name hello-http hello-http
```

Then visit `http://localhost:3000`.

一般來說Node.js程式專案都要有package.json檔案,但在這篇文章中的重點不是Node.js或是TypeScript,所以把package.json放在Docker環境中建立。

再來要新增.dockerignore檔案,這個檔案可以設定Docker在建立映像時所參考的目錄下的檔案有哪些是要排除在build context外的,我們只需要把有用到的檔案加進來就好了。雖然這個檔案不是必要的,但可以減少build context的大小,加快建立映像的速度。.dockerignore的撰寫方式如同.gitignore

下面是這個專案需要的.dockerignore

*

!/app.ts
# !/src
# !/tests
# !/*.json

在上面的.dockerignore檔案中,第1行的*表示要忽略所有檔案,第3行之後以!開頭的路徑表示要允許的檔案。如果是正常的Node.js程式專案,通常也會允許/src目錄、/tests目錄和程式專案根目錄下的JSON檔案。

再來就是撰寫Dockerfile檔案,內容如下:

FROM node:slim AS builder

WORKDIR /build

RUN chown node:node /build

COPY --chown=node:node . .

USER node

RUN npm init -y && npm i -D typescript @types/node && npx tsc --init # Create a typescript project

RUN npm i && npx tsc && npm ci # Build project and create node_modules in the release mode 


FROM node:alpine

EXPOSE 3000

WORKDIR /app

RUN chown node:node /app

COPY --chown=node:node --from=builder /build/app.js /app/
COPY --chown=node:node --from=builder /build/node_modules /app/node_modules/

USER node

CMD node app.js

以上的Dockerfile檔案,還有一個比較需要講的地方是第18行的EXPOSE命令。EXPOSE命令可以將容器內有需要外面使用的連接埠暴露出來,交給Docker來分配要映射(map)到哪個Host的連接埠。EXPOSE命令的撰寫格式如下:

EXPOSE <port1> <port2>...
EXPOSE <port_start>-<port_end>

在使用docker run指令建立並啟動容器時,即便有EXPOSE命令,預設也並不會去做連接埠的映射。我們可以加上-P參數來映射連接埠,Docker會選擇Host中可以使用的連接埠來映射。

docker

如上圖,可以看到Docker選擇了Host的連接埠32768來映射容器內的連接埠3000

如果要指定映射的Host連接埠,要改用一個或多個-p參數,格式如下:

-p <host_port>:<container_port>
-p <host_port_start>-<host_port_end>:<container_port_start>-<container_port_end>

如果不想把容器內的連接埠映射到Host的話,也可以使用Docker Network機制。docker inspect指令可以查看容器所使用到的Docker網路介面,指令格式如下:

docker inspect <CONTAINER_ID_or_NAME>

docker

如上圖,可以看到目前這個容器使用了Docker的bridge網路介面,容器在這個網路介面下的IP為172.17.0.2,此時用網頁瀏覽器開啟http://172.17.0.2:3000就可以看到Hello world!

開機後自動啟動Docker容器

docker run指令後可以加上--restart參數,並給定一個值來設定重啟容器的策略。以下是可以使用的值:

  • no:預設值,不自動重啟。
  • on-failure:,如因錯誤而停止運行的話重啟。可以接上冒號:再接上要嘗試的次數。
  • always:總是重啟,不管是什麼原因而停止。如果使用docker stop來停止容器,則容器會在Docker守護行程下次啟動的時候重啟。
  • unless-stopped:總是重啟,除非使用docker stop來停止容器。

以上的策略,除了no之外,如果沒有使用docker stop來停止容器,容器都會在開機之後被自動啟動,而其中只有unless-stopped會在使用docker stop來停止容器之後,才不於開機之後被自動啟動。

清理Docker,釋放硬碟空間

在不斷地使用docker builddocker run指令後,本地端會建立出許多映像和容器,將會佔用不少的硬碟空間。當您發現硬碟剩餘空間告急時,可以使用docker ps指令來查看目前正在執行的容器有哪些。看有沒有什麼沒有用到的,可以將其停止(docker stop)或是移除(docker rm)。然後執行以下指令來清除所有非執行中的容器、沒有被使用到的映像,以及所有快取:

docker container prune -f && docker image prune -af && docker builder prune -af

執行這個指令前要注意,那就是處在停止狀態但套用了on-failurealways重啟策略的容器將會被刪除掉。所以如果不確定環境中有沒有這樣的容器,可以先用docker container ls -a指令查看一下。

不同的階段使用不同的 Dockerfile 檔案

同一個專案下可能會有多個不同的Dockerfile,來應付不同的部署階段。docker build指令可以加上-f參數再接上Dockerfile檔案的路徑來指定要使用哪個Dockerfile檔案。

Dockerfile檔案的命名方式因人而異。像是筆者習慣把用於Production環境的Dockerfile檔案命名為Dockerfile,力求產出最精簡的映像;用於Development環境的Dockerfile檔案命名為Dockerfile.dev,其所建立出來的映像和容器中會有用於建置、偵錯及測試專案的工具、設定檔和程式碼,且會藉由掛載外部檔案系統目錄的方式將原始碼掛進容器內來運行,並提供監視(watch)檔案變化的機制,使得原始碼檔案在容器外發生更動時,容器內可以自動同步套用。

Docker Compose

Docker經常會搭配Docker Compose一同使用,關於Docker Compose的使用方式可以再參考這篇文章: