SSD Nodes Learn 🎉 VPS từ $5.50/tháng
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-21

systemd: Requires không có nghĩa là After

Phân biệt Requires, Wants, After, Before, ExecStartPre và Condition. Xem vì sao unit chạy song song, lỗi khi boot và cách debug unit không bao giờ chạy.

Requires không có nghĩa là After

Dependency và condition của systemd là 4 cơ chế riêng biệt nhưng nhiều unit file thường sử dụng như thể chúng là một. Requires=Wants= quyết định unit nào khác được kéo vào. After=Before= quyết định thứ tự khởi động của các unit. ExecStartPre= chạy một kiểm tra và kiểm tra này có thể làm unit fail. Các nhóm ConditionAssert quyết định unit có được chạy hay không. Mỗi cơ chế hoạt động độc lập với các cơ chế còn lại. Vì vậy, một unit có thể yêu cầu unit khác nhưng vẫn khởi động cùng thời điểm với unit đó.

Câu cuối chính là nguyên nhân của gần như mọi báo cáo kiểu “chạy thủ công thì được, nhưng boot lại fail”.

[Unit]
Description=Inventory API
Requires=postgresql.service

[Service]
ExecStartPre=/usr/bin/pg_isready -h 127.0.0.1 -t 5
ExecStart=/usr/local/bin/inventory-api

Requires=postgresql.service kéo PostgreSQL vào cùng start transaction. Nó không chờ PostgreSQL. systemd khởi động cả 2 job song song, nên pg_isready chạy trong khi PostgreSQL vẫn đang mở data directory. Lệnh này thoát với mã 2 vì chưa có tiến trình nào listening, và unit fail trước khi systemd đi đến ExecStart. Chạy sudo systemctl start inventory-api sau đó 1 giờ thì thành công vì lúc này PostgreSQL đã chạy. Không có gì trong unit file thay đổi. Vì vậy file này trông có vẻ không có vấn đề.

Cách sửa chỉ cần thêm 1 dòng.

[Unit]
Requires=postgresql.service
After=postgresql.service

Có một chi tiết quan trọng hơn ở cùng vị trí. Một dependency Requires= bị fail chỉ ngăn unit của bạn khởi động nếu bạn đồng thời đặt After= cho dependency đó. Nếu không có ordering, systemd đã khởi động unit của bạn trước khi unit kia fail, nên không còn gì để hủy. Chỉ dùng Requires= không mang lại cơ chế bảo vệ như nhiều người nghĩ. Hãy viết After= ngay cạnh mọi Requires= và mọi Wants=, trừ khi bạn có lý do cụ thể để không làm vậy.

What Requires, Wants, Requisite and BindsTo hứa hẹn

Tất cả đều là các thiết lập dependency. Không thiết lập thứ tự khởi động nào.

  • Wants=: kéo unit còn lại vào. Nếu unit đó fail hoặc không tồn tại, unit này vẫn khởi động. Đây là thứ systemctl enable tạo ra dưới dạng symlink bên trong thư mục .wants/.
  • Requires=: kéo unit còn lại vào. Nếu unit đó fail và bạn cũng đặt thứ tự After= cho nó, unit này sẽ không được khởi động. Nếu unit còn lại sau đó bị stop một cách rõ ràng, unit này cũng bị stop theo.
  • Requisite=: không kéo unit còn lại vào. Nếu unit đó chưa active, unit này fail ngay lập tức.
  • BindsTo=: giống Requires=, đồng thời unit này cũng stop bất cứ khi nào unit còn lại stop vì bất kỳ lý do nào, kể cả khi hardware biến mất.
  • PartOf=: thao tác stop và restart được truyền từ unit còn lại xuống unit này. Thao tác start không được truyền.
  • Conflicts=: khi khởi động unit này thì stop unit còn lại.

Với một daemon giao tiếp với daemon khác, Wants=After= thường là cặp phù hợp. Requires= ràng buộc vòng đời của hai unit: nếu stop database để bảo trì thì application cũng dừng theo, và không tự khởi động lại khi database hoạt động trở lại. Wants=After= cung cấp thứ tự khởi động khi boot mà không tạo ràng buộc này; policy restart sẽ xử lý trường hợp dependency biến mất về sau.

