Docker Compose tự khởi động sau khi reboot
Cấu hình Docker Compose tự chạy sau reboot bằng restart policy, hiểu vì sao on-failure không khởi động lại sau một lần dừng và khi nào nên dùng systemd unit.
Câu trả lời ngắn gọn
Các service trong Docker Compose sẽ khởi động khi boot nếu đồng thời thỏa mãn 2 điều kiện. Docker daemon phải được enable dưới dạng system service, và mỗi service trong file phải có restart policy là unless-stopped hoặc always. Thêm restart: unless-stopped vào từng service, chạy docker compose up -d một lần, rồi các container sẽ tự khởi động lại sau mỗi lần reboot. Với trường hợp thông thường, không cần làm gì thêm.
Bạn chỉ cần systemd unit khi thứ tự khởi động quan trọng: ví dụ stack phụ thuộc vào disk đã mount, VPN interface hoặc network share chưa sẵn sàng tại thời điểm Docker daemon khởi động. Đây là trường hợp thực tế và được trình bày trong nửa sau của hướng dẫn này. Nếu bạn vẫn đang làm quen với service definition và volume, hãy bắt đầu với kiến thức cơ bản về Docker Compose trên VPS rồi quay lại.
Đặt restart policy trong compose.yaml
Policy được khai báo một dòng cho từng service. Không có tùy chọn global. Vì vậy, service bị bỏ sót sẽ vẫn dừng sau khi reboot, trong khi các service còn lại trong stack khởi động bình thường.
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:Áp dụng thay đổi, rồi đọc lại policy từ container đang chạy:
docker compose up -d
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app)Lệnh này in ra unless-stopped. Nếu in ra no, nghĩa là file đã được chỉnh sửa nhưng container chưa được recreate.
Đây là lỗi thường gặp nhất. Restart policy được lưu trên container, không phải trong file YAML. Chỉnh sửa compose.yaml không thay đổi container đã tồn tại. docker compose restart cũng không giải quyết được, vì lệnh này chỉ stop rồi start cùng một container object mà không thay đổi cấu hình. Chỉ docker compose up -d mới so sánh file với các container đang chạy, phát hiện policy đã thay đổi và recreate chúng.
Với container chưa muốn recreate ngay, hãy thay đổi policy trực tiếp:
docker update --restart unless-stopped my-containerBạn vẫn phải chỉnh sửa file YAML. docker update thay đổi container đang chạy, còn lần chạy docker compose up -d tiếp theo sẽ đọc file và khôi phục giá trị cũ.
Thực tế mỗi giá trị restart hoạt động như thế nào
Docker định nghĩa 4 giá trị. Sự khác biệt giữa chúng chỉ thể hiện khi máy reboot hoặc daemon được restart.
nolà giá trị mặc định. Container không bao giờ được tự động restart trong bất kỳ trường hợp nào.alwaysrestart container mỗi khi container dừng. Nếu bạn tự dừng container, nó vẫn chạy lại vào lần Docker daemon khởi động tiếp theo. Điều này thường gây bất ngờ: container bạn chủ động dừng từ tuần trước lại đang chạy sau khi reboot.unless-stoppedhoạt động giốngalways, nhưng container được tự dừng sẽ tiếp tục ở trạng thái đã dừng sau khi daemon restart. Đây là giá trị phù hợp cho service mà bạn thỉnh thoảng dừng để bảo trì.on-failurechỉ restart container khi container thoát với exit code khác 0. Bạn có thể giới hạn số lần thử, như trongrestart: on-failure:3.
Với một stack chỉ cần luôn chạy khi server đang chạy, unless-stopped là giá trị mặc định phù hợp. Chỉ chọn always khi bạn muốn container không dễ bị để ở trạng thái đã dừng.
Vì sao restart: on-failure không hoạt động sau reboot
Nhiều người chọn on-failure vì tùy chọn này có vẻ an toàn, rồi phát hiện mọi container đều đã dừng sau lần reboot đầu tiên. Nguyên nhân nằm trong định nghĩa của nó. on-failure chỉ phản ứng với một việc: tiến trình container thoát với mã lỗi.
Reboot không phải là lỗi. Khi host tắt, systemd dừng docker.service, rồi daemon chủ động dừng từng container. Container không bị lỗi, nên policy không có gì để phản ứng. Khi hệ thống khởi động lại, daemon kiểm tra những container cần resume. Container on-failure đã được dừng sạch sẽ không nằm trong số đó. Nó vẫn ở trạng thái exited.
Bạn có thể kiểm tra trực tiếp. Đặt restart: on-failure cho một service, chạy docker compose up -d, reboot, rồi chạy:
docker compose ps -aService sẽ được liệt kê với trạng thái Exited và status như Exited (0) 2 minutes ago. Không có gì bị hỏng và cũng không có lỗi nào được ghi vào log, nên tình huống này khó chẩn đoán. Policy đã thực hiện đúng những gì nó được định nghĩa để làm.
on-failure vẫn hữu ích. Nó phù hợp với container chạy một job và có thể bị crash. Tự nó, restart: on-failure không đặt giới hạn và sẽ tiếp tục restart một job liên tục bị lỗi. Vì vậy, hãy thêm giới hạn như restart: on-failure:3 khi bạn muốn số lần retry có giới hạn và không muốn tạo restart loop. Đây là công cụ không phù hợp để giữ một service chạy liên tục qua các lần reboot.
Restart policies chỉ hoạt động nếu Docker service khởi động cùng hệ thống
Restart policies được thực thi bởi Docker daemon. Nếu daemon không khởi động, không có gì thực thi các chính sách này. Hãy kiểm tra:
systemctl is-enabled docker
systemctl is-enabled containerdCả hai lệnh nên in ra enabled. Các gói từ repository chính thức của Docker sẽ tự động enable chúng khi cài đặt, nên trên một server mới, bước này thường đạt yêu cầu. Nếu lệnh nào in ra disabled, hãy sửa lại:
sudo systemctl enable --now docker containerdCó một cái bẫy cần lưu ý ở đây. Ubuntu cũng cung cấp docker.socket, nó khởi động daemon theo yêu cầu ngay lần đầu tiên có tiến trình giao tiếp với Docker API. Người dùng thấy docker.socket được enable, tưởng rằng daemon đã được quản lý, rồi disable docker.service để tiết kiệm bộ nhớ. Khi boot, không có gì gọi API, socket không được chạm tới, daemon không khởi động, và không có container nào chạy cho đến khi bạn gõ lệnh docker đầu tiên. Socket activation không thay thế được việc docker.service phải được enable.
Khi systemd unit là lựa chọn phù hợp hơn
Các chính sách restart không có khái niệm về thứ tự khởi động so với phần còn lại của hệ thống. Daemon khởi động rồi đưa các container lên ngay khi có thể. Nếu stack bind-mount một thư mục từ volume riêng, share NFS (network file system) hoặc ổ đĩa được mã hóa, các container có thể khởi động trước khi đường dẫn đó tồn tại. Docker sẽ tự tạo một thư mục rỗng tại mount point rồi khởi động container với thư mục đó, khiến database khởi động mà không có dữ liệu.
Hãy viết một systemd unit khi có bất kỳ trường hợp nào sau đây. Stack cần một mount, một VPN interface hoặc một unit khác sẵn sàng trước. Bạn muốn systemctl stop myapp và systemctl start myapp hoạt động giống như với mọi service khác trên máy. Hoặc bạn muốn stack được dừng đúng cách trong quá trình shutdown thay vì bị kill cùng daemon. Nếu systemd unit còn mới với bạn, viết systemd service và timer sẽ trình bày chi tiết hơn về định dạng file.
Viết systemd unit
Đặt stack trong một path cố định bên ngoài home directory. /srv/myapp là lựa chọn phù hợp, vì một unit chạy trước khi có người đăng nhập không cần đọc /home.
Tạo /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.targetEnable và start unit:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.serviceMột unit hoạt động bình thường sẽ hiển thị Active: active (exited). Lần đầu thấy trạng thái này, bạn có thể nghĩ nó không đúng. Thực ra đây là trạng thái chính xác: Type=oneshot với RemainAfterExit=yes có nghĩa là unit đã chạy command, command đã hoàn tất, và systemd vẫn đánh dấu unit là active để ExecStop chạy khi shutdown.
Mỗi dòng đều có lý do. Requires=docker.service khiến unit fail nhanh thay vì chạy docker compose với một socket đã chết. After= thiết lập thứ tự, vì chỉ có Requires= thì chưa đủ. RequiresMountsFor= khiến systemd load mount unit cho path đó và chờ mount hoàn tất. Đây chính là lý do dùng unit thay vì restart policy. TimeoutStartSec=0 ngăn systemd kill start job trong khi một image lớn vẫn đang được pull.
Lưu ý khi kết hợp hai cơ chế này. Tài liệu Docker khuyến cáo không kết hợp restart policy với process manager ở cấp host và chỉ nêu rằng cách này tạo ra conflict. Conflict xảy ra khi process manager giám sát chính container process và restart nó trong lúc daemon cũng đang cố làm điều tương tự. Một unit Type=oneshot không giám sát process nào, nên giữ restart: unless-stopped trong compose file cùng với unit này là hoàn toàn ổn, và đó là cách bạn muốn. systemd xử lý thứ tự khi boot, còn daemon xử lý container bị crash lúc 3 giờ sáng.
Unit sẽ có cấu trúc khác khi thứ bạn cần giữ chạy là một process chạy lâu dài thông thường thay vì một stack. Khi đó không có daemon bên dưới, và Restart= của systemd phải tự giám sát process đó. chạy dsh headless phía sau systemd là ví dụ hoàn chỉnh cho mô hình này, bao gồm cả user riêng và journal.
Xác minh bằng một lần reboot thực tế
Không có gì thay thế được kiểm tra thực tế. systemctl restart docker không kiểm tra thứ tự mount, còn docker compose down theo sau bởi docker compose up -d không kiểm tra bất kỳ yếu tố nào liên quan đến quá trình boot.
sudo rebootChờ, kết nối lại rồi kiểm tra theo thứ tự này:
uptime
systemctl is-active docker
docker compose psuptime xác nhận bạn đang kiểm tra một máy thực sự đã reboot. docker compose ps, chạy trong thư mục stack, phải liệt kê mọi service ở trạng thái running với uptime gần bằng uptime của máy. Service hiển thị Exited là service cần kiểm tra.
Nếu có thành phần không khởi động, log của daemon sẽ bao quát khoảng thời gian boot:
journalctl -u docker.service -b --no-pager | tail -50Đối với stack được quản lý bằng một unit, journalctl -u myapp.service -b --no-pager hiển thị đầy đủ đầu ra docker compose từ lúc boot, bao gồm cả lỗi pull image hoặc thiếu file .env. Lần reboot bạn lên lịch là lần cần theo dõi, vì vậy hãy để unit cho bạn biết về những lần còn lại: một dòng OnFailure= trỏ đến ntfy server tự host sẽ biến stack không khởi động lại được thành một push notification thay vì để bạn chỉ phát hiện ra sau nhiều ngày.
Các nguyên nhân âm thầm làm hỏng việc tự khởi động
Các container được tạo bằng docker compose run không bao giờ nhận restart policy từ file. Compose xem chúng là các container chạy một lần. Nếu một service có vẻ bỏ qua policy của nó, hãy kiểm tra xem nó có được khởi động bằng run thay vì up hay không.
Đường dẫn tương đối trong volume hoặc trong mục env_file được phân giải dựa trên thư mục chứa compose file. Cách này hoạt động từ shell của bạn và cũng hoạt động từ một unit đặt WorkingDirectory. Nó sẽ lỗi khi chạy từ một unit không đặt biến này, vì khi đó working directory là /.
Rootless Docker là một trường hợp riêng. Daemon chạy dưới dạng user service, và user service sẽ dừng khi session cuối cùng của user đó kết thúc. Hãy enable service cho user đó và cho phép nó tiếp tục chạy khi không có user nào đăng nhập:
systemctl --user enable docker
sudo loginctl enable-linger $USERNếu không có enable-linger, rootless daemon sẽ tắt khi bạn logout và các container cũng dừng theo. Hiện tượng này trông giống hệt một restart policy bị hỏng.
Còn một điểm cuối cùng. Automatic security updates có thể reboot server vào một giờ cố định. Điều này chỉ có lợi nếu stack của bạn tự khởi động lại được. Việc cấu hình trên máy mới nên thực hiện cùng các công việc trong giờ đầu tiên, được nêu trong 10 phút đầu tiên trên một VPS mới.
FAQ
Sự khác biệt giữa restart: always và restart: unless-stopped là gì?
Cả hai đều khởi động lại container khi nó tự dừng. Chúng khác nhau sau khi bạn dừng container bằng tay. Với always, container sẽ khởi động lại vào lần tiếp theo Docker daemon khởi động, vì vậy việc reboot sẽ vô hiệu hóa thao tác dừng thủ công của bạn. Với unless-stopped, daemon ghi nhớ rằng container đã được dừng một cách chủ đích và sẽ không can thiệp. Hãy dùng unless-stopped trừ khi bạn thực sự muốn một container không bao giờ ở trạng thái dừng.
Tôi đã thêm restart: unless-stopped nhưng container vẫn không khởi động sau khi reboot. Tại sao?
Chính sách nằm trên container chứ không nằm trong file, và một container đã tồn tại sẽ không được cập nhật khi bạn sửa file YAML. Hãy chạy docker compose up -d để Compose tạo lại nó, sau đó xác nhận bằng docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app). Nếu lệnh đó in ra no, nghĩa là container đã được tạo trước khi bạn sửa file. Một nguyên nhân phổ biến khác là docker.service chưa được enable, bạn có thể kiểm tra bằng systemctl is-enabled docker.
Tôi có cần systemd unit nếu đã dùng restart policies không?
Thường là không. Restart policy là đủ cho một stack chỉ cần mạng, vốn là đa số các stack. Hãy thêm unit khi các container phụ thuộc vào thứ gì đó chưa sẵn sàng khi Docker daemon khởi động, ví dụ như ổ đĩa ngoài, volume đã mã hóa, NFS share, hoặc giao diện VPN. Unit cho phép bạn thiết lập thứ tự thông qua After= và RequiresMountsFor=, điều mà restart policy không thể thực hiện.
Làm thế nào để dừng một stack vĩnh viễn mà không để nó tự chạy lại sau khi reboot?
Với unless-stopped, docker compose stop là đủ, vì container bị dừng bằng tay sẽ không được khôi phục khi daemon khởi động lại. Với always, lệnh stop là chưa đủ và container sẽ quay lại sau khi reboot. Bạn cần chạy docker compose down để xóa các container, hoặc thay đổi chính sách trước bằng docker update --restart no my-container. Nếu một systemd unit quản lý stack đó, hãy chạy thêm sudo systemctl disable myapp.service, nếu không unit sẽ khởi động lại nó.