###### tags: `教學` `Docker` `Dockerfile` `學習筆記`
# Docker and Container 005 - Dockerfile
[TOC]
---
## 1. 關於dockerfile
- 在terminal打的一長串的指令 ←→ 可以整理進dockerfile裡
- 在這個檔案裡紀錄了要用哪些東西、做哪些事(安裝與設定的步驟),把它變成image(之後被拿來應用的東西)
- 建立Dockerfile,須注意檔名大小寫,且不具副檔名,必須完全一模一樣(亦可使用「docker build -f OTHERNAME .」指定其他檔名的Dockerfile)
- Dockerfile屬於文字檔案,可以使用任何編輯軟體撰寫,是以行為單位的指令和參數所組成,並以#標示為註解行
- 在 Dockerfile中,指令不區分大小寫。但為了更清楚地分辨指令和參數、指令一般是採用大寫形式。
### dockerfile的內容
- Dockerfile 通常會有四部分:基底映像檔資訊、維護者資訊、映像檔操作指令和容器啟動時執行指令。
- 基本的有:FROM, RUN, ENTRYPOINT
- 其他的還有什麼?
- MAINTAINER(X), WORKDIR, ONBUILD, ADD, COPY, CMD, EXPOSE, ENV, LABEL, USER, ARG, STOPSIGNAL, SHELL
- [參考資料](https://www.netadmin.com.tw/upload/news/NP171002000217100217565504.jpg)
- dockerfile的運作過程:
- dockerfile的第一個指令一定是FROM,去找指定的base image
- 抓好image之後,會產生一個臨時的container(每一個指令都會產生一層容器)
:::info
**延伸閱讀**
- [Docker 實戰系列(一):一步一步帶你 dockerize 你的應用](https://larrylu.blog/step-by-step-dockerize-your-app-ecd8940696f4)
:::
---
## 2. 編輯dockerfile
### 從**FROM**開始
1. 建立一個檔案
- `$ pwd`:查看現在所在位置(目錄)
- `$ touch Dockerfile`,touch建立一個檔案,Dockerfile的d為大寫
2. 編寫相關內容
- **FROM alpine:latest**,alpine內含輕量的linux
> 這行會載入程式需要的執行環境,會根據不同的需求下載不同的映像檔
- `$ cat Dockerfile`,列印出檔案內容
- `$ docker build -t kihifung/001 .`,在當下目錄建立一個image
- 
- 因為dockerfile指令只有一行,所以step也只有1
- 6dbb9cc54074 為建立的image名稱
- `$ docker images`,查看已建立的image
- 
- `$ docker run kihifung/001 cat /etc/os-release`,啟一個container,並順便查看作業系統
---
### 2.1. 利用dockerfile建立image
`$ docker build -t kihifung/course-image-build .`
- build 建立(映像檔)
- -t:tag 取名
- kihifung/course-image-build:kihifung帳號下面的course-image-build專案
- `docker build -t kihifung/course-image-build --build-arg my_name_is="Tom Cruise" .` 建立映像檔的同時,加上一個參數的屬性(更改名字)

---
### 2.2. 進到container
- `$ docker run -it kihifung/001 /bin/sh`,進到container的互動模式裡
- `$ docker container ls`,確認現在container的情形
- `$ docker run -d kihifung/001 tail -f /dev/null`
- `tail -f`,跟蹤某個file的內容,並印到console上
- `/dev/null`,常用的範例file
- `$ docker container ls`,確認現在container的情形
- `$ docker exec -it aa4e98479af1 /bin/sh`,進到container裡做事
- aa4e98479af1,container的名稱
---
### 2.3. **ENTRYPOINT**
> docker image執行時第一個執行的指令
> 寫在裡面,每一段都分別用"包著
```dockerfile=
FROM alpine:latest
ENTRYPOINT [ "tail", "-f", "/dev/null" ]
```
- `$ docker build -t kihifung/002 .`,重新建立一個新的image
- `$ docker images`,查看
- `$ docker run -d kihifung/002`,啟動container
#### CMD vs ENTRYPOINT:
這兩個指令其實大多數情況互通,以下列舉幾個Dockerfile中功能相同的寫法:
```dockerfile=
ENTRYPOINT ["java", "-jar", "target/accessing-data-mysql-0.0.1-SNAPSHOT.jar"] (建議寫法)
ENTRYPOINT java -jar target/accessing-data-mysql-0.0.1-SNAPSHOT.jar
CMD ["java", "-jar", "target/accessing-data-mysql-0.0.1-SNAPSHOT.jar"]
CMD java -jar target/accessing-data-mysql-0.0.1-SNAPSHOT.jar
```
#### 查看container
1. 進到裡面看
- `$ docker exec -it ada485409e25 /bin/sh`,進到container裡做事
2. 從外面看
- `$ docker run -d -p 8080:80 kihifung/002`,將內外的port連結起來
- 8080:VM linux的port
- 80:container的port
---
### 2.4. 安裝 Apache server
- 編輯dockerfile檔:
```dockerfile=
FROM alpine:latest
RUN apk --update add apache2 # apk:alpine安裝套件的指令
RUN rm -rf /var/cache/apk/* # 清掉快取
ENTRYPOINT ["httpd", "-D", "FOREGROUND"] # ,啟動apache的httpd的服務
```
- `$ docker build -t kihifung/004 .`
- `$ docker run -d -p 8081:80 kihifung/004`
- `$ ip a`,找出linux vm的ip位址,在瀏覽器查看,192.168.xxx.xxx:8081
---
### 2.5. 建立共用變數 **ENV**
> 在dockerfile裡,每個run都是使用臨時的container在執行
- 編輯dockerfile檔
```dockerfile=
FROM alpine:latest
LABEL name="MyName"
ENV myworkdir=/var/www/localhost/htdocs
RUN apk --update add apache2
RUN rm -rf /var/cache/apk/*
RUN cd ${myworkdir} \
&& echo "<h3>I am Sean, this is a test page. line 1</h3>" >> index.html
RUN cd ${myworkdir} \
&& echo "<h3>I am Sean, this is a test page. line 2</h3>" >> index.html
RUN cd ${myworkdir} \
&& echo "<h3>I am Sean, this is a test page. line 3</h3>" >> index.html
ENTRYPOINT ["httpd"]
CMD ["-D", "FOREGROUND"]
```
- `$ docker build -t kihifung/005 .`建立映像檔
- `$ docker run -d -p 8080:80 kihifung/005`
---
### 2.6. **WORKDIR**
>- 設定工作目錄
>- 修改ENV和WORKDIR
>- 表示在這個 Docker 中的 Linux 即將會建立一個目錄
```dockerfile=
FROM alpine:latest
LABEL name="MyName"
ENV myworkdir=/var/www/localhost/htdocs
WORKDIR ${myworkdir}
RUN apk --update add apache2
RUN rm -rf /var/cache/apk/*
RUN echo "<h3>I am Sean, this is a test page. line 1</h3>" >> index.html
RUN echo "<h3>I am Sean, this is a test page. line 2</h3>" >> index.html
RUN echo "<h3>I am Sean, this is a test page. line 3</h3>" >> index.html
RUN echo "<h3>I am Sean, this is a test page. line 4</h3>" >> index.html
ENTRYPOINT ["httpd"]
CMD ["-D", "FOREGROUND"]
```
- `$ docker build -t kihifung/006 .`,建立image
- `$ docker run -d -p 8084:80 kihifung/006`,起一個container跑
- 每次修改dockerfile內容,都要重新建立image,並重新使用container執行(之前port沒棄用,要重新給新port)
---
### 2.7. ARG
>- ARG, argument。跟ENV不同的是,可以在build階段更改變數的值
>- 更改預設
```dockerfile=
FROM alpine:latest
LABEL name="MyName"
ENV myworkdir=/var/www/localhost/htdocs
ARG whoami="Sean"
WORKDIR ${myworkdir}
RUN apk --update add apache2
RUN rm -rf /var/cache/apk/*
RUN echo "<h3>I am ${whoami}, this is a test page. line 1st</h3>" >> index.html
RUN echo "<h3>I am ${whoami}, this is a test page. line 2nd</h3>" >> index.html
RUN echo "<h3>I am ${whoami}, this is a test page. line 3rd</h3>" >> index.html
RUN echo "<h3>I am ${whoami}, this is a test page. line 9th</h3>" >> index.html
ENTRYPOINT ["httpd"]
CMD ["-D", "FOREGROUND"]
```
- `$ docker build -t kihifung/007 .`,建立image
- `$ docker run -d -p 8086:80 kihifung/007`,起一個container跑
- `$ docker build --build-arg whoami=John -t kihifung/008 .`build-arg whoami=John 更改變數
---
### 2.8. COPY
>- 代表會將本機端 `[OO路徑]` 的所有檔案加到 Linux 的 `[XX路徑]` 目錄底下
>- 複製/引入(本地)文件內容,遠端請使用ADD
- 格式為 `COPY <src> <dest>`
- 複製本地端的 `<src>`(為 Dockerfile 所在目錄的相對路徑)到容器中的 `<dest>`。
```dockerfile=
FROM alpine:latest
LABEL name="MyName"
ENV myworkdir=/var/www/localhost/htdocs
ARG whoami="Sean"
WORKDIR ${myworkdir}
RUN apk --update add apache2
RUN rm -rf /var/cache/apk/*
RUN echo "<h3>I am ${whoami}, this is a test page. line 1st</h3>" >> index.html
RUN echo "<h3>I am ${whoami},, this is a test page. line 2nd</h3>" >> index.html
RUN echo "<h3>I am ${whoami},, this is a test page. line 3rd</h3>" >> index.html
RUN echo "<h3>I am ${whoami},, this is a test page. line 9th</h3>" >> index.html
COPY ./content.txt ./
RUN ls -l ./
RUN cat ./content.txt >> index.html
ENTRYPOINT ["httpd"]
CMD ["-D", "FOREGROUND"]
```
- `$ docker build -t kihifung/009 .`,建立image
- `$ docker run -d -p 8089:80 kihifung/009`,起一個container跑
### 2.9. **EXPOSE**
- 是指 container 對外的埠號,在與外界溝通時使用
---
#### 小技巧
```dockerfile=
FROM <base_image> as <stage_name>
記得為每個 stage 命名,提高整體可讀性
未來若有更多 stage 加入時,可以不需跟著修改現存的 COPY 指令
COPY --from=<stage_name>
多利用 stage 名字取代數字以提高可讀性及維護性
```
### 2.10 其他範例
```dockerfile=
FROM node:9.2.0
COPY index.js package.json /app/
WORKDIR /app
RUN npm install && npm cache clean --force
CMD node index.js
```
1. 找到適合的基底映像檔 base image
> Docker 的 image 是一層一層疊加上去的,所以我們需要先在 [Docker Hub](https://hub.docker.com/explore/) 上選擇一個 base image,然後再慢慢把它加工成我們要的樣子

- 這個專案是用 Node.js 寫的,需要用 [node](https://hub.docker.com/_/node/) 作為 base image。決定 base image 後就在 Dockerfile 的第一行寫 [FROM](https://docs.docker.com/engine/reference/builder/#from) node:9.2.0
- `9.2.0` 指的是我們使用的 node image 版本,如果想要最新版的的話也可以指定 `node:latest`
2. copy 原始碼
- 建好 Node.js 環境之後要把 code 也包進去,這邊使用 `COPY` 指令,把 `index.js` 跟 `package.json` 複製到 `/app` 資料夾裡面
- 把本地(dockerfile所在地)的兩個檔案(index.js, package.json)複製到image的資料夾裡
3. 安裝 dependencies
- 環境和code 也已經在裡面了,現在可切換到 `/app` 這個目錄安裝 dependencies
- 使用 [WORKDIR](https://docs.docker.com/engine/reference/builder/#workdir) 切換到 `/app` 目錄,再用 [RUN](https://docs.docker.com/engine/reference/builder/#run) 跑 `npm install && npm cache clean --force`
- 清 npm cache 是為了讓 build 出來的 image 不要包含這些 cache,這樣 image 會小一點
- 如果是其他語言就把 `npm install …` 換成其他指令就可以了,像 python 可能就是 `pip install …`,有 cache 的話記得要清一下
4. 設定 initial command
- 環境、程式碼、dependencies 都準備好了,只剩把程式跑起來,這裡會用到 [CMD](https://docs.docker.com/engine/reference/builder/#cmd) 設定**這個 image 被跑起來時的預設指令**。把node跑起來: `node index.js`,這樣就完成了 Dockerfile。
5. build
- 完成後就可以開始 build image,在專案目錄下跑 `$ docker build -t simple-express-server .` 就會根據 Dockerfile build 出你的 image
- build 完之後查看現有映像檔, `$ docker image ls` ,可以看到多出一個 `simple-express-server`,這就是剛剛 build 出來的 image,裡面包含 node 的環境、程式碼還有 dependencies 跟預設啟動指令
6. 把 image 跑起來
* 有了 image 後可以 `$ docker run -p 3000:8080 simple-express-server` 把 image 跑起來
* 在 container 內跑的就是剛剛設定的預設指令 `node index.js`,`-p 3000:8080` 則是把 container 內的 8080 port 跟外部的 3000 port 接通,如此一來只要用瀏覽器到 `127.0.0.1:3000` 就可以看到 `Hello World!`,這樣就完成 dockerize 了,現在你的電腦即便什麼都沒有只裝 docker 也可以把這個程式跑起來
7. 跑在背景
* 如果照上面跑 `docker run -p 3000:8080 <image>` 的話會把終端機卡住,所以部屬的時候都會跑在背景,要跑在背景只要加一個 `-d` 就可以了,變成 `docker run -d <image>`,下指令後會得到一個 container ID,要看 log 的話可以跑 `docker logs <container ID>`
8. server 上沒有這個 image 怎麼辦
* 因為現在是在本機 build,遠端的 server 還沒有這個 image,如果要部屬必須先把 image 傳到遠端的機器,但這樣很麻煩,比較好的方式是**把 Dockerfile 跟所有原始碼放在 git repo 裡面,直接在 server 上 build image**,因為都是照 Dockerfile 裡面的內容跑,所以不管是在自己電腦還是 server 上 build 結果都是一樣的
- [參考資料](https://larrylu.blog/step-by-step-dockerize-your-app-ecd8940696f4)
### 2.11 撰寫規範
>- [Docker Shell vs. Exec Form](https://emmer.dev/blog/docker-shell-vs.-exec-form/)
:::info
- 矽谷牛的耕田筆記:
撰寫Dockerfile時有 Shell 與 Exec 兩種格式, RUN, CMD, ENTRYPOINT 等指令都同時支援。
- `Shell 格式就是 RUN command arg1 arg2 arg3` 這種直接描述的格式,
- `Exec 則是用 [] 包起來`,每個參數單獨敘述,譬如 RUN ["command", "arg1", "arg2", "arg3"] 等。
- 本篇文章推薦 **`RUN 指令採取 Shell`** 格式,而 **`CMD/ENTRYPOINT 都應該採用 EXEC`** 格式。
如果不清楚差異,以及為什麼這麽寫可以參考原文:
- 在 shell 形式中,`命令會從 shell 繼承環境變量`
- 涉及到`由ENV指令設置的環境變量時,這兩種形式的行為是相同的`
- 使用 exec 形式丟失的主要內容是所有有用的` shell 功能:子命令、管道輸出、鏈接命令、I/O 重定向等等,這些類型的命令僅適用於 shell 形式`。
- `大多數 shell 不會將進程信號轉發給子進程`,像是按下CTRL-C可能不會停止子進程
- 有一個特殊版本的 exec 形式CMD,CMD可作為ENTRYPOINT參數,這在shell無法實現。
:::
---
## 3. 其他操作
### 設定時區
```dockerfile=
FROM alpine:3.9
# 設定時區為上海
RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& apk del tzdata
CMD ["/bin/sh"]
```
:::info
- [Docker 映象,基於 alpine 系統的時區配置](https://itw01.com/YJIXZED.html)
:::
### 從映像檔產生 Dockerfile
1. [抓取特定映像檔](https://registry.hub.docker.com/r/centurylink/dockerfile-from-image)
2. 以這個image來建立一個container,以解析目標image(結果會產生一個Dockerfile.txt)
- [參考連結](https://philipzheng.gitbook.io/docker_practice/dockerfile/file_from_image)
## 4. 多階段構建 (multi-stage builds)
> 目的:透過拆解步驟來撰寫dockerfile,以縮小image的規模
- 多階段構建 (multi-stage builds)
- 透過將原先流水線中的多個映像檔整合進同個 Dockerfile 內,而後續的映像檔可透過指令取得中間映像檔 (intermediate image) 所產生的檔案 (artifacts),如此便能讓整個過程更為簡單,也**確保流水線簡潔易懂**及**提高維護性**。
:::info
### 參考資料
- [Use multi-stage builds(docker)](https://docs.docker.com/develop/develop-images/multistage-build/)
- [什麼是 Docker multi stage?](https://cindyliu923.com/2021/05/23/What-is-docker-multi-stage/)
- [透過 Multi-Stage Builds 改善持續交付流程](https://tachingchen.com/tw/blog/docker-multi-stage-builds/)
- [透過 Multiple Stage Builds 編譯出最小的 Docker Image](https://jiepeng.me/2018/06/09/use-docker-multiple-stage-builds)
- [Dockerfile - Multi-stage build 筆記](https://amikai.github.io/2021/03/01/docker-multi-stage-build/)
- [Docker - MultiStage](https://ithelp.ithome.com.tw/articles/10224514)
:::
---
## 回顧
- [ ] Dockerfile是幹嘛的
- [ ] Dockerfile的運作方式
- [ ] 熟悉各種指令
## 課後複習/測驗
### Q7:什麼是Dockerfile?
> 這對於初學者來說,是非常基本的問題,目的在於了解面試者是否已經具備建置任何Docker映像檔的能力,而適當的回答如下:
Docker藉由讀取名稱為Dockerfile的文字檔來自動建置容器映像檔,它**包含建置特定映像檔所需之依順序執行的所有命令**,Dockerfile遵循特定格式和指令集,如下所示:
```dockerfile=
FROM ubuntu CMD echo "This is a test." | wc
```
接著,進行操作示範。先來嘗試建立一個Docker映像檔並運行成Docker容器,首先新增名為cmd.Dockerfile檔案,並加入下列內容:`FROM ubuntu CMD echo "This is a test." | wc`,然後建置Docker Image:
- `$ docker build -t /mycmd -f ./cmd.Dockerfile`
現在,啟動容器及其結果:
- `$ docker run /mycmd 1 4 16`
---
1. Dockerfile練習(nginx)
1. 使用nginx
2. 加上額外訊息(看自己想加什麼)
3. 將範例的英文名字改成自己的英文名字
1. 將content.txt內容做更動
- [參考連結](https://hahow.in/courses/5df27f1fa5ee510022a08500/assignments?item=5e76ff159c4b140023a6caf1)
- content.txt的內容如下:
```htmlembedded=
<div>
<h3>My Book List</h3>
<ul>
<li><a href="#">Die Hard</a></li>
<li><a href="#">Secret</a></li>
<li><a href="#">Html 101</a></li>
<li><a href="#">Kubernetes 202</a></li>
<li><a href="#">AWS 303</a></li>
</ul>
</div>
```
1. [Docker Debug 挑戰題 - 網頁跑板](https://ithelp.ithome.com.tw/articles/10257226)
1. 下載專案
`$ git clone https://github.com/uopsdod/docker-debug-initial.git`
2. 下載圖片
[https://github.com/uopsdod/docker-debug-initial-image/blob/main/docker\_debug\_cover.jpeg](https://github.com/uopsdod/docker-debug-initial-image/blob/main/docker_debug_cover.jpeg)
3. 建立 dokcer image
`$ docker build -t mywebsite --no-cache .`
4. 啟動 docker container
`$ docker run -d -p 81:80 --name mywebsite mywebsite`
5. 查看首頁畫面
- 找出 ip 位置 (ex. 192.168.64.8:81),[http://localhost:81/](http://localhost:81/)
- [解答](https://youtu.be/TKIxxK9bJo4)
---
### 延伸閱讀
- [正確撰寫Dockerfile](https://www.netadmin.com.tw/netadmin/zh-tw/technology/7BD73E2A172C4847A3F72D238ACA5148)
- [撰寫Dockerfile](https://ithelp.ithome.com.tw/articles/10207535)
- [[Day 07]: Dockerfile 初探](https://zh-tw.coderbridge.com/series/9867865723164ad6b9de2a479ad9a37c/posts/6617bbab5e504cc28fd17475d035e849)
---
- [回到目錄](https://hackmd.io/@Hualiteq/r1lye3M3d)