Dockerfile 與 Compose 環境搭建學習筆記(二)

大愚Talk發表於2019-03-02
上一篇文章對整體結構進行了簡單記錄,這一篇介紹下關於Dockerfile自定義映象以及各個服務的配置。

> 其實 `https://hub.docker.com/` 上面各種基礎映象非常完善,特別是官方的映象質量非常之高,而我再搗騰一次完全是為了讓自己掌握 Dockerfile 方面的技能而已。

在選擇基礎映象方面,推薦使用 `Alpine` ,然後再它上面進行定製,因為它非常的小僅3M。我的 Nginx/Redis 是在 `Alpine` 基礎上定製的,`PHP` 是在 `CentOS7`上面進行的定製。截圖大家可以感受下大小:

![](&&&SFLOCALFILEPATH&&&FE7C23F8-FC2D-4B9A-8CF3-B22471ABC2E2.png)

# Dockerfile 與 Compose 建立關聯
關於概念可以看這裡:
https://yeasy.gitbooks.io/docker_practice/content/image/build.html

我這裡以 PHP/Redis/Nginx 的定製來進行一些說明(我也只是現學現用,希望高手多指教)。

在上篇的 docker-compose.yml 檔案中如下的配置:
“`yaml
dev.nginx.srv:
image: lei_nginx:1.14.0
build: ./nginx
volumes:
– ./nginx/conf:/home/work/app/nginx/conf
– ./www:/home/work/www
ports:
– “80:8080”
– “443:443”
restart: always
“`
這裡重要的是多了 build 這個選項,設定的對應目錄中可以找到 `Dockerfile` 這個檔案,當我們 `docker-compose up` 時,docker會根據這個檔案去先建立映象,然後啟動一個容器。

## Dockerfile 如何寫
網路上有非常多關於 `Dockerfile` 該如何寫的最佳實踐,我覺得有幾點特別重要:
– 一個容器只執行一個程式;
– 映象層數儘可能少,當然還需要考慮可讀性等方面的因素;
– RUN指令應該用 分成多行方便閱讀;
– 容器映象要儘可能的小。

更多最佳實踐可以看這裡:
https://yeasy.gitbooks.io/docker_practice/content/appendix/best_practices.html

接下來以 Redis 的 Dockerfile 來聊一聊實際如何編寫。
“`dockerfile
FROM alpine:3.7

# 解釋資訊
LABEL maintainer=”HeLei <dayugog@gmail.com>”

ENV REDIS_VERSION=3.2.11
SRC_DIR=/home/work/src
DATA_DIR=/home/work/app/redis/data
CONF_DIR=/home/work/app/redis/conf

# 設定系統時區
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

COPY src/ $SRC_DIR

# 編譯檔案
RUN set -ex;
addgroup -S work && adduser -S -G work work;
apk add –no-cache –virtual .build-deps
coreutils
gcc
jemalloc-dev
linux-headers
make
musl-dev
;
cd $SRC_DIR;
tar xvzf redis-$REDIS_VERSION.tar.gz;
cd redis-$REDIS_VERSION;
make && make install;
apk del .build-deps;
mkdir -p $DATA_DIR && mkdir -p $CONF_DIR;
chown -R work:work /home;
rm -rf $SRC_DIR

# 拷貝配置檔案
COPY conf/ /home/work/app/redis/conf

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT [“docker-entrypoint.sh”]

# 匯出埠
EXPOSE 6379
# 啟動redis
CMD [“redis-server”]

“`

第一行 **FROM** 用來指定基礎映象。也就是你要在什麼映象上進行定製,我這裡選擇的是 alpine,這是一個提供的基礎空白物件非常小。只是它上面的包管理是 `apk` ,使用時需要掌握下它的一些引數。

**LABEL**可以理解成新增一些說明、描述資訊。我這裡僅新增了自己的聯絡方式。可以通過反斜線 “ 來進行換行。

**ENV**用來設定環境變數,例如:定義一些系統版本、路徑的環境變數,在後續RUN中可以使用(當然不僅僅是RUN中可用),也可以用改寫原有的環境變數,例如:PATH。

**RUN**這是一個非常重要的命令,它是用來執行命令列的命令。就像上面看到的用 yum 安裝更新軟體,make編譯程式碼等。可以通過反斜線 “ 來進行換行。

**COPY**它是將宿主機的內容複製到容器中指定的路徑。

**EXPOSE**指令用於指定容器將要監聽的埠。一般設定為應用程式使用常見的埠,例如Redis設定為:`6379`

現在重點說下 **CMD** 與 **ENTRYPOINT** 兩個命令。如果Dockerfile中沒有 **ENTRYPOINT** 選項,**CMD** 的內容就相當於直接執行某個命令。但是當存在時就是另外一回事。以上面的為例:
“`docker
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT [“docker-entrypoint.sh”]

# 啟動redis
CMD [“redis-server”]
“`

這裡設定了一個 **ENTRYPOINT** ,像上面這種情況的時候如果直接啟動一個容器時,相當於最後應用啟動執行的命令是:`./docker-entrypoint.sh redis-server`。