Bạn cũng kế thừa những dependency mà mình chưa từng khai báo. Với DefaultDependencies=yes, là giá trị mặc định, một service thông thường tự động nhận Requires=sysinit.target, After=sysinit.target basic.targetConflicts=shutdown.target. Vì vậy, một service có phần [Unit] gần như trống vẫn khởi động muộn trong quá trình boot và vẫn được stop đúng cách khi shutdown.

After và Before chỉ sắp xếp thứ tự transaction, không làm gì khác

After=Before= chỉ dùng để sắp xếp thứ tự. Chúng hoàn toàn không tạo ra requirement nào. After=redis.service trong một unit mà không có thành phần nào khác kéo Redis vào là no-op: nếu redis.service không nằm trong transaction, thì không có gì để chờ, nên unit của bạn khởi động ngay lập tức.

Điểm này cần được nhắc lại, vì đây chính là dạng của lỗi network-online.target ở phần sau. Ordering chỉ chờ các unit vốn đã được khởi động trong cùng transaction.

Hai directive này có tính đối xứng. Viết After=b.service trong a.service có ý nghĩa giống viết Before=a.service trong b.service, vì vậy chỉ cần dùng một directive và đặt nó trong unit do bạn quản lý. Khi shutdown, thứ tự ordering tự động đảo ngược, nên After=b.service cũng có nghĩa là unit của bạn được dừng trước b.service.

After= chờ “started”, còn Type= định nghĩa ý nghĩa của trạng thái đó

After= chờ đến khi unit còn lại khởi động xong. “Khởi động xong” có nghĩa là gì hoàn toàn do Type= của unit đó quyết định.

  • Type=simple: ngay khi systemd fork process. Chương trình có thể còn chưa parse config, chứ chưa nói đến việc mở socket.
  • Type=exec: ngay khi execve() thành công. Mạnh hơn một chút. Tuy vậy, tùy chọn này vẫn không cho biết service đã sẵn sàng hay chưa.
  • Type=forking: khi process cha ban đầu thoát.
  • Type=oneshot: khi process thoát. Ở đây, “started” thực sự có nghĩa là công việc đã hoàn tất.
  • Type=notify: khi service gửi READY=1 qua notification socket của nó. Đây là type duy nhất báo trạng thái sẵn sàng thực sự.

Vì vậy, After= trên một daemon Type=simple chỉ là một cam kết yếu. Đây là nửa còn lại của race trong ví dụ đầu tiên. Nếu unit mà bạn phụ thuộc được phát hành với Type=simple, việc order sau unit đó không có nghĩa là unit đã chấp nhận connection. Có 2 cách xử lý hợp lý. Hãy order sau socket unit của nó để kernel queue các connection đến trong khi daemon vẫn đang khởi động. Hoặc cho service của bạn tự retry và để restart policy xử lý việc đó. Type mà một unit sử dụng có thể xem trong systemctl cat. Bạn nên đọc thiết lập Type= và ý nghĩa của từng giá trị đối với systemd trước khi dựa vào việc ordering.

ExecStartPre là bước kiểm tra có thể làm unit thất bại

ExecStartPre= chạy trước ExecStart=. Nếu lệnh này thoát với mã khác 0, quá trình activation bị hủy và unit chuyển sang failed. ExecStart= không bao giờ chạy. Đây là cơ chế khiến nhiều unit thất bại mà không có thông báo từ chương trình thực tế, vì chương trình chưa từng được khởi động.

Các điểm thường gây nhầm lẫn:

  • Đây không phải shell. Không có pipe, redirect, glob hoặc &&. Token đầu tiên phải là đường dẫn tuyệt đối. Bọc dòng lệnh trong /bin/sh -c '...' khi cần cú pháp shell.
  • Prefix - khiến mã thoát khác 0 không làm quá trình khởi động thất bại: ExecStartPre=-/usr/bin/optional-check.
  • Mọi ExecStartPre= phải thoát trước khi lệnh tiếp theo chạy. Không thể dùng nó để khởi động một process chạy lâu dài.
  • Tất cả các dòng ExecStartPre= dùng chung TimeoutStartSec= với ExecStart=. Một bước kiểm tra trước cứ lặp để chờ database sẽ chiếm hết thời gian timeout khởi động, sau đó unit thất bại với Result: timeout khi start operation timed out. Terminating. xuất hiện trong journal.

