撰寫一份符合需求的 Dockerfile

Docker Hub 有許多官方與非官方製作的 Docker images,這些 images 足以提供常用的功能,例如資料庫 (MySQLMongoDBRedis)、網路應用 (WordPressJoomlaJenkinsMediaWiki)、源碼版本控管 (GitLab) 等。但總有那麼一個時候,缺少符合自己需求的 image,這時候就需要自己製作一個 image。

製作 image 的方式有兩種:一是使用 Docker container,直接進入 container 內部,使用命令列增刪改,再匯出成 image;二是使用 Dockerfile,將增刪改的動作文本化,再建置成 image。Dockerfile 的方式容易調整與管理,也是推薦的方式。

入門篇

製作一個能計算加減乘除的 Docker image。專案配置請參照源碼範本

Dockerfile

FROM ubuntu:14.04.2

MAINTAINER minimum@cepave.com

RUN useradd -m Minimum

USER Minimum

WORKDIR /home/Minimum

COPY calc.py ./

ENTRYPOINT ["./calc.py"]  

用到的 Dockerfile 指令有

  • FROM
  • MAINTAINER
  • RUN
  • USER
  • WORKDIR
  • COPY
  • ENTRYPOINT

第 1 行,FROM 一定要有,撰寫任何 Dockerfile 都不可或缺的指令,一個 Dockerfile 只能有一個 FROM,基於某個已存在的 image 進行二次開發。在這個範本中,使用 Docker Hub 上的 *ubuntu 做為基底。14.04.2 是 tag,可視為 image 版本。

第 3 行,MAINTAINER 是選擇性,記載誰寫的。

第 5 行,RUN 能在 Docker 建置 image 時執行命令列指令,主要用來安裝 packages、設定系統環境等操作。在這個範本中,新增了一個使用者 Minimum。

第 7 行,USER 用來切換使用者身分。Docker 預設使用者是 root,但若不需要,建議切換使用者身分,畢竟 root 權限太大了,使用上有安全的風險。

第 9 行,WORKDIR 用來切換工作目錄。Docker 預設工作目錄是在根目錄,只有 RUN 能執行 cd 指令切換目錄,而且還只作用在當下的 RUN,也就是說每一個 RUN 都是獨立進行的。如果想讓其他指令在指定的目錄下執行,就得靠 WORKDIRWORKDIR 的變更影響是持續的,不用每個指令前都使用一次 WORKDIR

第 11 行,COPY 能將本機端的檔案或目錄,複製到 image 內。calc.py 的內容請參閱源碼範本

第 13 行,ENTRYPOINT 是指定 Docker image 運行成 instance (也就是 Docker container) 時,要執行的指令或檔案。在這個範本中,calc.py 就是要執行的檔案。

建置 Docker image

$ docker build -t calc .

執行 Docker container

$ docker run --rm calc '1+2*3'


進階篇

大部份的 Docker 與 Dockerfile 介紹就請看倌們自行參閱 官方 DockerfileDocker -- 從入門到實踐;接著,來介紹 Dockerfile 其中幾個比較特別的指令與用法。

Dockerfile 指令 - ONBUILD

先來看一個 Django 的範本,專案配置請參照源碼範本

Dockerfile - mysite

FROM django:onbuild  

這個 Dockerfile 就真的只有一行;但在建置 image 時,能把專案中的 requirements.txtmanage.py 等檔案,與 mysite 資料夾放入到 image 中,並成功執行。這當中的奧祕就在 django:onbuild 這個 image 的 Dockerfile

Dockerfile - django:onbuild

FROM python:3.4

RUN mkdir -p /usr/src/app  
WORKDIR /usr/src/app

ONBUILD COPY requirements.txt /usr/src/app/  
ONBUILD RUN pip install --no-cache-dir -r requirements.txt

ONBUILD COPY . /usr/src/app