根據這個特性,`docker-entrypoint.sh` 內部可以根據相關引數進行特殊處理。來看下我的 `docker-entrypoint.sh` 指令碼內容
“`shell
#!/bin/sh
set -e

cd `dirname $0`

# 對資料夾進行許可權修改
if [ “$1” = `redis-server` -a “$(id -u)” = `0` ]; then
chown -R work:work /home
exec redis-server /home/work/app/redis/conf/redis.conf
fi

exec “$@”
“`

可以看到如果指令碼後面帶的引數是`redis-server`則會先進行相關目錄授權,然後啟動redis。如果不是就會直接執行,例如:
“`shell
➜ ~/dockerEnv >docker run -it –rm redis:3.2.11 redis-cli -v
redis-cli 3.2.11
“`
會直接執行後面這個命令,你可以看到redis客戶端的版本資訊。這也就是表示,可以把映象當成一個命令來使用了。

有了 **ENTRYPOINT** 這個功能,可以用它在服務啟動時,做更多操作 。例如可以結合 docker-compose.yml 中設定的環境變數做更多事情。可以檢視官方的MySQL的 `docker-entrypoint.sh` 檔案內容。

# 依據Dockerfile啟動容器
Dockerfile 已經寫好了,通過下面的命令即可建立映象啟動容器。
“`shell
➜ ~/dockerEnv >docker build -t lei_redis:3.2.11 .
“`
在 redis/ 目錄下執行上面的命令,他會先獲取基礎映象,然後根據命令逐條執行,完成redis的編譯、安裝以及相關清理工作。

編譯完成後可用通過`docker image ls`檢視當前的映象列表資料。

然後通過 `docker run -it -p 6379:6379 -d lei_redis:3.2.11` 啟動一個容器。

啟動完成後,大家可以用redis客戶端連結檢視redis已經正常啟動。

當然還有 PHP/Nginx 的映象定製,以及每個服務的配置,大家可以在github上檢視詳情,這裡就不再贅述了,剩下再介紹下這個過程中遇的到的幾個錯誤。

# 遇到的錯誤
1. **在宿主機中無法連線Redis**
這是由於bind的問題。以前在 vagrant 中安裝redis也遇到過, 通過將配置修改為:
“`conf
bind 0.0.0.0
“`
宿主機能夠連線到伺服器上。這樣設定的含義是,讓容器中的Redis監聽容器ip的所有埠。這樣設定而不是指定ip是因為每個映象可以啟動多個容器,而每個容器的ip地址是不確定的。

2. **映象建立時報錯**
報錯資訊如下:
“`error
ERROR: for dockerenv_dev.php-fpm.srv_1 Cannot start service dev.php-fpm.srv: OCI runtime create failed: container_linux.go:348: starting container process caused “exec: “docker-entrypoint.sh”: executable file not found in $PATH”: unknown
“`
這個問題主要是:我的 `docker-entrypoint.sh` 檔案沒有可執行許可權,因此在映象建立完後,執行**ENTRYPOINT**指定的指令碼時導致錯誤,解決辦法當然很簡單,直接執行:`chmod +x docker-entrypoint.sh`。然後需要重新建立映象。

3. **Nginx 無法連線php-fpm**
這個錯誤其實與宿主機無法連線Redis很像,錯誤資訊:
“`error
2018/06/13 11:13:26 [error] 5#0: *8 connect() failed (111: Connection refused) while connecting to upstream, client: 172.18.0.1, server: localhost, request: “GET / HTTP/1.1”, upstream: “fastcgi://172.18.0.2:9000”, host: “localhost”
“`

修改 php-fpm 的監聽地址為:**0.0.0.0:9000**,Nginx可正常啟動。

4. **訪問php檔案時找不到檔案**
執行動態檔案時,出現了檔案找不到的提示,具體錯誤資訊:
“`error
2018/06/13 11:21:20 [error] 5#0: *10 FastCGI sent in stderr: “Primary script unknown” while reading response header from upstream, client: 172.18.0.1, server: localhost, request: “GET / HTTP/1.1”, upstream: “fastcgi://172.18.0.2:9000”, host: “localhost”
“`

由於Nginx與PHP沒有部署在同一個容器中,相關的專案檔案只與Nginx進行了共享,而沒有與PHP的容器進行共享。因此當訪問靜態檔案時,Nginx直接在自己的容器中完成操作,而訪問php檔案時資訊傳到了PHP所在的容器,容器內部無法找到對應的php檔案而導致的錯誤。

# 總結
經過2天的折騰,算是基本把環境搭建起來了。不過還有一些其他問題需要思考該如何進行:

– 如果我的PHP需要新的擴充套件,該如何去編譯這個擴充套件包?
– 如何去監控docker中的應用的狀態?比如:Redis/Nginx等服務的狀態。

後續會繼續摸索分享自己的經驗。

專案地址:
https://github.com/helei112g/docker-env

微信公眾號:
![pub](http://ol59nqr1i.bkt.clouddn.com/mp-qr.jpg)

參考資料:
– https://yeasy.gitbooks.io/docker_practice/content/
– https://docs.lvrui.io/2017/06/09/%E7%BC%96%E5%86%99docker-entrypoint-sh%E5%85%A5%E5%8F%A3%E6%96%87%E4%BB%B6/
– https://pkgs.alpinelinux.org/packages

相關文章