Dòng báo lỗi ghi tên control process, không phải main process:

inventory-api.service: Control process exited, code=exited, status=2/INVALIDARGUMENT
inventory-api.service: Failed with result 'exit-code'.

Đọc kỹ tên symbolic đó. systemd ánh xạ các mã thoát nhỏ qua một bảng cố định, nên 2 luôn in ra INVALIDARGUMENT, bất kể chương trình dùng mã đó với ý nghĩa gì. status=203/EXEC mới là thông tin có giá trị: systemd hoàn toàn không thể thực thi binary, vì path không đúng hoặc file không có quyền execute.

Không dùng ExecStartPre= để tạo directory. RuntimeDirectory=, StateDirectory=, LogsDirectory=CacheDirectory= tạo directory với owner và mode phù hợp, còn RuntimeDirectory= sẽ được dọn khi service dừng. Chúng cũng hoạt động đúng dưới DynamicUser=, điều mà mkdir tự viết không làm được.

Condition chỉ bỏ qua một cách im lặng. Assert báo lỗi rõ ràng.

Các nhóm ConditionAssert thực hiện cùng một phép kiểm tra. Chúng chỉ khác nhau ở cách xử lý khi phép kiểm tra thất bại.

Condition...= thất bại sẽ bỏ qua unit. Start job vẫn được báo là thành công. Unit vẫn ở trạng thái inactive (dead), không có gì được đánh dấu là failed, không có alert nào được kích hoạt, và journal ghi một dòng:

Condition check resulted in Inventory API being skipped.

Trên systemd 250 trở lên, systemctl status in lý do trực tiếp:

     Active: inactive (dead)
  Condition: start condition unmet at Thu 2026-08-20 09:14:02 UTC; 2min ago

Dòng thụt vào bên dưới nêu chính xác directive đã thất bại, ví dụ ConditionPathExists=/etc/inventory/api.conf was not met.

Assert...= thất bại sẽ làm unit failed. Journal ghi Assertion failed for Inventory API. và unit kết thúc ở trạng thái failed (Result: assert), đủ rõ để hệ thống monitoring phát hiện.

Hãy chọn giữa chúng bằng cách xác định ý nghĩa của phép kiểm tra thất bại. Condition có nghĩa là “unit này không áp dụng trên máy này”. Assert có nghĩa là “điều này phải đúng; nếu không, hãy báo cho ai đó”. Hầu hết unit cần Condition. Chỉ dùng Assert khi việc âm thầm không làm gì còn tệ hơn một unit failed.

Nhóm Condition có 2 bẫy.

Thứ nhất, condition thất bại không làm các unit phụ thuộc vào nó bị failed. Nếu a.serviceRequires=b.serviceb.service bị bỏ qua do condition, start job của b.service vẫn được tính là đã hoàn tất, nên a.service vẫn khởi động bình thường trong khi b không chạy. Condition chỉ bảo vệ unit chứa nó.

Thứ hai, condition được đánh giá mỗi lần unit khởi động, tại thời điểm job chạy. Một unit được kích hoạt bởi systemd timer trên VPS có thể bị bỏ qua 100 lần liên tiếp mà chưa một lần nào có trạng thái failed. Đây cũng là dạng silent no-op giống cron job chạy nhưng không làm gì, và cách tìm cũng giống nhau: đọc journal của unit thay vì tin vào exit state.

Các condition cần biết trên server:

  • ConditionPathExists=/etc/inventory/api.conf và dạng phủ định của nó là ConditionPathExists=!/etc/inventory/api.conf.
  • ConditionFileNotEmpty=ConditionDirectoryNotEmpty=, dùng cho config file hoặc data directory mà package đã tạo nhưng để trống.
  • ConditionVirtualization=, để unit cần một kernel interface thực tế có thể khai báo ConditionVirtualization=!container. Kiểm tra thông tin máy của bạn bằng systemd-detect-virt.
  • ConditionHost= khớp với hostname hoặc machine ID. Đây là cách một unit file dùng chung có thể hoạt động khác nhau trên 2 server.
  • ConditionKernelCommandLine=ConditionKernelVersion=, dùng cho unit phụ thuộc vào boot parameter hoặc yêu cầu kernel tối thiểu.

Một assignment rỗng sẽ xóa danh sách. Đây là cách drop-in xóa condition mà package đã cung cấp:

