日志
- 2023-04-18:优化image的latest标签的相关说明。
- 2023-04-15:新增关于端口号和tag号的提示。
前言
docker run转化为compose:composerize
最近为了解决OpenClash规避PT下载器流量的问题,我尝试了docker的macvlan网络模式。在那个时候,我忽然发现,自己虽然在Docker系列教程里广泛使用docker compose,但似乎从未系统地讲解过为什么推荐使用docker compose; docker compose和一般的docker run有什么区别;docker compose有哪些常用模块;等等。所以这里临时补充一期相关内容,带大家了解docker-compose的设计逻辑和基本结构。当然,本章只是讲一些docker-compose较为常见的用法;更加详细的教程请参考官方文档。
实例1
这里我会展示一个简单的docker-compose实例。一般,我们都是在某个文件夹里建立一个名为docker-compose.yml
的文件,加入类似下面的内容:
version: '3'
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
其实这个文件名可以随意指定;不过,如果你使用了其它名字,你需要用-f
或--file
特别指定某个配置文件。另外,眼尖的小伙伴可能发现,这就是我在《Docker系列 两大神器NPM和ddns go的安装》提供的Nginx Proxy Manager的docker-compose安装脚本。那么,里面的内容是什么意思呢?
首先,我们要先理清docker-compose的层级结构。对于上面的代码,层次关系大致如下图所示:
为了方便书写和观看,一般后一层级比前一层级后退2个空白字符或1个tab。Docker compose对于该格式的要求是挺严格的,有点不对就容易报错。
-
第1级中,
version
和services
是同一级参数。这个version只是为了方便帮助我们记录这个docker-compose.yml是什么版本,并不参与docker的实际进程。services
就是为了表明我们要开启一个服务。某个Service和它所在的network等组成的整体通常被称为Stack
;一般来说,Stack可由1个或多个Service组成 ,而一个Service可由1个或多个Container(即容器)组成。有点套娃的感觉了(ฅ´ω`ฅ) -
第2级的
app
其实是可以随便取的,你取dog
、cat
之类的都无所谓;但不同的Container不可以重复。它是Container在这个stack中的“小名”;在这个局域网中,如果你想和该Container进行数据交换,可以使用“小名”代替IP作为唯一ID。我自己的习惯是,这个Service里的核心应用称为app
;数据库称为db
;缓存称为cache
;其它应用则另外约定。这样的好处是,只看“小名”就可以大致猜到某Container在该Service中的基本角色。如果你看过我的其它Docker教程,基本上都遵循了这个习惯。 -
第3级定义了该Container的主要参数,比如使用哪个
image
;重启策略(restart
);端口映射(ports;从Container到Host);目录映射(volumes;从Container到Host);等等。 -
第4级定义了某Container参数的具体设置,比如要映射哪个端口或目录;等等。
值得注意的是,类似80:80
的映射是一种很常用的端口映射关系,代表宿主端口:容器端口
。一般容器端口
是由image定义好的,不能修改;但宿主端口
则可以随心所欲,只要不与现用端口冲突即可。
另外,jc21/nginx-proxy-manager:latest
后加:latest
是一种比较“偷懒”的做法——我不知道用哪个tag,你给爷来个最新版。实际上,从日后迁移的角度看,这并不是一种推荐的做法。最好的做法是你切实知道需要用哪个版本,然后填入具体的tag号,比如jc21/nginx-proxy-manager:github-pr-2816
、jc21/nginx-proxy-manager:2.10.2
之类的。你可以在dockerhub里面查看某个image的最新tag号,比如NPM的就在这里。结合RSShub,你甚至可以直接订阅docker image的tag更新:
这样我们就可以实时了解该image的状态并决定是否更新至某个版本。这也是我不建议大家使用:latest
或不加tag号(默认使用latest)的原因之一——它们不利于进行迁移操作(不同时间的latest实际上代表不同的版本)。另外,知道tag号还有利于提高image复用率。比如,缓存(如redis
)、数据库(如mysql
)等image经常被多个stack共用,使用不一致的tag号会重复下载类似的image,从而增加image相关的容量负荷。这在tag号规范管理的前提下是可以很大程度上避免的。这种感觉在你迁移/升级nextcloud时将会特别明显。
不过,有某些情况下是推荐使用:latest
的
- WordPress是建议使用
:latest
标签的,因为wordpress更新可以在后台进行,不需要特别去升级某个镜像。 - 需要/有必要频繁更新的镜像,比如
diygod/rsshub:chromium-bundled
或Kerwin1202/chatgpt-web
。带有:latest
标签的docker image更新很简单,只需要:
docker-compose down # 下线
docker-compose pull # 拉取latest镜像
docker-compose up -d # 重新上线
从机器的角度上看,除了网络层,docker-compose.yml
和docker run
之类的命令并没有本质区别的。上面的docker-compose.yml
可以部分地简化成下面的Linux命令:
docker run \
-p 80:80 \
-p 81:81 \
-p 443:443 \
-v <工作目录>/data:/data \
-v <工作目录>/letsencrypt:/etc/letsencrypt \
jc21/nginx-proxy-manager:latest
总之,这个简单的例子表明:docker-compose.yml
实际上是将docker命令里的严格等级关系进行良好的可视化。具体来说,就是使用者很容易看清它所表达的内涵;甚至Stack里有很多Container时,其中的层级关系也不难看清。运行一个复杂Stack的Linux命令是比较难写的,也不够直观。
实例2
这里我们看一个更加复杂的docker-compose.yml
,它取自我在《Docker系列 WordPress系列 搭建WordPress个人博客》中安装wordpress的脚本:
version: '3.0'
services:
db:
# arm的机器, mysql:5.7请改成mysql:oracle
image: mysql:5.7
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpassword # 按需修改
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: yourpassword # 按需修改
volumes:
- './db:/var/lib/mysql'
networks:
- default
app:
image: wordpress:latest
restart: unless-stopped
ports:
- 4145:80 # 按需修改。与防火墙开放端口一致。
environment:
WORDPRESS_DB_Host: db
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: yourpassword # 按需修改
volumes:
- './app:/var/www/html'
links:
- db:db
depends_on:
- redis
networks:
- default
redis:
image: redis:alpine
restart: always
volumes:
- ./redis-data:/data
networks:
- default
networks:
default:
name: wordpress
首先,这个脚本多了一些#
标记的文字。和大多数编程语言一样,它们是注释,在实际运行时并不生效,一般的作用就是给读者提供一些重要信息,或传达脚本作者的某些思想。
在这里,Services
里有3个Container的“小名”,分别叫app
、db
和redis
。它们分别对应wordpress、mysql和redis缓存等应用。
在app
这个应用里,我们还可以看到一些特别的Container参数:
links:
- db:db
depends_on:
- redis
这个links - db:db
的表达提示app
这个应用会使用db
;depends_on: redis
表明redis
要在app
之前启动且在之后停止。因此,虽然links
和depends_on
两者均涉及依赖关系,但 depends_on
主要表明应用必须启动和停止的顺序,而links
表明容器间存在网络通信。不过,实际使用中我感觉差别不大,有深入使用体会的小伙伴可以评论区留言。
我们还看到一个新的第1级变量networks
。其实它一直是存在的。在实例1
中,它是一个隐藏变量,但却实际生效。我们再仔细地观察各应用与network的关系:
version: '3.0'
services:
db:
...
networks:
- default
app:
...
networks:
- default
redis:
...
networks:
- default
networks:
default:
name: wordpress
这里其实就是说:db
/app
/redis
均使用一个叫default
的本地网络,它在docker networks中的名字为wordpress
。这是一种比较简单的情况,也是我比较常用的写法,它可以帮助我对某个Stack的个性化网络起个好听的名字。实际上,这里networks是可以不同的;一个Stack可以同时使用1个或多个network。
总之,从本实例中,我们可以感受:
- docker-compose的应用前后端关系十分清晰明了
- docker-compose可以定义复杂的网络结构和依赖关系
实例3
实例1和实例2均会产生一个独特的Stack局域网。一般来说,在docker networks中,不同的Stack网段可以通过172.17.0.1
通讯。这个地址类似于路由器中的网关地址192.168.1.1
一样——172.17.0.1
是docker networks的网关(可能理解不太准确 (ฅ´ω`ฅ))。这也是我在使用Nginx Proxy Manager反代相邻docker应用时,喜欢反代http://172.17.0.1:端口
地址的原因,因为这样IP地址一致比较好记,我只要关注某个应用的端口号就行;而且可以避免搜索更高级别的网络,也是一种更加安全、纯粹和高效的用法。
不过,其实Stack网络可以更具有侵略性——直接给Stack分配一个和宿主机同网段的IP!据我所知,macvlan
就是其中一种可靠的方式。
比如,我新建一个网络:
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=ens18 \
PTnetwork
其中,--subnet
指定了一个CIDR地址192.168.1.0/24
,它对应的是一个长度为256的网段(查询网址):
--gateway
指定了这个网段的网关地址,即192.168.1.1
。其实这个网段/网关对应的就是最常用的家庭网络地址方案。-o parent=ens18
对应的本机的网卡。PTnetwork
是我为这个docker network起的名字,意为“给PT应用提供的网络”。具体的设置我暂不展开。
总之,上述命令的意思是:建立一个名为PTnetwork
的docker network,它在ens18
网卡的192.168.1.0/24
子网中,网关为192.168.1.1
。如果你的宿主机/家庭局域网也是使用这个网段,这个docker network就顺利地“寄生”到了家庭局域网中;而这个docker network的Container就变成了一台台具有独立IP的局域网“设备”。因此,路由器的DHCP服务会给他们分配IP;不过一般我们都是使用静态IP的方式,即直接指定某Container的IP。
这里是一个我正在使用的transmission的docker-compose.yml
脚本:
---
version: "2.1"
services:
transmission:
image: chisbread/transmission:version-3.00-r13.1
restart: unless-stopped
Container_name: transmission
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
- USER=11 #optional
- PASS=1111111111 #optional
volumes:
- ./config:/config
- ./downloads:/downloads
- ./watch:/watch
ports:
- 9091:9091
- 51413:51413
- 51413:51413/udp
networks:
macvlan_network:
ipv4_address: 192.168.1.167 # 同网段IP
networks:
macvlan_network:
name: PTnetwork # 之前搞好的IP
我们关注其中和网络有关的命令:
...
networks:
macvlan_network:
ipv4_address: 192.168.1.167 # 同网段IP
networks:
macvlan_network:
name: PTnetwork # 之前搞好的IP
脚本意思是:该Container(名为transmission
)使用一个叫macvlan_network
的网络,并使用192.168.1.167
作为transmission
的静态IPv4地址。macvlan_network
是一个存在于Stack外部(external
)的docker networks,在那里它的名字叫PTnetwork
。
这样,在实际使用时,transmission就会拥有一个独立的局域网IP。这在设置OpenClash的网络时具有额外好处,因为OpenClash Fake-IP模式下,如果使用防火墙转发的方法进行本地DNS劫持,则可以设置不走代理的局域网设备IP。如些一来,transimission下载/上传PT的流量(往往是恐怖的大)就不会损耗节点流量(往往是有限的少),从而达到规避PT流量的目的。
据我所知,这种网络模式还常见于某些容器云的部署中,它们往往要求多个类似的容器云,但又要求有独立的局域网IP。
小结
Docker compose浅易易懂,但却可以支持非常复杂的功能。利用Docker compose可以使docker布署变得简单而强大。本文所描述的特性也是我系列教程里都十分推崇docker-compose的主要原因。
docker-compose.yml
就像是一个严格记录和执行你曾经执行过的docker run
等操作;而你想要再次执行的时候,只需要简单的docker-compose up -d
。因为只是一个文本文件,想要备份它也是轻而易举。我以前也是用docker run
或后台GUI之类的方式使用docker的。但是深刻了解docker-compose后,我基本已经抛弃了那些旧方式,它们实在是弱爆了!
如果你是docker初学者,看完本文后,希望你也可以尽快过渡到docker-compose的使用喔!难度应该不大的,跨过去就行了!
扩展阅读
- Wowu/docker-rollout: 🚀 Zero Downtime Deployment for Docker Compose: 不停机更新 Docker Compose 里面的某个服务。原理是同时新建两个实例,用已更新的实例替换未更新的实例。From 科技爱好者周刊(第 251 期):国产单板机值得推荐 – 阮一峰的网络日志
---------------
完结,撒花!如果您点一下广告,可以养活苯苯😍😍😍
ipv4_address: 192.168.1.167 # 同网段IP 这个配置ip无法生效,我的会报错。。 services.qbittorrent1 Additional property ipv4_address is not allowed 有救吗
我找ai问了一下ai说docker compose不支持这样我傻眼了
你使用的docker-compose版本、docker版本是什么呀 ~ 我这边是可以正常使用喔! 有可能不同的版本,语法有点不同。
Docker Compose version v2.7.0
Docker version 20.10.3, build b35e731 我这算是远古版本吗😂
解决了吗老哥 ~
ai有时候也不是对的 ~ 它只是提供解决问题的思路,很多时候不能直接解决问题
学会了,不涉及这些依赖关系的话,有没有直接把docker run命令直接转换成docker compose文件的应用。。。
有的,比如: https://www.composerize.com/
如果两个compose写的内容都用db这个会怎么样,会安装两个容器吗
是又不是。 “是”——如果两个在不同目录的docker-compose都安装了一个叫db的容器,默认情况下该db的容器名是
文件夹名-db-1
而不是db
。由于目录的名字一般是不重复的,所以两个容器是不冲突的。总之,docker-compose一定会保证不同容器有不同的容器名。“不是”——虽然有两个容器,但是只需要一个镜像(image)。 因此,除了卷(volume)的体积外,镜像是不会重复下载的(除非你使用了两个不同版本的镜像)。
具体可以见这个文章的介绍: Docker系列 了解Docker Compose的配置文件。
不过,我觉得理解这件事最快的方法——就是你自己试试看。 加油喔 (ฅ´ω`ฅ)
那如果两个compose想要复用同一个数据库,第二个服务有办法用db-1吗,主要不懂compose保存的时候是否要带上依赖服务,如果有很多容器想备份,怎么样才能性能和速度平衡,如果假定很compose都有db,那创建出很多db服务,那不是就很浪费服务器性能了
你说的对,像你说的那种方式也是行得通的。 你可以设置一个db,然后其它的前端应用与它交互。只要它们和这个db处于同一个局域网段就行。不过,我比较少这样做。 我比较喜欢一个stack一个db——虽然占用资源,但是方便备份和迁移。 比如,如果我要将chatgpt-web迁移到其它服务器里,我直接将整个根目录复制过去就行,完全不会影响其它应用的使用。 这其实是个人喜好问题,没有必然要怎么做。 此外,本教程中使用的mongodb的volume还蛮大的,最省资源的方案确实是共用一个db。不过我就装了2个chatgpt-web(一个API一个Access Token),还hold得住 Σ( ° △ °|||)︴
明白了!感谢博主,就是最麻烦的还是数据迁移和备份,所以牺牲点性能是为了更方便!如果有特别需求,再另外特别处理。
差不多。我是用资源换方便和稳定。 至少个人用户可以这样折腾。 至于企业环境是怎么样,我也不太清楚。没有这方面的从业经验 (~ ̄▽ ̄)~
学习了!!!
略懂一些 多多交流哈! 推荐加tg群: https://t.me/benszhub
一字不落的看完了,受益匪浅,感谢苯苯
其实我对docker-compose的了解也是比较粗浅了,哈哈!不过,日常使用足够了。加油喔