Learning Objectives
- 規劃建立映像檔的流程:先在互動式容器裡手動做一次,記下每個步驟
- 用 FROM 指定基底映像檔,用 LABEL 記錄維護者等資訊(取代已淘汰的 MAINTAINER)
- 用 RUN 安裝軟體,知道每個 RUN 會產生一層,並把相關指令合併成一行
- 用 COPY 把專案目錄裡的檔案放進映像檔,用 CMD 設定容器預設執行的程式
- 說明 EXPOSE 只是文件,真正對應埠號要在執行容器時用 -p
把環境寫成一份文字檔
到目前為止,我們都是拿別人做好的映像檔來用。如果想要一個「裝好 nginx、放好自己網頁」的映像檔,可以怎麼做?
答案是寫一份 Dockerfile:一個純文字檔,一行一個指令,描述「從哪個映像檔開始、裝什麼、複製什麼、啟動時執行什麼」。 有了它,任何人在任何裝了 Docker 的電腦上都能用同一份文字檔建出一模一樣的環境,而且可以放進 Git 管理版本。
這一課的目標是做出投影片的 static_web 映像檔:一個用 nginx 提供靜態網頁的網站。
先手動做一次:規劃建立流程
寫 Dockerfile 之前,先決定三件事:
| 要決定的事 | 選項 |
|---|---|
| 作業環境 | Debian/Ubuntu(用 apt-get 安裝套件)、Fedora(用 dnf)等 |
| 網頁伺服器軟體 | Nginx、Apache httpd |
| 基底映像檔 | 作業系統映像檔(例如 ubuntu:24.04,自己裝伺服器),或直接用伺服器的官方映像檔(例如 nginx) |
接著開一個互動式容器,手動做一次並把成功的指令記下來,這份紀錄就是 Dockerfile 的草稿:
docker container run -it --name nginx_test ubuntu:24.04 /bin/bash
在容器裡:
apt-get update
apt-get -y install nginx
dpkg -l nginx # 確認套件裝好了
ls -l /var/www/html # nginx 預設的網頁目錄
echo 'Hi, I am in your container' > /var/www/html/index.html
nginx -g "daemon off;" # 在前景啟動網頁伺服器
最後一行的 daemon off; 很重要:nginx 平常會轉到背景執行,但容器的主程式一結束,容器就停止,
所以在容器裡要讓 nginx 留在前景。測試時可以另外開一個容器連過去看看(投影片用 alpine 容器執行 wget -qO - http://172.17.0.2),
看到自己寫的那行字,就表示流程可行。按 Ctrl+C 結束 nginx、exit 離開。
記下來的三個關鍵步驟:apt-get update、apt-get -y install nginx、nginx -g "daemon off;",再加上「把網頁放到 /var/www/html/」。
第一份 Dockerfile
建立專案目錄,裡面放兩個檔案:Dockerfile(沒有副檔名,D 大寫)與網頁 index.html。
static_web/
├── Dockerfile
└── index.html
# Version: 0.0.1
FROM ubuntu:24.04
LABEL maintainer="[email protected]"
RUN apt-get update && apt-get install -y nginx
COPY index.html /var/www/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
| 指令 | 作用 |
|---|---|
FROM |
指定基底映像檔;必須是第一個指令(註解除外) |
LABEL |
在映像檔加上資訊(metadata),例如維護者、版本 |
RUN |
建置時執行指令,例如安裝套件;結果存成新的一層 |
COPY |
把專案目錄裡的檔案複製進映像檔 |
EXPOSE |
說明這個映像檔預期使用的埠號(只是文件) |
CMD |
容器啟動時預設執行的程式 |
以 # 開頭的行是註解。下面逐一說明。
FROM 與 LABEL
FROM:新的映像檔要從哪個映像檔開始。
# 沒寫標籤就是 ubuntu:latest
FROM ubuntu
# 指定版本,建議這樣寫,日後重建結果才一致
FROM ubuntu:24.04
# 也可以用別人做好的映像檔
FROM mysql/mysql-server
LABEL:用「鍵=值」為映像檔加上資訊。值裡有空格時要用雙引號包起來:
LABEL maintainer="[email protected]"
LABEL version="1.0" description="A static web site served by nginx"
建好之後可以用 docker image inspect 在 Labels 欄位看到這些資訊。
舊寫法:投影片與舊教材常見
MAINTAINER Docker Education Team <[email protected]>。這個指令已被官方淘汰,改用LABEL maintainer="…"。
ENV 也是常用的設定指令,用來設定環境變數,之後的指令與容器執行時都讀得到:
ENV APP_ENV=production
RUN:安裝軟體,每個 RUN 一層
RUN 在建置映像檔時執行指令,把檔案系統的變化存成新的一層,交給下一個指令繼續使用。
兩種寫法:
| 寫法 | 範例 | 說明 |
|---|---|---|
| shell 形式 | RUN apt-get update |
用 /bin/sh -c 執行,可以用 &&、管線、$變數 等殼層語法 |
| exec 形式 | RUN ["apt-get", "update"] |
不經過殼層,JSON 陣列,一定要用雙引號 |
幾個重點:
- RUN 不能和你互動。安裝時會問「是否繼續?[Y/n]」的指令,一定要加
-y,否則建置會卡住或失敗。 - 把
apt-get update與apt-get install寫在同一個 RUN,用&&連起來。分成兩個 RUN 時,update 那一層可能被快取住,日後只改 install 那行時,會拿舊的套件清單去安裝(第 08 課會看到快取怎麼運作)。 - 每個 RUN 都會多一層。相關的步驟合併成一個 RUN,層數少、映像檔也比較好管理。
- 只裝需要的東西。資料庫映像檔不需要文字編輯器,一般應用程式不需要網路除錯工具;裝得越多,映像檔越大、建置越久,要更新的套件也越多。
- RUN 不會幫你啟動服務。它只記錄檔案的變化,不記錄執行中的程序;容器啟動時要執行的程式交給
CMD或ENTRYPOINT。
COPY:路徑相對於建置目錄
COPY index.html /var/www/html/
# 投影片的寫法:把整個 html 目錄的內容複製過去
COPY html/ /var/www/html/
# 路徑有空白時用 JSON 陣列寫法
COPY ["my file.txt", "/data/"]
| 規則 | 說明 |
|---|---|
| 來源路徑 | 相對於建置目錄(通常就是 Dockerfile 所在的專案目錄),不是主機的根目錄;COPY /src/webapp/ /opt/webapp/ 的 /src/webapp/ 也是指專案目錄裡的 src/webapp/ |
| 不能往外拿 | 建置目錄以外的檔案(例如 ../secret.txt)複製不到 |
| 萬用字元 | COPY hom* /mydir/(hom 開頭的檔案)、COPY hom?.txt /mydir/(? 代表任一字元) |
目的地以 / 結尾 |
視為目錄,檔案放進該目錄 |
| 相對的目的地 | 相對於 WORKDIR 設定的工作目錄,例如 COPY test relativeDir/ |
--chown=使用者:群組 |
指定複製後檔案的擁有者(只適用於 Linux 容器) |
所以投影片才會建議:先建立專案目錄、切換進去、在裡面寫 Dockerfile,最後也在這個目錄執行建置指令。
放到哪個路徑,要看基底映像檔:
| 基底映像檔 | nginx 從哪裡讀網頁 |
|---|---|
ubuntu:24.04 + apt-get install nginx |
/var/www/html/ |
官方的 nginx 映像檔 |
/usr/share/nginx/html/ |
路徑放錯不會出現錯誤訊息,網站照樣起得來,只是你看到的是 nginx 的預設歡迎頁。
CMD 與 EXPOSE
CMD 設定容器啟動時預設執行的程式,有三種格式:
| 格式 | 範例 | 說明 |
|---|---|---|
| exec 形式(建議) | CMD ["nginx", "-g", "daemon off;"] |
JSON 陣列、雙引號;直接執行程式,不經過殼層 |
| shell 形式 | CMD nginx -g "daemon off;" |
用 /bin/sh -c 執行 |
| 當作 ENTRYPOINT 的預設參數 | CMD ["-g", "daemon off;"] |
搭配 ENTRYPOINT 使用(第 09 課) |
- 一份 Dockerfile 只有最後一個 CMD 有效。
- 有了 CMD,執行時只要
docker container run ncuedu/static_web,不必每次打nginx -g "daemon off;"。 - 執行時在映像檔名稱後面加上指令,會取代 CMD:
docker container run -it ncuedu/static_web /bin/bash跑的是 bash,不是 nginx。 - exec 形式不經過殼層,所以
CMD ["echo", "$HOME"]會印出字面上的$HOME,不會換成變數的值。
EXPOSE 說明這個映像檔預期使用哪些埠號:
# 預設是 TCP
EXPOSE 80
EXPOSE 80/udp
EXPOSE 不會真的開放埠號,它是寫給使用映像檔的人看的文件。容器的埠號預設都不對外公開,
真正的對應發生在執行容器時:-p 7777:80 指定主機的 7777 對到容器的 80;-P 則把 EXPOSE 列出的埠號都對應到主機上隨機的埠號(第 09 課)。
另一種寫法:從 nginx 映像檔開始
剛才的兩條路寫成 Dockerfile 是這樣:
# 寫法 A:從作業系統開始,自己安裝
FROM ubuntu:24.04
LABEL maintainer="[email protected]"
RUN apt-get update && apt-get install -y nginx
COPY index.html /var/www/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# 寫法 B:從 nginx 官方映像檔開始
FROM nginx:latest
LABEL maintainer="[email protected]"
COPY index.html /usr/share/nginx/html/
寫法 B 短很多,因為官方映像檔已經裝好 nginx,也寫好了 EXPOSE 80 與 CMD,你的 Dockerfile 會繼承這些設定。
寫法 A 能完全掌握安裝了什麼,適合學習每個步驟在做什麼,或需要另外安裝其他套件的時候。實務上常從官方映像檔開始,把心力放在自己的程式上。
想一想:Dockerfile 的指令
Dockerfile 的第一個指令(註解除外)必須是哪一個?
下面哪一種寫法比較好?
COPY html/ /var/www/html/ 裡的 html/ 是指哪裡?
想一想:CMD 與 EXPOSE
Dockerfile 裡寫了 EXPOSE 80,用 docker container run -d ncuedu/static_web(沒有 -p)啟動,從主機的瀏覽器連得到嗎?
Dockerfile 裡依序寫了 CMD ["echo", "one"] 與 CMD ["echo", "two"],用 docker container run 啟動(不加其他參數)會印出什麼?
重點精華回顧
- 先在互動式容器裡手動做一次、記下成功的指令,再把它寫成 Dockerfile。
FROM必須是第一個指令,建議指定標籤(例如ubuntu:24.04);LABEL maintainer="…"取代已淘汰的MAINTAINER。RUN在建置時執行指令、每個 RUN 產生一層;apt-get update && apt-get install -y …寫在同一行,只裝需要的套件。COPY的來源相對於建置目錄;網頁要放到 nginx 會讀的路徑(Ubuntu 安裝的是/var/www/html/,nginx 映像檔是/usr/share/nginx/html/)。CMD建議用 exec 形式["程式", "參數"],只有最後一個有效,執行時加上指令會取代它;容器的主程式要留在前景,例如nginx -g "daemon off;"。EXPOSE只是文件,真正對應埠號要在docker container run時用-p或-P。