[Unit]
ConditionPathExists=
ConditionPathExists=/srv/inventory/api.conf

Vì sao network.target không có nghĩa là mạng đã sẵn sàng

network.target là một điểm đồng bộ, không phải một trạng thái. Khi boot, sắp xếp một unit chạy sau target này chỉ có nghĩa là phần mềm quản lý mạng đã được khởi động. Điều đó không có nghĩa là interface đã có địa chỉ hoặc đã có route ra Internet. Target này chủ yếu tồn tại cho chiều ngược lại: unit được sắp xếp After=network.target sẽ dừng trước khi mạng bị tháo xuống trong lúc shutdown.

network-online.target mới là target chờ mạng sẵn sàng. Nó dựa trên một wait-online service thuộc network manager bạn đang dùng:

  • systemd-networkd-wait-online.service khi systemd-networkd quản lý các link. Đây là trường hợp thông thường trên Ubuntu server được cấu hình bằng netplan.
  • NetworkManager-wait-online.service khi dùng NetworkManager.

Các hệ thống ifupdown cũ cũng có hiệu ứng tương tự với networking.service. Dù bạn dùng target nào, cấu hình đúng cần 2 dòng, không phải 1 dòng.

[Unit]
Wants=network-online.target
After=network-online.target

network-online.target không nằm trong boot transaction mặc định và không có unit nào tự kéo nó vào. Nếu chỉ viết After=, bạn đang sắp xếp unit này sau một unit chưa từng được đưa vào hàng đợi, nên việc sắp xếp hoàn toàn không có tác dụng. Đây là no-op đã nói ở trên, ở dạng gây tốn thời gian nhất. Dòng Wants= mới là dòng đưa target vào transaction, để dòng After= có target mà chờ.

Điều thứ hai cần biết là “online” được định nghĩa bởi implementation của wait-online, không phải bởi systemd. systemd-networkd-wait-online trả về khi các link do nó quản lý đạt đến trạng thái đã cấu hình. Nó không kiểm tra việc phân giải DNS và cũng không kiểm tra khả năng kết nối đến bất kỳ host từ xa nào.

Định nghĩa này gây ra một lỗi phổ biến trên VPS. Một máy có interface thứ hai dành cho private network, được khai báo trong netplan nhưng chưa được gán địa chỉ, sẽ khiến wait service chờ cho đến khi timeout:

systemd-networkd-wait-online[612]: Timeout occurred while waiting for network connectivity.
systemd-networkd-wait-online.service: Failed with result 'exit-code'.

Boot mất thêm 2 phút vì timeout mặc định là 120 giây. Có 2 cách khắc phục. Đánh dấu interface không dùng là optional: true trong file netplan để networkd không chờ interface đó nữa. Hoặc thêm drop-in cho wait service, chỉ định link cần chờ bằng --interface=, hoặc truyền --any để service trả về ngay khi một link bất kỳ hoạt động.

Tốt hơn nữa là tránh phụ thuộc vào target này. Nhiều service được sắp xếp chạy sau network-online.target chỉ vì chúng bind vào một địa chỉ cụ thể và fail khi boot với dòng như sau:

nginx: [emerg] bind() to 203.0.113.10:443 failed (99: Cannot assign requested address)

Kernel từ chối bind vì địa chỉ đó chưa hoạt động. Thiết lập net.ipv4.ip_nonlocal_bind=1 cho phép process bind vào một địa chỉ mà máy chưa sở hữu, còn restart policy sẽ xử lý phần còn lại. Trì hoãn toàn bộ boot để chờ mạng sẵn sàng là cách xử lý quá nặng cho một vấn đề thường chỉ nằm ở một socket.

Cách đọc dependency thực tế của systemd trên máy đang chạy

Không nên chỉ dựa vào unit file. Drop-in, symlink .wants/ và các dependency mặc định ngầm định đều có thể thêm edge mà unit file không hiển thị.

systemctl cat inventory-api.service

Lệnh này in unit file và mọi drop-in theo đúng thứ tự áp dụng, kèm source path ở trên mỗi block. Hãy chạy lệnh này trước. Một override 5 dòng trong /etc/systemd/system/inventory-api.service.d/ sẽ ghi đè file do package cung cấp và nếu không kiểm tra thì bạn sẽ không thấy.

