Cách thiết lập Docker Compose tự khởi động cùng hệ thống
Hướng dẫn cấu hình restart policy để Docker Compose tự chạy sau khi reboot. Giải thích tại sao on-failure không hiệu quả và cách dùng systemd cho các dịch vụ phụ thuộc.
Câu trả lời ngắn gọn
Các dịch vụ Docker Compose tự khởi động cùng hệ thống khi thỏa mãn đồng thời hai điều kiện. Docker daemon phải được kích hoạt như một systemd service, và mỗi dịch vụ trong file cấu hình phải có chính sách restart là unless-stopped hoặc always. Thêm restart: unless-stopped vào mỗi dịch vụ, chạy docker compose up -d một lần, các container sẽ tự khởi động lại sau khi reboot. Không cần thực hiện thêm thao tác nào khác cho các trường hợp thông thường.
Bạn chỉ cần dùng systemd unit khi thứ tự khởi động là quan trọng: một stack phụ thuộc vào ổ đĩa được mount, giao diện VPN, hoặc một network share chưa sẵn sàng tại thời điểm Docker daemon khởi động. Trường hợp đó là có thật, và nửa sau của hướng dẫn này sẽ đề cập đến nó. Nếu bạn vẫn đang tìm hiểu về định nghĩa dịch vụ và volume, hãy bắt đầu với các kiến thức cơ bản về Docker Compose trên VPS rồi quay lại sau.
Thiết lập chính sách khởi động lại trong compose.yaml
Chính sách này được cấu hình trên mỗi service. Không có tùy chọn toàn cục, vì vậy nếu bạn quên cấu hình cho một service, nó sẽ không tự khởi động sau khi reboot trong khi các service khác vẫn chạy 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 cấu hình và kiểm tra lại chính sách từ container đang chạy:
docker compose up -d
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app)Lệnh này sẽ in ra unless-stopped. Nếu nó in ra no, nghĩa là file đã được chỉnh sửa nhưng container chưa được tạo lại.
Đây là lỗi phổ biến nhất. Chính sách khởi động lại được lưu trên container, không phải trong file YAML. Việc chỉnh sửa compose.yaml không làm thay đổi gì đối với container đã tồn tại. Lệnh docker compose restart cũng không có tác dụng, vì nó chỉ dừng và khởi động lại chính đối tượng container đó mà không thay đổi cấu hình. Chỉ có docker compose up -d mới so sánh file với các container đang chạy, phát hiện chính sách đã thay đổi và tạo lại chúng.
Đối với container mà bạn không muốn tạo lại ngay lúc này, hãy thay đổi chính sách trực tiếp:
docker update --restart unless-stopped my-containerBạn vẫn cần chỉnh sửa file YAML. Lệnh docker update thay đổi container đang chạy, và lần chạy docker compose up -d tiếp theo sẽ đọc file và ghi đè giá trị cũ trở lại.
Ý nghĩa thực tế của từng giá trị restart
Docker định nghĩa bốn giá trị, và sự khác biệt giữa chúng chỉ xuất hiện khi máy chủ khởi động lại hoặc daemon được khởi động lại.
nolà giá trị mặc định. Container không bao giờ tự động khởi động lại trong bất kỳ trường hợp nào.alwayskhởi động lại container bất cứ khi nào nó dừng. Nếu bạn dừng nó thủ công, nó vẫn sẽ chạy lại vào lần tiếp theo Docker daemon khởi động. Điều này thường gây bất ngờ: một container bạn đã chủ động dừng từ tuần trước lại chạy lại sau khi reboot.unless-stoppedhoạt động giống nhưalways, ngoại trừ việc container bị dừng thủ công sẽ vẫn ở trạng thái dừng sau khi daemon khởi động lại. Đây là giá trị bạn cần cho một service mà bạn thỉnh thoảng dừng để bảo trì.on-failurechỉ khởi động lại container khi nó thoát với mã lỗi khác 0. Bạn có thể giới hạn số lần thử, như trongrestart: on-failure:3.
Đối với một stack cần luôn chạy mỗi khi server hoạt động, unless-stopped là giá trị mặc định phù hợp. Chỉ chọn always khi bạn muốn một container không bao giờ bị để ở trạng thái dừng.
Tại sao restart: on-failure không hoạt động sau khi reboot
Nhiều người chọn on-failure vì nghe có vẻ an toàn, sau đó nhận ra mọi container đều dừng lại sau lần reboot đầu tiên. Lý do nằm ở định nghĩa của nó. on-failure chỉ phản ứng với một thứ duy nhất: tiến trình container thoát với mã lỗi.
Reboot không phải là một lỗi. Khi host tắt máy, systemd dừng docker.service, và daemon dừng từng container một cách chủ động. Container không hề bị lỗi, nên chính sách này không có gì để phản ứng. Khi khởi động lại, daemon kiểm tra các container cần khôi phục, và một container on-failure đã được dừng sạch sẽ không nằm trong danh sách đó. Nó vẫn ở trạng thái exited.
Bạn có thể kiểm tra trực tiếp. Thiết lập restart: on-failure cho một service, chạy docker compose up -d, reboot, sau đó chạy:
docker compose ps -aService được liệt kê với trạng thái Exited và status kiểu như Exited (0) 2 minutes ago. Không có gì bị hỏng và không có lỗi nào được ghi lại, đó là lý do tại sao vấn đề này khó chẩn đoán. Chính sách đã thực hiện đúng những gì nó được thiết lập.
on-failure vẫn hữu ích. Nó phù hợp với một container chạy một tác vụ và có thể bị crash, nơi bạn muốn giới hạn số lần thử lại và không muốn bị lặp lại việc restart. Đây là công cụ sai mục đích để duy trì một service chạy dài hạn sau khi reboot.
Các chính sách khởi động lại chỉ hoạt động nếu dịch vụ Docker khởi động cùng hệ thống
Các chính sách khởi động lại được thực thi bởi Docker daemon. Nếu daemon không khởi động, sẽ 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 trên đều phải trả về enabled. Các gói từ kho lưu trữ chính thức của Docker sẽ tự động kích hoạt chúng khi cài đặt, vì vậy trên một server mới, điều này thường đã được thiết lập sẵn. Nếu một trong hai lệnh trả về disabled, hãy sửa lại:
sudo systemctl enable --now docker containerdCó một cái bẫy ở đây cần lưu ý. Ubuntu cũng cung cấp docker.socket, dịch vụ này sẽ khởi động daemon theo yêu cầu ngay khi có tiến trình kết nối tới Docker API. Nhiều người thấy docker.socket đã được kích hoạt, họ cho rằng daemon đã được quản lý và vô hiệu hóa docker.service để tiết kiệm bộ nhớ. Khi khởi động hệ thống, không có tiến trình nào gọi API, socket không được kích hoạt, 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 phải là sự thay thế cho việc kích hoạt docker.service.
Khi nào unit systemd là giải pháp tốt hơn
Các chính sách restart không có khái niệm về thứ tự so với phần còn lại của hệ thống. Daemon khởi động và nó sẽ chạy các container của bạn ngay khi có thể. Nếu stack của bạn bind-mount một thư mục từ một volume riêng biệt, một share NFS (network file system), hoặc một ổ đĩ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ạo một thư mục trống tại điểm mount và khởi động container với nó, kết quả là database của bạn chạy mà không có dữ liệu.
Hãy viết một unit systemd khi có bất kỳ trường hợp nào sau đây xảy ra. Stack cần một mount, một interface VPN, hoặc một unit khác phải sẵn sàng trước. Bạn muốn systemctl stop myapp và systemctl start myapp hoạt động giống như mọi service khác trên máy chủ. Hoặc bạn muốn stack được tắt một cách sạch sẽ trong quá trình shutdown thay vì bị kill cùng với daemon. Nếu bạn chưa quen với các unit systemd, viết một service và timer systemd sẽ đề cập chi tiết hơn về định dạng file này.
Viết unit cho systemd
Đặt stack vào một đường dẫn cố định bên ngoài thư mục home. /srv/myapp là một lựa chọn tốt, vì một unit chạy trước khi bất kỳ ai đăng nhập không có lý do gì để đọ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.targetKích hoạt và khởi chạy nó:
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). Điều này trông có vẻ sai trong lần đầu bạn thấy. Nó hoàn toàn đúng: Type=oneshot với RemainAfterExit=yes nghĩa là unit đã chạy xong lệnh của nó, lệnh đã kết thúc, và systemd giữ unit ở trạng thái active để ExecStop chạy khi tắt máy.
Mỗi dòng đều có mục đích riêng. Requires=docker.service nghĩa là unit sẽ thất bại ngay lập tức thay vì chạy docker compose trên một socket đã chết. After= thiết lập thứ tự, vì Requires= khi đứng một mình sẽ không làm việc đó. RequiresMountsFor= khiến systemd kéo unit mount cho đường dẫn đó vào và chờ đợi nó, đây chính là lý do chính để sử dụng unit thay vì chính sách restart. TimeoutStartSec=0 ngăn systemd hủy công việc khởi động trong khi một image lớn vẫn đang được tải về.
Một lưu ý về việc kết hợp hai cơ chế. Tài liệu của Docker khuyên không nên trộn lẫn các chính sách restart với một trình quản lý tiến trình trên host. Cảnh báo đó dành cho trình quản lý tiến trình giám sát chính tiến trình container và khởi động lại nó trong khi daemon cũng đang cố gắng làm điều tương tự. Một unit Type=oneshot không giám sát gì cả, vì vậy việc giữ restart: unless-stopped trong file compose cùng với unit này là hoàn toàn ổn, và đó là điều bạn muốn. systemd xử lý thứ tự khi khởi động, và daemon xử lý container bị crash vào lúc ba giờ sáng.
Kiểm tra bằng cách khởi động lại thực tế
Không có gì thay thế được bài kiểm tra thực tế. systemctl restart docker không kiểm tra thứ tự mount, và docker compose down theo sau bởi docker compose up -d hoàn toàn không kiểm tra bất kỳ điều gì về quá trình khởi động.
sudo rebootĐợi, kết nối lại và kiểm tra theo thứ tự sau:
uptime
systemctl is-active docker
docker compose psuptime xác nhận bạn đang truy cập vào một máy đã thực sự khởi động lại. docker compose ps, chạy từ thư mục stack, sẽ liệt kê mọi service là running với thời gian uptime gần bằng với uptime của máy. Một service hiển thị Exited là service cần phải xem xét.
Nếu có thành phần nào không khởi chạy, log của daemon sẽ bao gồm khoảng thời gian khởi động:
journalctl -u docker.service -b --no-pager | tail -50Đối với một stack được quản lý bởi một unit, journalctl -u myapp.service -b --no-pager hiển thị chính xác đầu ra docker compose từ lúc khởi động, bao gồm cả lỗi pull image hoặc thiếu file .env.
Những nguyên nhân khiến tính năng tự động khởi động bị lỗi ngầm
Các container được tạo bằng docker compose run sẽ không bao giờ nhận được chính sách khởi động lại từ tệp cấu hình. Compose coi chúng là các container chạy một lần. Nếu một service có vẻ bỏ qua chính sách của nó, hãy kiểm tra xem nó đã được khởi động bằng run thay vì up hay chưa.
Đường dẫn tương đối trong một volume hoặc trong một mục env_file được phân giải dựa trên thư mục chứa tệp compose. Điều này hoạt động bình thường từ shell của bạn và từ một unit có thiết lập WorkingDirectory. Nó sẽ thất bại nếu unit không có thiết lập này, vì thư mục làm việc lúc đó là /.
Rootless Docker là một trường hợp riêng biệt. Daemon chạy dưới dạng một user service, và user service sẽ dừng lại khi phiên làm việc cuối cùng của người dùng đó kết thúc. Hãy kích hoạt nó cho người dùng và cho phép nó tiếp tục chạy ngay cả khi không có ai đă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 đăng xuất và các container cũng sẽ dừng theo, điều này trông giống hệt như một chính sách khởi động lại bị lỗi.
Một điều cuối cùng. Các bản cập nhật bảo mật tự động có thể khởi động lại máy chủ vào một giờ cố định, điều này chỉ tốt nếu stack của bạn tự động khởi động lại. Việc thiết lập điều đó trên một máy mới nên được thực hiện cùng với các công việc đầu giờ khác trong mười 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 sử 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ày nằm trên container, không phải trong file, và một container đã tồn tại sẽ không được cập nhật khi chỉnh sửa 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 đã tồn tại trước khi bạn chỉnh sửa. Một nguyên nhân phổ biến khác là docker.service chưa được kích hoạt, bạn có thể kiểm tra bằng systemctl is-enabled docker.
Tôi có cần systemd unit nếu đã sử dụng các chính sách restart không?
Thông thường là không. Chính sách restart là đủ cho một stack chỉ cần mạng, vốn là hầu hết các stack. Hãy thêm một unit khi các container phụ thuộc vào thứ gì đó chưa sẵn sàng khi Docker daemon khởi động, chẳng hạn như ổ đĩa ngoài, volume được mã hóa, share NFS hoặc giao diện VPN. Unit cung cấp cho bạn khả năng thiết lập thứ tự thông qua After= và RequiresMountsFor=, điều mà chính sách restart không thể thực hiện được.
Làm thế nào để dừng vĩnh viễn một stack mà không để nó tự khởi động 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 dừng là chưa đủ và container sẽ quay lại sau khi reboot. Hãy 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 stack.