Docker Compose 如何设置开机自动启动?
为 Docker Compose 服务配置开机自动恢复:了解 restart 策略差异、为何 on-failure 无法应对一次重启,以及何时应使用 systemd 单元。
简短回答
Docker Compose 服务会在系统启动时启动,但必须同时满足两个条件。Docker daemon 必须已启用为系统服务,文件中的每个服务都必须设置 unless-stopped 或 always 重启策略。为每个服务添加 restart: unless-stopped,运行一次 docker compose up -d,容器就会在系统重启后自动恢复。对于常见情况,不需要其他配置。
只有在启动顺序很重要时,才需要 systemd 单元:例如,服务栈依赖已挂载的磁盘、VPN 接口,或 Docker daemon 启动时尚未就绪的网络共享。此类情况确实存在,本指南的后半部分会介绍。如果您还不熟悉服务定义和卷,请先阅读 VPS 上 Docker Compose 基础,然后再回来。
在 compose.yaml 中设置重启策略
每个服务单独设置一行策略。没有全局开关。因此,忘记设置策略的服务会在重启后保持停止状态,而堆栈中的其他服务会正常启动。
services:
app:
image: nginx:1.27
restart: unless-stopped
ports:
- "8080:80"
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD: change-me
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:应用配置,然后从正在运行的容器中读取重启策略:
docker compose up -d
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app)该命令会输出 unless-stopped。如果输出 no,说明文件已编辑,但容器从未重新创建。
这是最常见的故障。重启策略存储在容器中,而不是 YAML 文件中。编辑 compose.yaml 不会改变已经存在的容器。docker compose restart 也没有帮助,因为它只会停止并启动同一个容器对象,不会修改其配置。只有 docker compose up -d 会将文件与正在运行的容器进行比较,发现策略已更改后重新创建容器。
如果暂时不希望重新创建某个容器,可以直接修改其现有配置:
docker update --restart unless-stopped my-container同时也要编辑 YAML 文件。docker update 会修改正在运行的容器,而下一次执行 docker compose up -d 时会读取文件,并将旧值恢复。
各个重启值的实际作用
Docker 定义了 4 个值。它们之间的差异只会在机器重启或 daemon 重启时体现出来。
no是默认值。在任何情况下,容器都不会自动重启。always会在容器停止时重启容器。如果您手动停止容器,那么下次 Docker daemon 启动时,容器仍会重新运行。这通常令人意外:您上周主动停止的容器,在重启后又运行了。unless-stopped的行为类似always,但手动停止的容器在 daemon 重启后仍保持停止状态。对于需要偶尔停机维护的服务,应使用此值。on-failure仅在容器以非零退出码退出时重启。您可以限制重试次数,例如restart: on-failure:3。
如果某个 stack 只要服务器运行就应保持运行,unless-stopped 是合适的默认值。只有在希望容器不会长期保持停止状态时,才选择 always。
为何 restart: on-failure 无法在重启后保持生效
许多人选择 on-failure,因为它听起来比较稳妥,但第一次重启后却发现所有容器都已停止。原因取决于它的定义。on-failure 只对一种情况作出反应:容器进程以错误码退出。
重启不是错误。主机关闭时,systemd 会停止 docker.service,而 daemon 会主动停止每个容器。容器并未失败,因此该策略没有需要处理的事件。主机恢复启动后,daemon 会检查需要恢复运行的容器,而一个已被正常停止的 on-failure 容器不在其中。它会保持在 exited 状态。
您可以直接验证这一点。在某个服务上设置 restart: on-failure,运行 docker compose up -d,重启系统,然后运行:
docker compose ps -a该服务会显示为 Exited 状态,状态信息类似 Exited (0) 2 minutes ago。没有任何故障,也不会记录错误,这正是问题难以诊断的原因。该策略完全按照其定义运行。
on-failure 仍然有用。它适用于运行任务且可能崩溃的容器;您可以为其设置有限的重试次数,同时避免形成重启循环。对于需要在重启后持续运行的长期服务,它不是合适的工具。
重启策略仅在 Docker 服务随系统启动时生效
重启策略由 Docker daemon 强制执行。如果 daemon 未启动,就没有任何组件执行这些策略。检查方法如下:
systemctl is-enabled docker
systemctl is-enabled containerd两条命令都应输出 enabled。Docker 官方仓库中的软件包会在安装时启用这些单元,因此在全新的服务器上通常可以通过检查。如果任一命令输出 disabled,请修复:
sudo systemctl enable --now docker containerd这里有一个值得理解的陷阱。Ubuntu 还提供 docker.socket,它会在某个程序首次访问 Docker API 时按需启动 daemon。用户看到 docker.socket 已启用,就以为 daemon 已受到保障,于是为了节省内存而禁用 docker.service。系统启动时没有程序访问 API,因此 socket 从未被访问,daemon 也不会启动;直到您首次输入 docker 命令,容器才会启动。socket 激活不能替代启用 docker.service。
systemd unit 更合适的场景
重启策略无法定义与系统其他部分之间的启动顺序。daemon 启动后,会尽快启动容器。如果您的 stack 将独立卷、NFS(网络文件系统)共享或加密磁盘中的目录绑定挂载到容器中,容器可能会在该路径存在之前启动。Docker 会直接在挂载点创建一个空目录,然后让容器使用这个目录启动,导致数据库启动时没有数据。
出现以下任一情况时,请编写 systemd unit。stack 需要先准备好某个挂载点、VPN 接口或其他 unit。您希望 systemctl stop myapp 和 systemctl start myapp 的行为与该服务器上的其他服务一致。或者,您希望在关机期间正常停止 stack,而不是让它与 daemon 一起被强制终止。如果您不熟悉 systemd unit,编写 systemd service 和 timer会更详细地介绍文件格式。
编写 systemd 单元
将整个堆栈放在主目录之外的固定路径中。/srv/myapp 是不错的选择,因为在任何人登录前运行的单元不应读取 /home。
创建 /etc/systemd/system/myapp.service:
[Unit]
Description=myapp docker compose stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
RequiresMountsFor=/srv/myapp/data
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target启用并启动它:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.service运行正常的单元会显示 Active: active (exited)。第一次看到时,这看起来似乎不对。实际上这是正确的:Type=oneshot 和 RemainAfterExit=yes 表示单元已执行其命令,命令已完成,而 systemd 会继续将单元标记为 active,以便在关机时运行 ExecStop。
每一行都有明确作用。Requires=docker.service 表示单元会快速失败,而不是在失效的套接字上运行 docker compose。After= 设置启动顺序,因为 Requires= 本身不会设置顺序。RequiresMountsFor= 会让 systemd 加载该路径对应的挂载单元并等待它完成,这正是应使用单元而不是 restart policy 的原因。TimeoutStartSec=0 可防止大型镜像仍在拉取时,systemd 终止启动任务。
下面说明如何组合这两种机制。Docker 文档建议不要将 restart policy 与主机进程管理器混用。该警告针对的是直接监管容器进程的进程管理器:它会在 daemon 尝试执行相同操作时重启容器进程。Type=oneshot 单元不会监管任何进程,因此在 compose 文件中保留 restart: unless-stopped,并同时使用此单元是没有问题的,而且这正是所需配置。systemd 负责启动时的顺序,daemon 负责处理凌晨三点崩溃的容器。
如果需要保持运行的是普通的长时间运行进程,而不是一个堆栈,单元的写法会有所不同。因为此时没有底层 daemon,必须由 systemd 自身的 Restart= 负责监管;在 systemd 后台运行无头 dsh 是这种结构的完整示例,其中还包括专用用户和 journal 配置。
通过实际重启验证
实际测试无法替代。systemctl restart docker 不会测试挂载顺序,先执行 docker compose down 再执行 docker compose up -d 则完全不会测试启动过程。
sudo reboot等待系统重启,重新连接,然后按以下顺序检查:
uptime
systemctl is-active docker
docker compose psuptime 可确认你连接的确实是刚刚重启过的计算机。在 stack 目录中执行 docker compose ps,应列出每个服务,并显示为 running,其运行时间应接近计算机的运行时间。显示为 Exited 的服务就是需要检查的对象。
如果某个服务未能启动,守护进程日志会记录启动时间段内的情况:
journalctl -u docker.service -b --no-pager | tail -50对于由 unit 管理的 stack,journalctl -u myapp.service -b --no-pager 会显示启动时的完整 docker compose 输出,包括镜像拉取失败或缺少 .env 文件等错误。你安排的这次重启就是需要监控的对象,因此应让 unit 告知你那些未被监控的重启:指向自托管 ntfy 服务器的 OnFailure= 行,可以将 stack 未能恢复运行的情况转换为推送通知,而不是等到几天后才发现。
会悄悄导致自动启动失效的情况
使用 docker compose run 创建的容器不会从文件中获取 restart policy。Compose 会将它们视为一次性容器。如果某个服务似乎没有遵循其策略,请检查它是否使用了 run,而不是 up 启动。
卷或 env_file 条目中的相对路径,会相对于 compose 文件所在目录解析。从 shell 中运行时可以正常工作;从设置了 WorkingDirectory 的单元中运行时也可以正常工作。没有设置该项的单元会失败,因为此时工作目录是 /。
Rootless Docker 需要单独处理。daemon 以用户服务运行,而用户的最后一个会话结束后,用户服务也会停止。请为该用户启用服务,并允许它在没有用户登录时继续运行:
systemctl --user enable docker
sudo loginctl enable-linger $USER如果没有 enable-linger,rootless daemon 会在您退出登录时关闭,容器也会随之停止。这看起来与 restart policy 失效完全相同。
还有一点。自动安全更新可能会在固定时间重启服务器。只有在您的服务栈能够自行恢复时,这才是好事。在新机器上完成这项配置,应与首次设置时的其他工作一起进行,参见新 VPS 的前十分钟。
FAQ
restart: always 和 restart: unless-stopped 有什么区别?
两者都会在容器自行停止时重新启动。区别在于您手动停止容器之后的行为。使用 always 时,Docker daemon 下次启动时会再次启动该容器,因此重启系统会取消您的手动停止操作。使用 unless-stopped 时,daemon 会记住该容器是被主动停止的,并保持其停止状态。除非您明确需要容器保持停止状态,否则请使用 unless-stopped。
我添加了 restart: unless-stopped,但容器重启后仍未启动。为什么?
重启策略保存在容器上,而不是文件中。编辑 YAML 不会更新已经存在的容器。运行 docker compose up -d,让 Compose 重新创建容器,然后使用 docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app) 确认。如果该命令输出 no,说明该容器是在您编辑配置之前创建的。另一个常见原因是未启用 docker.service。您可以使用 systemctl is-enabled docker 检查。
如果已经使用重启策略,是否还需要 systemd 单元?
通常不需要。对于只依赖网络的堆栈,重启策略已经足够,而大多数堆栈都属于这种情况。如果容器依赖 Docker daemon 启动时尚未就绪的组件,则应添加单元,例如外部磁盘、加密卷、NFS 共享或 VPN 接口。该单元可以通过 After= 和 RequiresMountsFor= 控制启动顺序,而重启策略无法表达这些依赖关系。
如何永久停止堆栈,避免它在下次重启后再次启动?
使用 unless-stopped 时,docker compose stop 就足够了,因为手动停止的容器不会在 daemon 重启时恢复。使用 always 时,仅停止容器还不够,系统重启后容器会再次启动。您可以运行 docker compose down 删除容器,或者先使用 docker update --restart no my-container 更改重启策略。如果 systemd 单元管理该堆栈,还要运行 sudo systemctl disable myapp.service,否则该单元会再次启动堆栈。