systemctl show inventory-api.service -p Requires -p Wants -p After -p Before -p ConditionResult -p AssertResult

Lệnh này in các giá trị đã được resolve, sau khi áp dụng drop-in và sau khi systemd thêm các dependency ngầm định. ConditionResult=no trả lời trực tiếp câu hỏi “unit báo thành công nhưng không làm gì cả”.

systemctl list-dependencies inventory-api.service
systemctl list-dependencies --reverse inventory-api.service
systemctl list-dependencies --after inventory-api.service
systemctl list-dependencies --before inventory-api.service

Dạng lệnh không có tùy chọn sẽ duyệt Requires=Wants= theo hướng từ trên xuống. --reverse cho biết những unit nào pull unit của bạn vào, nhờ đó bạn tìm được target khởi động nó khi boot. --after--before hiển thị thứ tự khởi động. Hãy đọc cả hai khi cần xác định có thành phần nào thực sự chờ hay không.

journalctl -b -u inventory-api.service --no-pager
journalctl -b -o short-precise -u inventory-api.service -u postgresql.service

Lệnh thứ hai interleave 2 unit với timestamp độ chính xác đến millisecond. Đây là cách chứng minh race do thứ tự khởi động thay vì phỏng đoán. Lỗi pg_isready xuất hiện trước khi PostgreSQL ghi log database system is ready to accept connections, và khoảng cách giữa 2 sự kiện hiển thị ngay trong output.

systemd-analyze verify /etc/systemd/system/inventory-api.service
systemd-analyze critical-chain inventory-api.service

verify load unit theo cách systemd sẽ load và báo các directive không xác định, dependency đến unit không tồn tại, vòng lặp thứ tự và cú pháp không thể parse. Lệnh này không thay đổi gì trên hệ thống. critical-chain in chuỗi thứ tự đã làm unit khởi động trễ, kèm thời điểm mỗi bước trở nên active. Lệnh này chỉ hoạt động với unit đã khởi động trong lần boot hiện tại.

Sau khi chỉnh sửa bất kỳ unit file nào, hãy chạy sudo systemctl daemon-reload. Để thay đổi unit do package cung cấp, hãy dùng sudo systemctl edit inventory-api.service. Lệnh này sẽ tạo drop-in cho bạn. Chỉnh sửa file của vendor trong /usr/lib/systemd/system/ chỉ có tác dụng cho đến lần package upgrade tiếp theo, khi file bị thay thế. Cơ chế drop-in tương tự cũng cho phép bạn gắn giới hạn memory và CPU cho một service mà không cần sửa file do package quản lý.

Vòng lặp thứ tự và dòng log mà chúng để lại trong journal

Thêm thứ tự theo cả hai hướng khiến systemd phá vòng lặp bằng cách xóa một job:

systemd[1]: Found ordering cycle on inventory-api.service/start
systemd[1]: Job postgresql.service/start deleted to break ordering cycle starting with inventory-api.service/start

systemd tự chọn job cần xóa và có thể không chọn job mà bạn muốn. Kết quả là service có lúc biến mất sau một số lần reboot, có lúc lại xuất hiện sau những lần khác. Điều này rất khó debug từ bên ngoài. Hầu hết vòng lặp phát sinh từ các unit đặt DefaultDependencies=no rồi vẫn sắp xếp thứ tự với basic.target, hoặc từ việc thêm Before= vào một unit đã có After= trỏ ngược lại unit của bạn. systemd-analyze verify phát hiện các vòng lặp này mà không cần reboot.

Unit cố định

[Unit]
Description=Inventory API
Wants=postgresql.service network-online.target
After=postgresql.service network-online.target
ConditionPathExists=/etc/inventory/api.conf

