关于
在使用Docker来打包程序时,肯定会了解到WatchTower这么一个东西。它会自动监视指定的Container,若在Registry中发现了新的版本后就会主动拉取并且自动重启这个Container。但是不得不承认,WatchTower其实从来就没有被我正确使用过。
问题
问题就出在我的每个Container都是由SupervisorD监视并且在启动时自动拉起这件事情上。事实上这两个管理工具互相是有冲突的:当Container被WatchTower重启后,此时的PID就会更改,但是SupervisorD监视的仍然是老的PID,它会发现PID对应的进程已经被结束(WatchTower结束的),于是SupervisorD开始尝试重新启动该Container。而事实上,Container已经被WatchTower重启成功,所以SupervisorD会报告该Container无法启动,尝试数次后失败退出,并且标记该Container处于Fail状态。但是真实的情况是,Container启动成功,服务在运行,而SupervisorD上的报错是错误的。
寻求解决办法
其实简单的解决办法就是,不要使用WatchTower来做更新和重启。但是WatchTower在我的脑子里一直处于非常重要的位置,所以我从来没有想过不要使用它,因为我总觉得更新和重启应该是一件非常重要和不能出错的任务。重要性,的确是的。但其实更新和重启这两件事情,WatchTower也是使用Docker内建的功能来完成的,如果使用其它方式来调用和使用WatchTower来调用,可靠性差别并不大。
在和Grok不断的讨论这个话题后,也去了解了k8s和k3s。但是,在我坚持不要把系统安装上k8s这么庞大的东西只是为了解决自动更新和重启Container这个简单的逻辑之下,Grok终于给出了让我比较满意的方法。它帮我写了一个系统脚本,让我使用SystemD的Timer功能每隔一段时间自动运行一下,这个脚本的功能就是监视指定的Container然后发现新版本就Pull和Update,最后调用SystemD来重启这个进程。而对于WatchTower,这个Star数量有两万多,近百人一起开发的一个开源项目,我一时间对它感觉到很迷茫,它到底在干什么?官方和在Reddit上用户都说,它的主要功能就是更新和重启Container,而它又和SystemD及SupervisorD天然有冲突。同时,官方和Reddit上都建议不要在生产环境上使用它,并且Reddit上还有人建议说,如果你需要更加保险的方法来更新你的Container,请使用一个自定义的脚本,这不就是Grok最后给出来的答案吗。
脚本
这段脚本就是Grok给出的更新脚本,我按照自己的习惯稍做了修改。手动执行一次就会检查SERVICES数组中的所有指定了名称的Container,如果有新的版本就更新它,然后使用SystemD的命令对齐进行重启。
#!/bin/bash
# file:/usr/local/bin/update-docker-services.sh
SERVICES=("container-name-1" "container-name-2")
for service in "${SERVICES[@]}"; do
image=$(docker inspect --format '{{.Config.Image}}' "$service" 2>/dev/null)
if [ -n "$image" ]; then
if docker pull "$image" | grep "Downloaded newer image"; then
systemctl stop "$service"
systemctl start "$service"
echo "Updated and restarted $service"
else
echo "No update for $service"
fi
else
echo "Service $service not found"
fi
done
下面两个文件分别是这个更新脚本的服务和定时器,在SystemD里,一个服务需要对应一个定时器,定时器在指定的时间里会执行这个服务,而这个服务就是执行这个更新脚本。定时器可以指定服务文件名。若没有指定,则会执行与其名称相同的那个service文件,所以这两个脚本文件除了扩展名不同外,文件名是一样的。
# file:update-docker-services.service
[Unit]
Description=Update Docker images and restart services
[Service]
Type=oneshot
ExecStart=/usr/local/bin/update-docker-services.sh
# file:update-docker-services.timer
[Unit]
Description=Run Docker image update interval
[Timer]
OnCalendar=*:0/3 # 每3分钟执行一次
[Install]
WantedBy=timers.target
这里给出一个Container执行的service脚本的例子。要注意的是参数“–name”指定的名称要和“/usr/local/bin/update-docker-services.sh”中的SERVICES数组中的名称一至。
# file:your-container.service
[Unit]
Description=Your container service
After=docker.service
Requires=docker.service
[Service]
ExecStart=/usr/bin/docker run --name container-name-1 -p 20010:3000 ghcr.io/xxxx/container-name-1:main
ExecStop=/usr/bin/docker stop -t 10 container-name-1
ExecStopPost=/usr/bin/docker rm container-name-1
Restart=always
RestartSec=60
[Install]
WantedBy=multi-user.target
执行
在脚本保存好后,要记得使用chmod +x /usr/local/bin/update-docker-services.sh
来对脚本添加可执行属性。同时使用systemctl enable update-docker-services.timer
来保证timer在系统启动的时候被执行。最后请使用systemctl start update-docker-services.timer
来启动这个timer。
如果使用root用户来执行update-docker-services.sh,那么要注意的是,那些需要授权才能登录的registry是否在/root/.docker/config.json
里是否已经保存了正确的授权信息。如果没有,则要记得给root用户授权,否则更新Container会因为没有权限而失败。同样,若使用其它的用户来更新Container,也请注意该用户的HOME目录下的登录授权情况。