RUN apt-get update && apt-get install -y \  
        mysql-client libmysqlclient-dev \
        postgresql-client libpq-dev \
        sqlite3 \
        gcc \
    --no-install-recommends && rm -rf /var/lib/apt/lists/*

EXPOSE 8000  
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]  

注意第 6、7、9 行,COPYRUN 指令前多了 ONBUILD,只因為 ONBUILD,就讓 mysite 的 Dockerfile 不用自己寫 COPYRUNONBUILD 的作用就是讓指令延遲執行,延遲到下一個使用 FROM 的 Dockerfile 在建置 image 時執行,只限延遲一次。ONBUILD 的使用情境在於建置時取得最新的源碼 (搭配 RUN) 與限定系統框架 (如同上面 Django 範例)。

建置 Docker image

$ docker build -t mysite .

執行 Docker container

$ docker run -d -p 8000:8000 mysite

再用瀏覽器開啟 http://YOUR_IP:8000/ 就可以看到範本的頁面。

Dockerfile 指令 - ARG

ARG 是 Docker 1.9.0 提供的新指令。

Dockerfile

FROM ubuntu:14.04.2  
ARG Name=Minimum  
ENV Email=minimum@cepave.com  
LABEL Owner=$Name  

第 2 行,ARG 定義一個建置變數 Name,並賦值。

第 4 行,ENV 定義一個環境變數 Email,並賦值。

第 5 行,LABEL 定義一個 image 標籤 Owner,並賦值,其值為變數 Name 的值。

ARG 定義的變數效果只在建置 image 時有效,建置完成後變數就消失。ENV 定義的變數是環境變數,image 運行成 instance 時依然存在,可從 instance 內的環境變數中取得。LABEL 定義的標籤可以使用在 docker 指令中作為過濾用途,在 instance 內是無法取得。

ARG 的用途是建置 image 時可以有可變參數,例如

$ docker build -t example .

建置出來的 example 的 LABEL Owner=Minimum;

$ docker build --build-arg Name=Minimum2 -t example2 .

建置出來的 example2 的 LABEL Owner=Minimum2。

ARG 對於注入建置資料很有幫助,例如源碼版本、源碼來源、建置參數等。

Dockerfile 優化

Dockerfile 1

FROM ubuntu:14.04.2

MAINTAINER minimum@cepave.com

ENV GOLANG_VERSION=1.4.1 GOLANG_OS=linux GOLANG_ARCH=amd64  
ENV GOROOT=/home/go GOPATH=/home/workspace  
ENV PATH=$GOROOT/bin:$GOPATH/bin:$PATH

WORKDIR /home

RUN apt-get update  
RUN apt-get install -y wget vim git  
RUN wget https://storage.googleapis.com/golang/go$GOLANG_VERSION.$GOLANG_OS-$GOLANG_ARCH.tar.gz  
RUN tar xzf go$GOLANG_VERSION.$GOLANG_OS-$GOLANG_ARCH.tar.gz  
RUN mkdir -p workspace/src  
RUN apt-get remove -y wget  
RUN apt-get clean  
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*  
RUN rm -f go$GOLANG_VERSION.$GOLANG_OS-$GOLANG_ARCH.tar.gz  

Dockerfile 2

FROM ubuntu:14.04.2

MAINTAINER minimum@cepave.com

ENV GOLANG_VERSION=1.4.1 \  
    GOLANG_OS=linux \
    GOLANG_ARCH=amd64 \
    GOROOT=/home/go \
    GOPATH=/home/workspace \
    PATH=$GOROOT/bin:$GOPATH/bin:$PATH

WORKDIR /home

RUN \  
  apt-get update && \
  apt-get install -y wget vim git && \
  wget https://storage.googleapis.com/golang/go$GOLANG_VERSION.$GOLANG_OS-$GOLANG_ARCH.tar.gz && \
  tar -xzf go$GOLANG_VERSION.$GOLANG_OS-$GOLANG_ARCH.tar.gz && \
  mkdir -p workspace/src && \
  apt-get remove -y wget && \
  apt-get clean && \
  rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* go$GOLANG_VERSION.$GOLANG_OS-$GOLANG_ARCH.tar.gz

Dockerfile 1 和 Dockerfile 2 的差別在於 ENVRUN 的使用數量,ENV 允許一次定義多個變數,RUN 允許使用 && 串接指令,這麼做的好處是減少建置成 image 的 layer 數量,因為 layer 數量的上限為 127。當然還有更好的做法,例如使用 Shell scripts 執行複雜的指令。


補充說明

  • 查看 image 或 container 的 LABEL?

    $ docker inspect IMAGE_OR_CONTAINER_NAME

  • 統計 image 的 layer 數量?

    $ docker history IMAGE_NAME | wc -l