目錄
- 前言
- Dockerfile的書寫規則及指令使用方法
- 創建Dockerfile,構建運行環境
- 構建鏡像
- Dockerfile參考示例
- Dockerfile最佳實踐
1. 前言
Dockfile是一種被Docker程序解釋的腳本,Dockerfile由一條一條的指令組成,每條指令對應Linux下面的一條命令。Docker程序將這些Dockerfile指令翻譯真正的Linux命令。Dockerfile有自己書寫格式和支持的命令,Docker程序解決這些命令間的依賴關系,類似于Makefile。Docker程序將讀取Dockerfile,根據指令生成定制的image。相比image這種黑盒子,Dockerfile這種顯而易見的腳本更容易被使用者接受,它明確的表明image是怎么產生的。有了Dockerfile,當我們需要定制自己額外的需求時,只需在Dockerfile上添加或者修改指令,重新生成image即可,省去了敲命令的麻煩。
2. Dockerfile的書寫規則及指令使用方法
Dockerfile的指令是忽略大小寫的,建議使用大寫,使用#
作為注釋,每一行只支持一條指令,每條指令可以攜帶多個參數。
Dockerfile的指令根據作用可以分為兩種,構建指令和設置指令。構建指令用于構建image,其指定的操作不會在運行image的容器上執行;設置指令用于設置image的屬性,其指定的操作將在運行image的容器中執行。
(1). FROM(指定基礎image)
構建指令,必須指定且需要在Dockerfile其他指令的前面。后續的指令都依賴于該指令指定的image。FROM指令指定的基礎image可以是官方遠程倉庫中的,也可以位于本地倉庫。
該指令有兩種格式:
FROM <image>
指定基礎image為該image的最后修改的版本。或者:
FROM <image>:<tag>
指定基礎image為該image的一個tag版本。
(2). MAINTAINER(用來指定鏡像創建者信息)
構建指令,用于將image的制作者相關的信息寫入到image中。當我們對該image執行docker inspect命令時,輸出中有相應的字段記錄該信息。
指令格式:
MAINTAINER <name>
(3). RUN(安裝軟件用)
構建指令,RUN可以運行任何被基礎image支持的命令。如基礎image選擇了ubuntu,那么軟件管理部分只能使用ubuntu的命令。
- RUN命令將在當前image中執行任意合法命令并提交執行結果。命令執行提交后,就會自動執行Dockerfile中的下一個指令。
- 層級 RUN 指令和生成提交是符合Docker核心理念的做法。它允許像版本控制那樣,在任意一個點,對image 鏡像進行定制化構建。
- RUN 指令緩存不會在下個命令執行時自動失效。比如 RUN apt-get dist-upgrade -y 的緩存就可能被用于下一個指令. --no-cache 標志可以被用于強制取消緩存使用。
指令格式:
RUN <command> (the command is run in a shell -
/bin/sh -c
)
RUN ["executable", "param1", "param2" ... ] (exec form)
(4). CMD(設置container啟動時執行的操作)
設置指令,用于container啟動時指定的操作。該操作可以是執行自定義腳本,也可以是執行系統命令。該指令只能在文件中存在一次,如果有多個,則只執行最后一條。
該指令有三種格式:
CMD ["executable","param1","param2"] (like an exec, this is the preferred form)
CMD command param1 param2 (as a shell)
當Dockerfile指定了ENTRYPOINT,那么使用下面的格式:
CMD ["param1","param2"] (as default parameters to ENTRYPOINT)
ENTRYPOINT指定的是一個可執行的腳本或者程序的路徑,該指定的腳本或者程序將會以param1和param2作為參數執行。所以如果CMD指令使用上面的形式,那么Dockerfile中必須要有配套的ENTRYPOINT。
(5). ENTRYPOINT(設置container啟動時執行的操作)
設置指令,指定容器啟動時執行的命令,可以多次設置,但是只有最后一個有效。
兩種格式:
ENTRYPOINT ["executable", "param1", "param2"] (like an exec, the preferred form)
ENTRYPOINT command param1 param2 (as a shell)
該指令的使用分為兩種情況,一種是獨自使用,另一種和CMD指令配合使用。
當獨自使用時,如果你還使用了CMD命令且CMD是一個完整的可執行的命令,那么CMD指令和ENTRYPOINT會互相覆蓋只有最后一個CMD或者ENTRYPOINT有效。
# CMD指令將不會被執行,只有ENTRYPOINT指令被執行
CMD echo “Hello, World!”
ENTRYPOINT ls -l
另一種用法和CMD指令配合使用來指定ENTRYPOINT的默認參數,這時CMD指令不是一個完整的可執行命令,僅僅是參數部分;ENTRYPOINT指令只能使用JSON方式指定執行命令,而不能指定參數。
FROM ubuntu
CMD ["-l"]
ENTRYPOINT ["/usr/bin/ls"]
(6). USER(設置container容器的用戶)
設置指令,設置啟動容器的用戶,默認是root用戶。
# 指定memcached的運行用戶
ENTRYPOINT ["memcached"]
USER daemon
或
ENTRYPOINT ["memcached", "-u", "daemon"]
(7). EXPOSE(指定容器需要映射到宿主機器的端口)
設置指令,該指令會將容器中的端口映射成宿主機器中的某個端口。當你需要訪問容器的時候,可以不是用容器的IP地址而是使用宿主機器的IP地址和映射后的端口。要完成整個操作需要兩個步驟,首先在Dockerfile使用EXPOSE設置需要映射的容器端口,然后在運行容器的時候指定-p選項加上EXPOSE設置的端口,這樣EXPOSE設置的端口號會被隨機映射成宿主機器中的一個端口號。也可以指定需要映射到宿主機器的那個端口,這時要確保宿主機器上的端口號沒有被使用。EXPOSE指令可以一次設置多個端口號,相應的運行容器的時候,可以配套的多次使用-p選項。
指令格式:
EXPOSE <port> [<port>...]
# 映射一個端口
EXPOSE port1
# 相應的運行容器使用的命令
docker run -p port1 image
# 映射多個端口
EXPOSE port1 port2 port3
# 相應的運行容器使用的命令
docker run -p port1 -p port2 -p port3 image
# 還可以指定需要映射到宿主機器上的某個端口號
docker run -p host_port1:port1 -p host_port2:port2 -p host_port3:port3 image
端口映射是docker比較重要的一個功能,原因在于我們每次運行容器的時候容器的IP地址不能指定而是在橋接網卡的地址范圍內隨機生成的。宿主機器的IP地址是固定的,我們可以將容器的端口的映射到宿主機器上的一個端口,免去每次訪問容器中的某個服務時都要查看容器的IP的地址。對于一個運行的容器,可以使用docker port加上容器中需要映射的端口和容器的ID來查看該端口號在宿主機器上的映射端口。
(8). ENV(用于設置環境變量)
- ENV指令可以用于為docker容器設置環境變量
- ENV設置的環境變量,可以使用docker inspect命令來查看。同時還可以使用docker run --env <key>=<value>來修改環境變量。
格式:
ENV <key> <value>
設置了后,后續的RUN命令都可以使用,container啟動后,可以通過docker inspect查看這個環境變量,也可以通過在docker run --env key=value時設置或修改環境變量。
假如你安裝了JAVA程序,需要設置JAVA_HOME,那么可以在Dockerfile中這樣寫:
ENV JAVA_HOME /path/to/java/dirent
(9). ADD(從src復制文件到container的dest路徑)
構建指令,所有拷貝到container中的文件和文件夾權限為0755,uid和gid為0;如果是一個目錄,那么會將該目錄下的所有文件添加到container中,不包括目錄;如果文件是可識別的壓縮格式,則docker會幫忙解壓縮(注意壓縮格式);如果<src>是文件且<dest>中不使用斜杠結束,則會將<dest>視為文件,<src>的內容會寫入<dest>;如果<src>是文件且<dest>中使用斜杠結束,則會<src>文件拷貝到<dest>目錄下。
格式:
ADD <src> <dest>
- <src> 是相對被構建的源目錄的相對路徑,可以是文件或目錄的路徑,也可以是一個遠程的文件url;
- <dest> 是container中的絕對路徑
(10). VOLUME (指定掛載點)
創建一個可以從本地主機或其他容器掛載的掛載點,一般用來存放數據庫和需要保持的數據等。
Volume設置指令,使容器中的一個目錄具有持久化存儲數據的功能,該目錄可以被容器本身使用,也可以共享給其他容器使用。我們知道容器使用的是AUFS,這種文件系統不能持久化數據,當容器關閉后,所有的更改都會丟失。當容器中的應用有持久化數據的需求時可以在Dockerfile中使用該指令。
格式:
VOLUME ["<mountpoint>"]
例如:
FROM base
VOLUME ["/tmp/data"]
運行通過該Dockerfile生成image的容器,/tmp/data目錄中的數據在容器關閉后,里面的數據還存在。例如另一個容器也有持久化數據的需求,且想使用上面容器共享的/tmp/data目錄,那么可以運行下面的命令啟動一個容器:
docker run -t -i -rm -volumes-from container1 image2 bash
說明:container1為第一個容器的ID,image2為第二個容器運行image的名字。
(11). WORKDIR(切換目錄)
設置指令,可以多次切換(相當于cd命令),對RUN,CMD,ENTRYPOINT生效。
格式:
WORKDIR /path/to/workdir
示例:
# 在 /p1/p2 下執行 vim a.txt
WORKDIR /p1
WORKDIR p2
RUN vim a.txt
(12). ONBUILD(在子鏡像中執行)
ONBUILD 指定的命令在構建鏡像時并不執行,而是在它的子鏡像中執行。
格式:
ONBUILD <Dockerfile關鍵字>
(13). COPY(復制本地主機的src文件為container的dest)
復制本地主機的src文件(為Dockerfile所在目錄的相對路徑、文件或目錄 )到container的dest。目標路徑不存在時,會自動創建。
格式:
COPY <src> <dest>
當使用本地目錄為源目錄時,推薦使用COPY
(14). ARG(設置構建鏡像時變量)
ARG指令在Docker1.9版本才加入的新指令,ARG 定義的變量只在建立 image 時有效,建立完成后變量就失效消失
格式:
ARG <key>=<value>
(15). LABEL(定義標簽)
定義一個 image 標簽 Owner,并賦值,其值為變量 Name 的值。
格式:
LABEL Owner=$Name
3. 創建Dockerfile,構建運行環境
Dockerfile文件
# 指定基于的基礎鏡像
FROM ubuntu:13.10
# 維護者信息
MAINTAINER zhangjiayang "zhangjiayang@sczq.com.cn"
# 鏡像的指令操作
# 獲取APT更新的資源列表
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe"> /etc/apt/sources.list
# 更新軟件
RUN apt-get update
# Install curl
RUN apt-get -y install curl
# Install JDK 7
RUN cd /tmp && curl -L 'http://download.oracle.com/otn-pub/java/jdk/7u65-b17/jdk-7u65-linux-x64.tar.gz' -H 'Cookie: oraclelicense=accept-securebackup-cookie; gpw_e24=Dockerfile' | tar -xz
RUN mkdir -p /usr/lib/jvm
RUN mv /tmp/jdk1.7.0_65/ /usr/lib/jvm/java-7-oracle/
# Set Oracle JDK 7 as default Java
RUN update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-7-oracle/bin/java 300
RUN update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/java-7-oracle/bin/javac 300
# 設置系統環境
ENV JAVA_HOME /usr/lib/jvm/java-7-oracle/
# Install tomcat7
RUN cd /tmp && curl -L 'http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.8/bin/apache-tomcat-7.0.8.tar.gz' | tar -xz
RUN mv /tmp/apache-tomcat-7.0.8/ /opt/tomcat7/
ENV CATALINA_HOME /opt/tomcat7
ENV PATH $PATH:$CATALINA_HOME/bin
# 復件tomcat7.sh到容器中的目錄
ADD tomcat7.sh /etc/init.d/tomcat7
RUN chmod 755 /etc/init.d/tomcat7
# Expose ports. 指定暴露的端口
EXPOSE 8080
# Define default command.
ENTRYPOINT service tomcat7 start && tail -f /opt/tomcat7/logs/catalina.out
tomcat7.sh命令文件
export JAVA_HOME=/usr/lib/jvm/java-7-oracle/
export TOMCAT_HOME=/opt/tomcat7
case $1 in
start)
sh $TOMCAT_HOME/bin/startup.sh
;;
stop)
sh $TOMCAT_HOME/bin/shutdown.sh
;;
restart)
sh $TOMCAT_HOME/bin/shutdown.sh
sh $TOMCAT_HOME/bin/startup.sh
;;
esac
exit 0
4. 構建鏡像
根據配置完的dockerfile構建Docker鏡像,并啟動docker容器。
docker build -t wechat-tomcat.
docker run -d -p 8090:8080 wechat-tomcat
默認情況下,tomcat會占用8080端口,所以在啟動container的時候,指定了 -p 8090:8080,映射到宿主機端口就是8090。
5. Dockerfile參考示例
示例1:構建Wordpress + nginx運行環境
# 指定基于的基礎鏡像
FROM ubuntu:14.04
# 維護者信息
MAINTAINER Eugene Ware <eugene@noblesamurai.com>
# Keep upstart from complaining
RUN dpkg-divert --local --rename --add /sbin/initctl
RUN ln -sf /bin/true /sbin/initctl
# Let the conatiner know that there is no tty
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
RUN apt-get -y upgrade
# Basic Requirements
RUN apt-get -y install mysql-server mysql-client nginx php5-fpm php5-mysql php-apc pwgen python-setuptools curl git unzip
# Wordpress Requirements
RUN apt-get -y install php5-curl php5-gd php5-intl php-pear php5-imagick php5-imap php5-mcrypt php5-memcache php5-ming php5-ps php5-pspell php5-recode php5-sqlite php5-tidy php5-xmlrpc php5-xsl
# mysql config, 配置MySQL運行參數
RUN sed -i -e"s/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/" /etc/mysql/my.cnf
# nginx config, 配置Nginx運行參數
RUN sed -i -e"s/keepalive_timeout\s*65/keepalive_timeout 2/" /etc/nginx/nginx.conf
RUN sed -i -e"s/keepalive_timeout 2/keepalive_timeout 2;\n\tclient_max_body_size 100m/" /etc/nginx/nginx.conf
RUN echo "daemon off;" >> /etc/nginx/nginx.conf
# php-fpm config
RUN sed -i -e "s/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/g" /etc/php5/fpm/php.ini
RUN sed -i -e "s/upload_max_filesize\s*=\s*2M/upload_max_filesize = 100M/g" /etc/php5/fpm/php.ini
RUN sed -i -e "s/post_max_size\s*=\s*8M/post_max_size = 100M/g" /etc/php5/fpm/php.ini
RUN sed -i -e "s/;daemonize\s*=\s*yes/daemonize = no/g" /etc/php5/fpm/php-fpm.conf
RUN sed -i -e "s/;catch_workers_output\s*=\s*yes/catch_workers_output = yes/g" /etc/php5/fpm/pool.d/www.conf
RUN find /etc/php5/cli/conf.d/ -name "*.ini" -exec sed -i -re 's/^(\s*)#(.*)/\1;\2/g' {} \;
# nginx site conf,將本地Nginx配置文件復制到容器中的目錄
ADD ./nginx-site.conf /etc/nginx/sites-available/default
# Supervisor Config
RUN /usr/bin/easy_install supervisor
RUN /usr/bin/easy_install supervisor-stdout
ADD ./supervisord.conf /etc/supervisord.conf
# Install Wordpress
ADD https://wordpress.org/latest.tar.gz /usr/share/nginx/latest.tar.gz
RUN cd /usr/share/nginx/ && tar xvf latest.tar.gz && rm latest.tar.gz
RUN mv /usr/share/nginx/html/5* /usr/share/nginx/wordpress
RUN rm -rf /usr/share/nginx/www
RUN mv /usr/share/nginx/wordpress /usr/share/nginx/www
RUN chown -R www-data:www-data /usr/share/nginx/www
# Wordpress Initialization and Startup Script
ADD ./start.sh /start.sh
RUN chmod 755 /start.sh
# private expose
EXPOSE 3306
EXPOSE 80
# volume for mysql database and wordpress install
VOLUME ["/var/lib/mysql", "/usr/share/nginx/www"]
# 容器啟動時執行命令
CMD ["/bin/bash", "/start.sh"]
示例2:構建Ruby on Rails環境
# 指定基礎鏡像
FROM fcat/ubuntu-universe:12.04
# development tools
RUN apt-get -qy install git vim tmux
# ruby 1.9.3 and build dependencies
RUN apt-get -qy install ruby1.9.1 ruby1.9.1-dev build-essential libpq-dev libv8-dev libsqlite3-dev
# bundler
RUN gem install bundler
# create a "rails" user
# the Rails application will live in the /rails directory
RUN adduser --disabled-password --home=/rails --gecos "" rails
# copy the Rails app
# we assume we have cloned the "docrails" repository locally
# and it is clean; see the "prepare" script
ADD docrails/guides/code/getting_started /rails
# Make sure we have rights on the rails folder
RUN chown rails -R /rails
# copy and execute the setup script
# this will run bundler, setup the database, etc.
ADD scripts/setup /setup
RUN su rails -c /setup
# copy the start script
ADD scripts/start /start
EXPOSE 3000
# 創建用戶
USER rails
# 設置容器啟動命令
CMD /start
示例3: 構建Nginx運行環境
# 指定基礎鏡像
FROM sameersbn/ubuntu:14.04.20161014
# 維護者信息
MAINTAINER sameer@damagehead.com
# 設置環境
ENV RTMP_VERSION=1.1.10 \
NPS_VERSION=1.11.33.4 \
LIBAV_VERSION=11.8 \
NGINX_VERSION=1.10.1 \
NGINX_USER=www-data \
NGINX_SITECONF_DIR=/etc/nginx/sites-enabled \
NGINX_LOG_DIR=/var/log/nginx \
NGINX_TEMP_DIR=/var/lib/nginx \
NGINX_SETUP_DIR=/var/cache/nginx
# 設置構建時變量,鏡像建立完成后就失效
ARG BUILD_LIBAV=false
ARG WITH_DEBUG=false
ARG WITH_PAGESPEED=true
ARG WITH_RTMP=true
# 復制本地文件到容器目錄中
COPY setup/ ${NGINX_SETUP_DIR}/
RUN bash ${NGINX_SETUP_DIR}/install.sh
# 復制本地配置文件到容器目錄中
COPY nginx.conf /etc/nginx/nginx.conf
COPY entrypoint.sh /sbin/entrypoint.sh
# 運行指令
RUN chmod 755 /sbin/entrypoint.sh
# 允許指定的端口
EXPOSE 80/tcp 443/tcp 1935/tcp
# 指定網站目錄掛載點
VOLUME ["${NGINX_SITECONF_DIR}"]
ENTRYPOINT ["/sbin/entrypoint.sh"]
CMD ["/usr/sbin/nginx"]
示例4:構建Postgres鏡像
# 指定基礎鏡像
FROM sameersbn/ubuntu:14.04.20161014
# 維護者信息
MAINTAINER sameer@damagehead.com
# 設置環境變量
ENV PG_APP_HOME="/etc/docker-postgresql"\
PG_VERSION=9.5 \
PG_USER=postgres \
PG_HOME=/var/lib/postgresql \
PG_RUNDIR=/run/postgresql \
PG_LOGDIR=/var/log/postgresql \
PG_CERTDIR=/etc/postgresql/certs
ENV PG_BINDIR=/usr/lib/postgresql/${PG_VERSION}/bin \
PG_DATADIR=${PG_HOME}/${PG_VERSION}/main
# 下載PostgreSQL
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
&& echo 'deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main' > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y acl \
postgresql-${PG_VERSION} postgresql-client-${PG_VERSION} postgresql-contrib-${PG_VERSION} \
&& ln -sf ${PG_DATADIR}/postgresql.conf /etc/postgresql/${PG_VERSION}/main/postgresql.conf \
&& ln -sf ${PG_DATADIR}/pg_hba.conf /etc/postgresql/${PG_VERSION}/main/pg_hba.conf \
&& ln -sf ${PG_DATADIR}/pg_ident.conf /etc/postgresql/${PG_VERSION}/main/pg_ident.conf \
&& rm -rf ${PG_HOME} \
&& rm -rf /var/lib/apt/lists/*
COPY runtime/ ${PG_APP_HOME}/
COPY entrypoint.sh /sbin/entrypoint.sh
RUN chmod 755 /sbin/entrypoint.sh
# 指定端口
EXPOSE 5432/tcp
# 指定數據掛載點
VOLUME ["${PG_HOME}", "${PG_RUNDIR}"]
# 切換目錄
WORKDIR ${PG_HOME}
# 設置容器啟動時執行命令
ENTRYPOINT ["/sbin/entrypoint.sh"]
具體用例可以參考Github的Docker文件相應的示例。
6. Dockerfile最佳實踐
- 使用.dockerignore文件
為了在docker build過程中更快上傳和更加高效,應該使用一個.dockerignore文件用來排除構建鏡像時不需要的文件或目錄。例如,除非.git在構建過程中需要用到,否則你應該將它添加到.dockerignore文件中,這樣可以節省很多時間。
- 避免安裝不必要的軟件包
為了降低復雜性、依賴性、文件大小以及構建時間,應該避免安裝額外的或不必要的包。例如,不需要在一個數據庫鏡像中安裝一個文本編輯器。
- 每個容器都跑一個進程
在大多數情況下,一個容器應該只單獨跑一個程序。解耦應用到多個容器使其更容易橫向擴展和重用。如果一個服務依賴另外一個服務,可以參考 Linking Containers Together。
- 最小化層
我們知道每執行一個指令,都會有一次鏡像的提交,鏡像是分層的結構,對于 Dockerfile,應該找到可讀性和最小化層之間的平衡。
- 多行參數排序
如果可能,通過字母順序來排序,這樣可以避免安裝包的重復并且更容易更新列表,另外可讀性也會更強,添加一個空行使用 \ 換行:
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion
- 創建緩存
鏡像構建過程中會按照 Dockerfile 的順序依次執行,每執行一次指令 Docker 會尋找是否有存在的鏡像緩存可復用,如果沒有則創建新的鏡像。如果不想使用緩存,則可以在docker build 時添加--no-cache=true
選項。
從基礎鏡像開始就已經在緩存中了,下一個指令會對比所有的子鏡像尋找是否執行相同的指令,如果沒有則緩存失效。在大多數情況下只對比 Dockerfile 指令和子鏡像就足夠了。ADD 和 COPY 指令除外,執行 ADD 和 COPY 時存放到鏡像的文件也是需要檢查的,完成一個文件的校驗之后再利用這個校驗在緩存中查找,如果檢測的文件改變則緩存失效。RUN apt-get -y update命令只檢查命令是否匹配,如果匹配就不會再執行更新了。
為了有效地利用緩存,你需要保持你的Dockerfile一致,并且盡量在末尾修改。