SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-25

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-container

Bạ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.

  • no là giá trị mặc định. Container không bao giờ được tự động restart trong bất kỳ trường hợp nào.
  • always restart 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-stopped hoạt động giống always, 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-failure chỉ 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ư trong restart: 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 chính sách restart: on-failure không tồn tại sau khi reboot

Nhiều người chọn on-failure vì nghe 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 chính sách này. on-failure chỉ phản ứng với một sự kiện: 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 các container cần được chạy lại. Một container on-failure đã được dừng bình thường không thuộc nhóm đó. Container 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 -a

Service sẽ được liệt kê với trạng thái Exited và status tương tự 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 vấn đề này khó chẩn đoán. Policy đã thực hiện đúng như định nghĩa.

on-failure vẫn hữu ích. Policy này phù hợp với container chạy một job và có thể bị crash, khi bạn muốn giới hạn số lần thử lại và không muốn xảy ra restart loop. Đây không phải công cụ phù hợp để giữ một service chạy liên tục qua các lần reboot.

Chính sách restart chỉ hoạt động nếu Docker service khởi động khi boot

Docker daemon thực thi các chính sách restart. Nếu daemon không khởi động thì không có thành phần nào thực thi các chính sách đó. Kiểm tra như sau:

systemctl is-enabled docker
systemctl is-enabled containerd

Cả hai lệnh đều phải in ra enabled. Các package từ repository chính thức của Docker enable các service này khi cài đặt, nên trên server mới cài đặt kết quả này thường đạt yêu cầu. Nếu một trong hai lệnh in ra disabled, hãy sửa:

sudo systemctl enable --now docker containerd

Có một điểm dễ nhầm cần hiểu rõ. Ubuntu cũng cung cấp docker.socket, service này khởi động daemon theo yêu cầu vào lần đầu có tiến trình gọi Docker API. Người dùng thấy docker.socket đang được enable, cho rằng daemon đã được quản lý, rồi disable docker.service để tiết kiệm memory. Khi boot, không có tiến trình nào gọi API, nên socket không được truy cập, daemon không khởi động và không có container nào chạy cho đến khi bạn nhập lệnh docker đầu tiên. Socket activation không thay thế cho việc enable docker.service.

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 myappsystemctl 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 đường dẫn cố định bên ngoài thư mục home. /srv/myapp là lựa chọn phù hợp, vì một unit chạy trước khi bất kỳ ai đăng nhập thì không có lý do gì phải đọ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.target

Enable và start unit:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.service

Một unit hoạt động bình thường 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 đúng: Type=oneshot với RemainAfterExit=yes nghĩa là unit đã chạy command, command đã kết thúc và systemd vẫn giữ unit ở trạng thái active để ExecStop chạy khi shutdown.

Mỗi dòng đều có lý do. Requires=docker.service khiến unit fail fast thay vì chạy docker compose trên 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 đường dẫn đó 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 hủy start job trong khi image lớn vẫn đang được pull.

Lưu ý về việc kết hợp hai cơ chế này. Tài liệu Docker khuyến cáo không trộn restart policy với host process manager. Cảnh báo đó nói về process manager trực tiếp giám sát container process và restart nó trong khi daemon cũng đang làm việc tương tự. Một unit Type=oneshot không giám sát gì cả, nên giữ restart: unless-stopped trong compose file cùng với unit này là an toàn và đúng với mục đích sử dụng. 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ứ cần duy trì là một process chạy lâu dài thông thường thay vì một stack, vì khi đó không có daemon bên dưới và Restart= của systemd phải thực hiện việc giám sát; chạy dsh không cần terminal 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 reboot

Chờ, kết nối lại rồi kiểm tra theo thứ tự này:

uptime
systemctl is-active docker
docker compose ps

uptime 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 $USER

Nế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 nhau giữa restart: always và restart: unless-stopped là gì?

Cả hai đều khởi động lại container khi container tự dừng. Chúng khác nhau khi bạn dừng container thủ công. Với always, container khởi động lại vào lần Docker daemon khởi động tiếp theo, nên reboot sẽ hủy 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 có chủ đích và để nguyên trạng thái đó. Dùng unless-stopped trừ khi bạn thực sự muốn container không tự khởi động lại.

Tôi đã thêm restart: unless-stopped nhưng container vẫn không khởi động sau reboot. Vì sao?

Policy được lưu trên container, không nằm trong file; chỉnh sửa YAML không cập nhật container đã tồn tại. Chạy docker compose up -d để Compose tạo lại container, rồi xác nhận bằng docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app). Nếu lệnh in ra no, container đã được tạo trước khi bạn chỉnh sửa. Nguyên nhân phổ biến khác là docker.service chưa được bật. Bạn có thể kiểm tra bằng systemctl is-enabled docker.

Nếu đã dùng restart policy thì tôi có cần systemd unit không?

Thông thường là không. Restart policy đủ cho stack chỉ cần network, mà đây là trường hợp của hầu hết stack. Hãy thêm unit khi container phụ thuộc vào thành phần chưa sẵn sàng lúc Docker daemon khởi động, chẳng hạn như disk bên ngoài, volume được mã hóa, NFS share hoặc VPN interface. Unit cung cấp thứ tự khởi động thông qua After=RequiresMountsFor=, điều mà restart policy không thể mô tả.

Làm cách nào để dừng vĩnh viễn một stack mà không để stack khởi động lại ở lần reboot tiếp theo?

Với unless-stopped, chỉ cần docker compose stop vì container được dừng thủ công sẽ không được khởi động lại khi daemon khởi động lại. Với always, chỉ dừng container là chưa đủ và container sẽ khởi động lại sau reboot. Bạn có thể chạy docker compose down để xóa container, hoặc trước tiên đổi policy bằng docker update --restart no my-container. Nếu 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 stack lại.