上一篇文章对总体结构进行了简单记录,这一篇介绍下关于Dockerfile自定义镜像以及各个服务的配置。
> 其实 `https://hub.docker.com/` 上面各类基础镜像很是完善,特别是官方的镜像质量很是之高,而我再捣腾一次彻底是为了让本身掌握 Dockerfile 方面的技能而已。
在选择基础镜像方面,推荐使用 `Alpine` ,而后再它上面进行定制,由于它很是的小仅3M。个人 Nginx/Redis 是在 `Alpine` 基础上定制的,`PHP` 是在 `CentOS7`上面进行的定制。截图你们能够感觉下大小:

# 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
微信公众号:

参考资料:
- 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