[Service]
Type=notify
StateDirectory=inventory
ExecStart=/usr/local/bin/inventory-api
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Mỗi dòng chỉ thực hiện một nhiệm vụ. Wants= đưa cả hai dependency vào transaction nhưng không ràng buộc vòng đời của unit này với chúng. After= thực hiện việc chờ, và phải lặp lại cả hai tên vì dependency và thứ tự khởi động là hai thiết lập riêng biệt. ConditionPathExists= có nghĩa là máy đã cài package nhưng chưa có config sẽ âm thầm bỏ qua unit thay vì phát cảnh báo. Đây là hành vi phù hợp với service do config điều khiển. Type=notify có nghĩa là mọi unit được sắp xếp chạy sau unit này sẽ chờ đến khi service thực sự sẵn sàng, thay vì chỉ chờ tiến trình fork. Restart=on-failure xử lý trường hợp database ngừng hoạt động sau khi hệ thống đã boot lâu, vì việc sắp xếp thứ tự chỉ áp dụng cho lần start đầu tiên. Mức độ retry cần mạnh đến đâu do các thiết lập Restart= và RestartSec= kiểm soát.

Hãy kiểm tra unit trước khi tin tưởng nó:

sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemctl list-dependencies --after inventory-api.service
sudo systemctl start inventory-api.service
systemctl show inventory-api.service -p ConditionResult -p ActiveState -p Result

Một unit hoạt động bình thường sẽ đọc ConditionResult=yes cùng với ActiveState=active, còn Result=success xác nhận rằng lần chạy trước không thất bại. ConditionResult=no đi cùng ActiveState=inactive có nghĩa là unit đã bị bỏ qua; dòng journal nêu tên condition sẽ cho biết bài kiểm tra nào không đạt.

FAQ

Requires= có chờ unit kia khởi động không?

Không. Requires=After= là hai thiết lập riêng biệt. Requires= đưa unit kia vào cùng transaction, sau đó systemd khởi động cả hai job song song. Để chờ, thêm After= và chỉ rõ cùng unit đó. Có thêm một lý do để thêm thiết lập này: dependency Requires= bị lỗi chỉ ngăn unit của bạn khởi động khi đồng thời đặt After=, vì nếu không có ordering thì unit của bạn đã được khởi động trước khi unit kia bị lỗi.

Nên order sau network.target hay network-online.target?

Khi boot, network.target chỉ có nghĩa là phần mềm quản lý network đã được khởi động. Nó không đảm bảo đã có địa chỉ hoặc route. Dùng network-online.target khi service cần một địa chỉ đang hoạt động ngay lúc khởi động, và khai báo cả Wants=network-online.target lẫn After=network-online.target, vì target này không nằm trong boot transaction mặc định và chỉ dùng After= sẽ chờ một unit không được đưa vào queue. Nếu service chỉ fail vì bind vào một IP cụ thể, dùng net.ipv4.ip_nonlocal_bind=1 cùng Restart=on-failure sẽ nhẹ hơn so với trì hoãn boot.

Vì sao unit của tôi báo thành công nhưng không bao giờ chạy?

Khi test Condition...= fail, systemd bỏ qua unit và báo start job thành công, nên không có gì được đánh dấu là failed. Chạy systemctl show <unit> -p ConditionResult, rồi ConditionResult=no sẽ xác nhận điều đó. Sau đó đọc journalctl -b -u <unit> để tìm dòng Condition check resulted in <description> being skipped. Trên systemd 250 trở lên, systemctl status <unit> cũng chỉ rõ directive chính xác không đạt điều kiện.

Condition khác Assert như thế nào?

Chúng thực hiện các test giống hệt nhau. Khi Condition fail, systemd âm thầm bỏ qua unit và job vẫn thành công. Khi Assert fail, unit bị đánh dấu failed, log ghi Assertion failed for <description>. và unit ở trạng thái failed (Result: assert). Dùng Condition cho trường hợp “unit này không áp dụng trên máy này”, phù hợp với gần như mọi tình huống thực tế. Chỉ dùng Assert khi một precondition bị thiếu cần hiển thị cho người theo dõi các unit failed.

Vì sao ExecStartPre fail với status=203/EXEC?

203/EXEC có nghĩa là systemd hoàn toàn không thể thực thi command. Các nguyên nhân thường gặp là path không phải absolute path, binary không tồn tại trên máy đó, file không có execute bit, hoặc script có dòng #! trỏ đến một interpreter không tồn tại. Các mã nhỏ khác của systemd lấy từ một bảng cố định, nên status=2/INVALIDARGUMENT chỉ có nghĩa là command đã thoát với mã 2 và không cho biết gì về các argument. Hãy nhớ rằng ExecStartPre= không chạy qua shell, nên pipe và glob cần /bin/sh -c '...'.

#systemd#units#dependencies#ordering#troubleshooting