Docker ( 2 ) - Dockerfile

一、Dockerfile

1.1 概述

Docker通过读取Dockerfile中的指令来自动构建镜像,Dockerfile是一个文本文件(可以方便做版本管理),它包含了构建镜像的所有命令。可以通过docker build读取Dockerfile文件来构建镜像。docker build提交构建镜像请求给Docker守护进程,同时会将当前目录递归传过去。所以最好是将Dockerfile和需要文件放到一个空目录,再在这个目录构建。

1.2 构建方法

$ docker tag pengbotao/php:7.4.8-fpm-alpine 127.0.0.1:5000/php:7.4.8-fpm-alpine

1.3 文件格式

可以通过Dockerfile文件来定义构建过程,通过.dockerignore用来忽略文件,构建时不会将ignore的文件传给Docker服务端。

1、 Dockerfile

# Comment
INSTRUCTION arguments

INSTRUCTION不区分大小写,建议大写。Dockerfile中的指令第一个指令必需是FROM用来指定基础镜像。#用来注释。

2、 .dockerignore:

# comment
*/temp*
*/*/temp*
temp?

此文件导致以下构建行为:

规则 行为
# comment 忽略
*/temp* 不包含子目录中temp打头的文件和目录。如:文件 /somedir/temporary.txt,目录 /somedir/temp
*/*/temp* 不包含子目录下的子目录中temp打头的文件和目录。 如:/somedir/subdir/temporary.txt
temp? 不包含根目录中temp打头的文件和目录。 如:/tempa/tempb

1.4 指令集合

编号 指令 参数 必须 示例
1 FROM FROM [--platform=<platform>] <image> [AS <name>]
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
FROM nginx
2 RUN RUN <command>
RUN ["executable", "param1", "param2"]
RUN ["/bin/bash", "-c", "echo hello"]
3 CMD CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
CMD ["/usr/bin/wc","--help"]
4 LABEL LABEL <key>=<value> <key>=<value> <key>=<value> ... LABEL version="1.0"
5 EXPOSE EXPOSE <port> [<port>/<protocol>...] EXPOSE 80/tcp
6 ENV ENV <key> <value>
ENV <key>=<value> ...
ENV myCat fluffy
7 ADD ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
ADD hom?.txt /mydir/
8 COPY COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
COPY hom?.txt /mydir/
9 ENTRYPOINT ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2
ENTRYPOINT ["top", "-b"]
10 VOLUME VOLUME ["/data"] VOLUME /var/log
VOLUME ["/var/log"]
11 USER USER <user>[:<group>]
USER <UID>[:<GID>]
USER patrick
12 WORKDIR WORKDIR /path/to/workdir WORKDIR /data
13 ARG ARG <name>[=<default value>] ARG user1=someuser
14 ONBUILD ONBUILD <INSTRUCTION> ONBUILD ADD . /app/src
15 STOPSIGNAL STOPSIGNAL signal
16 HEALTHCHECK HEALTHCHECK [OPTIONS] CMD command
HEALTHCHECK NONE
17 SHELL SHELL ["executable", "parameters"] SHELL ["powershell", "-command"]

二、指令详解

@todo

三、Dockerfiles最佳实践

3.1 多阶段构建

前一篇文章中构建的镜像大小有901M,Go的基础镜像就比较大。可以尝试将编译后的二进制放在新的容器中执行,解除对golang环境的依赖。

REPOSITORY            TAG        IMAGE ID       CREATED             SIZE
pengbotao/itopic.go   latest     36e405364fd8   25 hours ago        901MB

如,将编译后的itopic拷贝到alpine系统中,在看大小就只有27M了。

FROM golang:1.14 AS build

COPY . /go/src/github.com/pengbotao/itopic.go/
RUN cd /go/src/github.com/pengbotao/itopic.go/ \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o itopic


FROM alpine
COPY --from=build /go/src/github.com/pengbotao/itopic.go/ /www/itopic.go/
EXPOSE 8001
WORKDIR /www/itopic.go
CMD [ "-host", "0.0.0.0:8001" ]
ENTRYPOINT [ "/www/itopic.go/itopic" ]

3.2 通过.dockerignore忽略无关文件

构建时会将该指定目录传到Docker守护进程,所以一般建议在空目录或者只包含需要的文件,对于项目中不需要的文件可以通过.dockerignore文件忽略。

3.3 减少镜像层数

a. 镜像与层

Docker镜像由一系列层组成。每层代表Dockerfile中的一条指令。除最后一层外的每一层都是只读的。看以下Dockerfile:

FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py

包含了4条指令,每个命令会创建一层,FROM语句首先从ubuntu:18.04镜像创建一层。然后COPY命令从当前目录拷贝一些文件。RUN命令执行make操作,最后一层指定在容器中运行的命令。每个层仅包含了前一层的差异部分。 当我们启动一个容器的时候,Docker会加载镜像层并在其上添加一个可写层。容器上所做的任何更改,譬如新建文件、更改文件、删除文件,都将记录与可写层上。容器层与镜像层的结构如下图所示。

b. 容器与层

容器和镜像之间的主要区别是最顶层的可写层。在容器中所有写操作都存储在可写层中。删除容器后,可写层也会被删除,基础镜像保持不变。因为每个容器都有自己的可写容器层,并且所有更改都存储在该容器层中,所以多个容器可以共享对同一基础镜像。下图显示了共享相同Ubuntu 18.04镜像的多个容器。

3.4 清理无用数据

通常为命令执行过程中的一些缓存或者中间数据,可以随镜像创建完成时执行删除操作。

四、构建示例

4.1 php5.6

docker build -t pengbotao/php:5.6-fpm-alpine .

安装了常用PHP扩展:redis gd bcmath bz2 pdo_mysql mysqli opcache sockets pcntl xsl soap dom shmop zip mcrypt

FROM php:5.6-fpm-alpine

WORKDIR "/data/www"

RUN  sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update \
&& apk add autoconf libmcrypt-dev bzip2-dev libxslt-dev freetype-dev libjpeg-turbo-dev libpng-dev zlib-dev libzip-dev\
&& docker-php-ext-install bcmath bz2 pdo_mysql mysqli opcache sockets pcntl xsl soap dom shmop zip mcrypt\
&& docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ --with-png-dir=/usr/include/ \
&& docker-php-ext-install gd \
&& mkdir -p /usr/src/php/ext/redis \
&& curl -L https://github.com/phpredis/phpredis/archive/3.1.6.tar.gz | tar xvz -C /usr/src/php/ext/redis --strip 1 \
&& docker-php-ext-install redis \
&& docker-php-ext-enable redis 

CMD [ "php-fpm" ]

推送到Docker Hub

docker push pengbotao/php:5.6-fpm-alpine

4.2 php7.4.8

docker build -t pengbotao/php:7.4.8-fpm-alpine .

和上面类似,通过PECL安装了Redis扩展(http://pecl.php.net/package/redis)

FROM php:7.4.8-fpm-alpine

WORKDIR "/data/www"

RUN  sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update \
&& apk add build-base autoconf libmcrypt-dev bzip2-dev libxslt-dev freetype-dev libjpeg-turbo-dev libpng-dev zlib-dev libzip-dev\
&& docker-php-ext-install bcmath bz2 pdo_mysql mysqli opcache sockets pcntl xsl soap dom shmop zip \
&& docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ \
&& docker-php-ext-install gd \
&& pecl install redis-5.3.1 \
&& docker-php-ext-enable redis \
&& apk del build-base 

CMD [ "php-fpm" ]

推送到Docker Hub

docker push pengbotao/php:7.4.8-fpm-alpine

4.3 k8s-go-demo

测试过程中可能会碰到多版本的需要,这里通过go打一个多版本的镜像,方便做功能演示。可以打印当前版本号、时间、请求IP这几个信息。仓库地址:https://github.com/pengbotao/k8s-go-demo

Dockerfile:

FROM golang:1.14 AS build

WORKDIR /go

COPY . /go/k8s-go-demo/
RUN cd /go/k8s-go-demo/ \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o k8s-go-demo .


FROM alpine
COPY --from=build /go/k8s-go-demo/k8s-go-demo /bin/
EXPOSE 38001

CMD [ "-host", "0.0.0.0:38001" ]
ENTRYPOINT [ "k8s-go-demo" ]

创建镜像:

$ docker build -t pengbotao/k8s-go-demo:latest .
$ docker build -t pengbotao/k8s-go-demo:v1 .
$ docker build -t pengbotao/k8s-go-demo:v2 .
$ docker build -t pengbotao/k8s-go-demo:v3 .

推送:

$ docker push pengbotao/k8s-go-demo
$ docker push pengbotao/k8s-go-demo:v1
$ docker push pengbotao/k8s-go-demo:v2
$ docker push pengbotao/k8s-go-demo:v3

启动:

docker run -p 38001:38001 --name k8s-go-demo pengbotao/k8s-go-demo
docker run -p 38002:38002 --name k8s-go-demo-v1 pengbotao/k8s-go-demo:v1 -host 0.0.0.0:38002

访问示例:

{
    "ClientIP": "172.17.0.1",
    "Host": "3acd87ae93db",
    "ServerIP": "172.17.0.3",
    "Time": "2020-09-08 05:32:25",
    "Version": "latest"
}

五、构建调试

通过RUN可以在容器内的系统执行一些命令,但因为并非在容器内直接执行,调试上可能会打一些折扣,所以可以先在容器中跑一跑命令看看,跑通在整理到Dockerfile中,如果Dockerfile构建时失败了,我们可以用docker run命令来基于这次构建到目前为止已经成功的最后创建的一个容器,比如下面示例:

Dockerfile

FROM php:7.4.8-fpm-alpine
RUN  sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories 
RUN docker-php-ext-install redis

无法通过docker-php-ext-install redis来安装redis扩展,所以可以看到到第三步时报错了。

$ docker build -t test1 .
Sending build context to Docker daemon  9.216kB
Step 1/3 : FROM php:7.4.8-fpm-alpine
 ---> c8aada1d51a4
Step 2/3 : RUN  sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
 ---> Using cache
 ---> e101c0717b4f
Step 3/3 : RUN docker-php-ext-install redis
 ---> Running in 3455205e4c52
error: /usr/src/php/ext/redis does not exist

...

Some of the above modules are already compiled into PHP; please check
the output of "php -i" to see which modules are already loaded.
The command '/bin/sh -c docker-php-ext-install redis' returned a non-zero code: 1

这是我们可以进入第二步创建成功的容器,然后运行并进入该容器(我这里是alpine所以进入后终端用的是/bin/sh),然后直接在容器内做进一步调试,运行成功后就可以调整Dockerfile重新尝试构建

$ docker run -it e101c0717b4f /bin/sh
/var/www/html # more /etc/apk/repositories
http://mirrors.aliyun.com/alpine/v3.12/main
http://mirrors.aliyun.com/alpine/v3.12/community

也可以指定容器名字

$ docker run -it --name test e101c0717b4f /bin/sh

六、私有镜像仓库

6.1 通过registry镜像搭建

通过Docker的镜像很容易创建自己的镜像仓库。

1、安装。这里直接用官方的registry镜像来构建仓库,安装和启动方式和前面类似:

$ docker pull registry
$ docker run -d -v ~/docker/registry:/var/lib/registry -p 5000:5000 --restart=always --name registry registry:latest
d18931b1bd1b4b3ce7fe151c6f241c4dd1eebbfab8287dfed422ab5427a7f6a6

会启动5000端口,这里本机测试就不绑host了,也可以绑个host

2、打TAG

$ docker tag pengbotao/php:7.4.8-fpm-alpine 127.0.0.1:5000/php:7.4.8-fpm-alpine

docker tag类似别名的操作,推送到远程仓库需要类似这样子的格式,仓库/镜像名称:版本号,当然仓库默认是官方仓库,版本号是latest

3、推送镜像

$ docker push 127.0.0.1:5000/php:7.4.8-fpm-alpine
The push refers to repository [127.0.0.1:5000/php]
9c8d79537668: Pushed
106be351bd42: Pushed
7d85ada8d1c7: Pushed
767b1a2e8377: Pushed
9c55f034ee50: Pushed
6d3a6e58ccca: Pushed
fc5c686c158f: Pushed
d274b3a9c362: Pushed
81c338ff74a3: Pushed
308ef7bef157: Pushed
d94df04fea90: Pushed
50644c29ef5a: Pushed
7.4.8-fpm-alpine: digest: sha256:364931dd4bf7c5b159f93b458ab64bc3914218452c82d06d9359f82e3476c9bc size: 2830

4、查看镜像

# 查看镜像
$ curl http://127.0.0.1:5000/v2/_catalog
{"repositories":["php"]}

# 查看镜像tags列表
$ curl http://127.0.0.1:5000/v2/php/tags/list
{"name":"php","tags":["7.4.8-fpm-alpine"]}

也可以在~/docker/registry/docker/registry/v2/repositories/中查看仓库里的镜像信息。

5、镜像拉取(尝试删除后再从镜像仓库拉取)

$ docker rmi 127.0.0.1:5000/php:7.4.8-fpm-alpine
Untagged: 127.0.0.1:5000/php:7.4.8-fpm-alpine
Untagged: 127.0.0.1:5000/php@sha256:364931dd4bf7c5b159f93b458ab64bc3914218452c82d06d9359f82e3476c9bc

$ docker pull 127.0.0.1:5000/php:7.4.8-fpm-alpine
7.4.8-fpm-alpine: Pulling from php
Digest: sha256:364931dd4bf7c5b159f93b458ab64bc3914218452c82d06d9359f82e3476c9bc
Status: Downloaded newer image for 127.0.0.1:5000/php:7.4.8-fpm-alpine
127.0.0.1:5000/php:7.4.8-fpm-alpine

6.2 通过harbor搭建

下载地址:https://github.com/goharbor/harbor/releases

七、小结

借助Dockerfile和官方的基础镜像,基本可以编译出需要的环境,Dockerfile里的RUN命令和往常没有太大区别,但对比虚拟机或者ECS,好处是配置一次之后便可以以文本的方式存储起来或者将镜像推送到镜像仓库,轻量很多,后续配起来比较方便。到这里我们创建了3个镜像,有需要可以直接使用:

这一篇主要介绍了Dockerfile创建镜像,到这里我们就基本掌握了Docker的基本使用以及镜像的构建、推送。但像PHP的运行环境需要服务同时协作,按目前的理解就需要一个服务一个服务进行启动,启停上比较麻烦。而docker-compose基本可以解决开发环境下的这个问题,将各个容器打包启动。

下篇来看看docker-compose的用法。


-- EOF --
最后更新于: 2024-08-17 14:44
发表于: 2020-06-30 14:08
标签: Docker 容器化