序
如今Docker已经越来越普及,一台电脑(特别是开发人员的电脑)上安装十七八个工具和服务的时代已经过去。一次偶然在打开DockerHub网站,首页的一张图吸引了我的注意力:
这是15个(我猜是)Docker镜像库中最常用的官方镜像,大部分我很熟悉。温故而知新(摸🐟),索性整理一篇备忘录,目的有二:记录常用的镜像,已备在开发过程中突然要用到其中某个而卡壳;其次是扫清最常用应用的知识盲区。即然被全球广泛使用,没有理由不熟悉一下。
下面在介绍每个镜像的过程中,还会结合日常使用的案例一并介绍一些使用技巧。
注:本文主要以docker命令为主,同学们有需要稍微看看docker-compose的文档也能很方便转换。
分类
做分类之前先清理一下盲区:
- Nginx:Web服务的反向代理
- mongoDB:文档数据库系统
- alpine:以体积小著称的Linux的发行版
- node:NodeJS程序员引擎
- redis:key-value缓存
- busybox:常用工具箱集合
- ubuntu:源自Debian的Linux发行版
- python:Python程序引擎
- postgres:SQL+NoSQL 的数据库系统
- httpd:Apache HTTP服务
- mysql:SQL关系型数据库系统
- memcached:分布式内存缓存系统
- traefik:HTTP方向代理和负载平衡
- mariadb:兼容MySQL的关系性数据库系统
- docker:docker的套娃
以上15镜像,大致可以分为以下5种类:
- 操作系统:常用于构建应用的系统底座;
- 数据库系统:NoSQL、SQL和缓存服务;
- 程序语言引擎:用于解释执行或编译应用程序;
- Web服务:提供HTTP或微服务的应用程序;
- 工具箱:扩展宿主机功能的一些应用;
接下去,根据不同的类型,并结合以往的经验,假设有几类应用场景,在以下的场景中合理应用这些镜像
操作系统
十亿+的操作系统镜像有两个:alpine和ubuntu。两者各有所长,前者主要以体积小而著称,后者以通用性著称。我认为操作系统的镜像主要有三类用途:
- 构建定制应用程序镜像的系统底座;
- 直接运行,作为某些作业的测试环境;
- 作为宿主机的应用程序运行环境的补充;
不同的操作系统(镜像)之间的使用方法基本类似,主要的区别是分不同的应用场景,所以这里根据上述的用途场景,分析使用的命令和脚本使用。至于使用那个操作系统,只需要把镜像名称和版本换掉即可;
镜像构建
FROM alpine:latest
RUN apk add tzdata curl
RUN apk add nodejs
# ……
WORKDIR /var/app
HEALTHCHECK --interval=5s --timeout=2s CMD curl --fail localhost:3000 || exit 1
如果选用alpine缺点是在镜像脚本中需要明确安装应用程序。优点是alpine打包的镜像相比ubuntu明显更小;
# 在Dockerfile文件所在的目录中
$ docker build -t image-name:1.0.0 .
# 或任意其他目录
$ docker build -t image-name:1.0.0 -f /path/to/Dockerfile .
尝试(playground)
在编写构建脚本之前,往往需要先进行测试,以明确需要(缺少)那些包和依赖。所以,必不可少的步骤就要运行起来一个相同版本的系统环境,并在环境中测试运行有待部署的应用程序。
$ docker run -it -v ${PWD}:/var/bcm2835-1.71 --rm alpine
上述命令表示讲当前目录映射到容器中的一个目录,当推出容器时,自动删除容器。当命令执行后,环境就是在容器的环境中了。尝试编译或者运行你的应用程序。
环境的补充
例如在一个Windows或者Mac环境中需要模拟Ubuntu的环境,可以这样模拟部署(到服务器上)环境的执行情况。与测试环境类似,通过命令即可实现:
$ docker run -it -v ${PWD}:/var/app -w /var/app --rm node:16-alpine3.11 npm install
上述命令假设我已经在NodeJS的项目中,-v进行目录的映射到容器中 -w设定工作目录,启动容器后,直接安装项目的依赖。输出的结果:
added 3 packages, removed 1 package, changed 14 packages, and audited 150 packages in 51s
found 0 vulnerabilities
npm notice
npm notice New major version of npm available! 8.1.0 -> 9.2.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v9.2.0
npm notice Run npm install -g npm@9.2.0 to update!
npm notice
数据库系统
mysql、postgresql、mongodb、redis和 memcache经常出现在我们开发应用程序持久化层的依赖中。在开发系统时,没必要往自己的操作系统中安装各种数据库或缓存服务,只要根据应用程序的性质,配置容器即,不需要使用的时候,停掉容器。有占用资源少,灵活启动的优点。
数据库的配置关注主要有几个方面:
- 开放的端口:应用程序的关键配置之一;
- 身份验证:默认账户和密码配置,同样也是应用的关键配置之一;
- 数据库的存储路径:容器被销毁之后,如何处理历史数据;
- 数据导入、导出:初始化或者备份数据;
MongoDB
启动
mongo 默认端口27017;
$ docker run \
--name mongodb \
--restart always \
-v ${PWD}/.db:/data/db \
-v ${PWD}/.conf/mongod.conf:/etc/mongo \
-v ${PWD}/db-restore:/dump \
-e MONGO_INITDB_ROOT_USERNAME=root \
-e MONGO_INITDB_ROOT_PASSWORD=aabbcc \
-p 27017:27017 \
-d mongo:latest --config /etc/mongo/mongod.conf
以上命令含义:
--restart always表示随服务器一起启动;-v ${PWD}/.db:/data/db将当前目录的.db映射到容器的(默认)数据库位置,方便容器销毁后重启保留数据;-v ${PWD}/.conf/mongod.conf:/etc/mongo将当前目录的.conf映射到容器的配置文件目录,并在后续的--config /etc/mongo/mongod.conf指定数据库服务的配置参数;-v ${PWD}/db-restore:/dump将当前目录的db-restore映射到容器的默认导入导出目录,在做dump或restore操作时可以方便的复制文件;MONGO_INITDB_ROOT_USERNAME和MONGO_INITDB_ROOT_PASSWORD设定数据库访问账号
备份与恢复
镜像内的mongodump和mongorestore命令行工具进行操作;
# 备份(导出)数据
$ docker exec -i mongodb bash -c "rm -rf /dump && \
mongodump -u root -p aabbcc --authenticationDatabase admin --db=db-name"
# 初始化(导入)数据
$ docker exec -i mongodb bash -c "\
mongorestore -u root -p aabbcc --authenticationDatabase admin --db=db-name"
注意,导入数据前需要先用docker cp命令将数据复制到/dump/目录中,或宿主机的映射目录中。
MySQL/MariaDB
启动
mysql默认端口3306
$ docker run \
--name mysql \
--restart always \
-v ${PWD}/.db:/var/lib/mysql \
-v ${PWD}/.conf/conf.d:/etc/mysql/conf.d \
-e MYSQL_USER=dbUser \
-e MYSQL_PASSWORD=dbPassword \
-e MYSQL_DATABASE=appDb \
-e MYSQL_ROOT_PASSWORD=123456 \
-p 3306:3306 \
-d mysql:latest \
--default-authentication-plugin=mysql_native_password
命令含义:
-v ${PWD}/.db:/var/lib/mysql数据库存储的目录-v ${PWD}/.conf/conf.d:/etc/mysql/conf.d配置文件映射MYSQL_USER,MYSQL_PASSWORD,MYSQL_DATABASE配置特定的数据库以及访问的账户MYSQL_ROOT_PASSWORD根账户的密码--default-authentication-plugin=mysql_native_password让容器采用传统的密码验证方式
备份与恢复
# 备份在宿主机
$ docker exec -it mysql \
mysqldump -u root -p \
--databases appDb > appDb.sql
# 备份到容器中
$ docker exec -it mysql bash -c \
"mysqldump -u root -p \
--databases appDb > /var/backups/appDb.sql"
# 恢复数据库
# 1. 先将sql文件复制到容器中
$ docker cp ./appDb.sql mysql:/
$ docker exec -it mysql mysql -u root -p
# 进入MySQL服务,用source命令
mysql> source /appDb.sql
MariaDB
mariadb源自于MySQL的一个分支,所以两者有很强的兼容性和互换性。如果使用mariadb只需要将镜像名称mysql改为mariadb即可,甚至不需要更换配置名称。不放心的,可以将配置、命令中的mysql改为mariadb,例如 mysqldump => mariadb-dump
PostgreSQL
启动
postgres默认端口5432
$ docker run \
--name postgres \
--restart always \
-v ${PWD}/.db:/var/lib/postgresql/data/pgdata \
-v ${PWD}/.conf/postgresql.conf:/etc/postgresql/postgresql.conf \
-e PGDATA=/var/lib/postgresql/data/pgdata \
-e POSTGRES_DB=appDb \
-e POSTGRES_PASSWORD=123456 \
-p 5432:5432 \
-d postgres:latest -c 'config_file=/etc/postgresql/postgresql.conf'
命令含义:
-v ${PWD}/.db:/var/lib/postgresql/data/pgdata映射数据库存储地址,需要留意与-e PGDATA=/var/lib/postgresql/data/pgdata配合使用-v ${PWD}/.conf/conf.d:/etc/postgresql/postgresql.conf映射配置文件地址,需要留意与-c 'config_file=/etc/postgresql/postgresql.conf'配合使用-e POSTGRES_DB=appDb实例化容器时创建数据库-e POSTGRES_PASSWORD=123456超级用户密码,默认用户名postgres,也可以通过POSTGRES_USER指定超级用户名
恢复与备份
# 将数据库备份到宿主机
$ docker exec -i postgres pg_dump -U postgres -W appDb > db.sql
# 恢复数据
# 先将sql的备份文件复制到容器中,或者用目录的方式映射到容器中
$ docker exec -i postgres psql -U postgres -W -d appDb -f /path/to/db.sql
Redis
redis默认端口6379
docker run \
--name redis \
--restart always \
--v {$PWD}/.db:/data \
--v {$PWD}/.conf/redis.conf:/usr/local/etc/redis/redis.conf \
-p 6379:6379 \
-d redis:latest redis-server /usr/local/etc/redis/redis.conf --save 60 1 --loglevel warning
命令含义:
--v {$PWD}/.db:/data持久化的数据文件路径映射;--v {$PWD}/.conf/redis.conf:/usr/local/etc/redis/redis.conf配置文件映射,需要配置启动命令使用redis-server /usr/local/etc/redis/redis.conf;--save 60 160秒内至少写入一次;--loglevel warning日志等级为警告级;
近些年的开发,redis增加了几个重要的模块,例如支持全文搜索和JSON的格式,如果要运行这类镜像,可以将-d的镜像名称改为redislabs/redisearch和redislabs/rejson即可。另外,如果镜像的空间有有要求,可以下载alpine版本的镜像:redis:alpine。
Memcached
memcached默认端口11211
$ docker run \
--name memcached \
--restart always \
-p 11211:11211 \
-d memcached:latest memcached -m 1024
推荐使用alpine尺寸很小。命令的含义也比较简单,就是暴露11211端口,设定使用的内存(作缓存)的空间为1024MB。
程序语言
我是这么认为的:程序语言镜像的存在是因为两个原因:一方面减去人们从头(操作系统)开始构建自定义镜像的麻烦;另外一方面是可以很方便的切换不同的程序版本(主版本)以检查程序的兼容性。排名前10的程序语言除了SQL只有两个JIT类型的语言,Python和NodeJS。也许正是因为如此,对运行环境的依赖库要求要更高一些(不像编译型,编译后只要是目标系统都能直接运行);
同上文的操作系统类似,也分别从如何构建自定义镜像和如何搭建一个立刻可以运行验证的应用场景入手介绍这两个镜像。
NodeJS
自定义镜像与容器
因为对于相对Python,我更熟悉NodeJS,那么就从NodeJS开始,两者应用的逻辑上非常相似。
# OS 和环境基础内容
FROM node:16-alpine
RUN apk add tzdata curl
# 设置时区
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# App 系统层面环境变量
ENV PORT=3000
ENV APP=hr_3
ENV LOG_DIR=/var/log/
# 安装基础依赖组建
RUN npm install -g npm @nestjs/cli --registry=https://registry.npmmirror.com/
COPY ./dockerfiles/entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]
WORKDIR /var/app
HEALTHCHECK --interval=5s --timeout=2s --start-period=10s --retries=2 CMD curl --fail localhost:${PORT} || exit 1
制作镜像的应用程序源代码处理有两种方式:将程序文件用COPY命令全部拷贝到镜像中,或者在容器启动的时候用volume参数映射到容器的目录中。我比较偏向于后者,这样源代码更新后,只需要重启容器即可,打包发布的过程也更简单。配合上面的Dockerfile,启动容器的命令大概是下面这个样子:
$ docker build -t hr-3:latest .
$ docker run \
--network=hr_network \
--env NODE_ENV=prod \
--name hr_3 \
--restart always \
-v ${PWD}/backend:/var/www/ \
-v ${PWD}/frontend:/var/frontend/ \
-v ${PWD}/prod.local.env:/prod.local.env \
-p 3001:3000 \
-d hr_3:latest
配合entrypoint.sh的启动程序,每次应用升级只需要拉取代码,再重启容器即可完成升级过错。
#!/bin/sh
# 先构建前端
echo 'Building frontend...'
cd /var/frontend
npm install
npm run build
# 再处理后端
echo 'Building backend...'
cd /var/www
npm install
npm run build
npm run start:prod
echo 'Starting...'
当这个镜像被验证已经可以工作,可以用三种方式发布镜像:
- 将
Dockerfile发布到docker hub或私有的仓库上,其他的开发者或测试、生产环境中下载; - 直接分享
Dockerfile,或干脆将文件连同项目源代码一起托管到git仓库中。 - 有些自定义镜像需要下载远程文件或是特殊文件,那么无法通过三方仓库分享,那可以将构建成功的镜像导出到文件
docker save -o custom_image.tar hr_3:latest
将文件复制到运行镜像的主机上,导入到docker数据库中
docker load -i custom_image.tar
Playground
当初次尝试构建应用程序时,可以尝试使用构建临时容器的方式,尝试一步步地部署容器中的应用程序。
$ docker run -it -v ${PWD}:/home/ -p 3000:3000 --rm node:18-alpine sh
$ cd /home/app
$ npm install
$ npm run dev
此时,如果运行正常,那么宿主机的3000端口应该可以访问到应用程序的服务了。
Python
构建自定义镜像
FROM python:latest
WORKDIR /home/
COPY ./requirements.txt ./
COPY . .
RUN pip3 install -r requirements.txt
CMD ["python3", "-m", "./app", "--host=0.0.0.0", "--port=1234"]
构建镜像
$ docker build -t il:latest .
直接启动容器
$ docker run -it -v {$PWD}:/home --rm python:alpine python
Web服务程序
Web服务一般不会独立存在,甚至反向代理纯静态网站的场景也不多见。往往是将应用部署为集群或(和)分布式的网络提供服务。对应服务模式就有负载均衡和路由两种配置方式。不过本文不讨论具体如何配置Web服务,仅聚焦于镜像本身以及应用的实践。所以,考虑的重点就放在两个方面:
- 如何方便地维护服务的配置文件,高效地存取日志文件;
- 如何将服务指向不同位置的应用;
第一个问题很好解决,就通过-v命令映射目录既可。第二个问题稍微复杂,我们需要梳理一下。应用程序的位置,相对于容器来说,无外乎三个地方:远程主机、宿主机和容器。Web服务代理远程主机常见于调用远程API或隐藏服务的同于,处理起来也最为简单,直接在配置文件中使用远程地址和端口即可。
如果服务位于宿主机,那么需要看容器的网络模式决定,bridge的方式与主机组网,可以采用访问主机的ip地址,无论是局域网的还是虚拟网卡的都可以。例如,主机的局域网地址是192.168.1.3或者在默认情况下的容器虚拟网络地址是172.17.0.0/8,主机的地址是172.17.0.1。host的方式与主机组网,那么容器的localhost就等同主机,所以只需要设定端口号即可,例如localhost:1234。
第三种位置,即在其他容器的内的服务,我们也可以通过两种方式进行访问。一种是明确容器的虚拟网络的IP地址,即上文提及的172.17.0.0/8访问,缺点是因为地址是自动分配的,所以不同的启动顺序,可能导致IP地址变化。第二种方法比较简单,创建一个网络,并将容器加入到这个网络中,根据容器的名称通过内部DNS直达访问。结合以下镜像的使用,讲一讲这种方式的使用。
Nginx
命令方式
# 创建网络
$ docker network create app
# 启动应用程序(群)加入上述网络的容器
$ docker run --name task0 --restart:always --network app -d task:latest
$ docker run --name task1 --restart:always --network app -d task:latest
# 启动nginx,并指定配置文件
$ docker run --name nginx \
--network app \
-p 8080:8080 \
-v ${PWD}/tasks-nginx.conf:/etc/nginx/site-enabled/tasks-nginx.conf:ro \
-d nginx
第一步:创建一个(默认bridge)的网络;
第二步:启动应用程序,并加入这个网络;注意,此时已经不需要单独将应用程序的端口暴露出来了;并且可以启动多个相同或者不同的(根据集群还是分布式)应用程序;
第三步:启动nginx,并向主机暴露端口;
以下Nginx的配置文件只是简单的将上述同一个镜像启动的2个容器以负载均衡方式启动,供参考:
upstream task_server {
server task0:3000;
server task1:3000;
}
server {
listen 8080;
server_name _;
proxy_buffer_size 128k;
proxy_buffers 16 64k;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://task_server;
}
}
docker-compose 方式启动
命令的方式每次都需要好一顿敲,略麻烦。在确认容器的方案可行后,可以将启动的配置写成docker-compose的配置文件,通过一条命令就可以实现各种服务的启动
version: "3.5"
services:
task0:
image: task:latest
container_name: task0
networks:
- app
task1:
image: task:latest
container_name: task2
networks:
- app
nginx:
image: nginx:latest
container_name: nginx
networks:
- app
depends_on:
- task0
- task1
ports:
- 8080:8080
volumes:
- ${PWD}/tasks-nginx.conf:/etc/nginx/site-enabled/tasks-nginx.conf:ro
networks:
app:
name: app
driver: bridge
nginx启动时会检测上游服务是否可访问,所以必须等task容器先启动,然后启动nginx,所以有depends_on的配置。配置文件写完后,在配置文件所在目录执行命令,即可实现容器的启动:
$ docker-compose up -d
httpd
老牌的Web服务,常用于Java后端服务,使用逻辑跟nginx类似,不多做介绍了
$ docker run -dit --name my-apache-app -p 8080:80 -v ${PWD}:/usr/local/apache2/htdocs/ httpd:latest
traefik
我不熟悉这个反向代理,更无法在这里用很短的篇幅来介绍它,为了凑全这拼图,不得不面向搜索引擎快速学习。如果部署的是微服务架构的系统,那么traefik会相比nginx更好用。原生就兼容主流的集群技术。网上很多案例是以docker-compose的方式启动:
version: '3'
services:
reverse-proxy:
# The official v2 Traefik docker image
image: traefik:v2.9
# Enables the web UI and tells Traefik to listen to docker
command: --api.insecure=true --providers.docker
ports:
# The HTTP port
- "80:80"
# The Web UI (enabled by --api.insecure=true)
- "8080:8080"
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
然后可以通过8080端口访问dashboard。
工具箱
busybox
被成为Linux/Unix下的瑞士军刀工具,例如在我日常使用的ubuntu系统中已经被预装。命令方式执行:
busybox ls -lt
在苹果或Windows系统中执行、调试shell程序,就很方便。另外,在与其他容器联网后, 也可以方便执行一些例如健康检测、定时备份等任务。镜像文件只有不足3M,展开后也仅不足5M。
# 开启shell方式进入容器
$ docker run -it --network app --rm busybox sh
# 直接执行命令
$ docker run -it --network app --rm -v ${PWD}/scripts:/home busybox sh /home/app.sh
docker
emm,对这个镜像对我来说也是陌生的,简单的说就是套娃使用docker。一番搜索引擎学习后,发现有两种使用场景:在CI/CD中,需要开发一些基于docker的应用,此时推荐容器内的Docker共享宿主机的docker,具有最佳兼容性。缺点是这种场景下,容器内的docker直接会影响主机的docker环境,所以就相当于把docker的CLI给放进一个容器:
docker run -it -v /var/run/docker.sock:/var/run/docker.sock docker:latest
第二种场景,即两个完全独立的docker,虽然可以这样执行操作,官方不推荐,原因是可能会引发一些底层问题(注意,两者不是一个镜像)。
docker run --privileged -d docker:dind
一些问题
- 在一边整理上面的笔记一边操作以确认内容的正确性时,发现自己下再的镜像总是很久以前的。回忆起来自己的系统当时配置了国内的镜像(阿里云),于是将镜像配置移除,问题解决。
- 在配置mysql/mariadb时,启动容器后,总是无法客户端工具从宿主机连接到服务,折腾了很久。后来发现必须在容器第一次启动的时候先登录一次,然后就正常了(很奇怪,每找到具体原因和文档)。
- 以上15个最常用的镜像被单独使用的情景应该并不多,往往是组合使用,例如数据库+缓存+自定义应用程序镜像的方式。一个个地启动也很麻烦,虽好用
docker-compose一起运行一起关闭。如果关注的同学比较多,以后整理一篇docker-compose。 - 以上,这15个镜像虽然在
docker hub的首页,但并不一定是下载量最大的。超过10亿下载的镜像除了上述之外,还有rabbitmq,hello world,openjdk,golang,registry,wordpress,centos。 - 对这15个镜像,我也不是各个都熟悉,还希望有经验的同学批评和补充。