# Ba thứ hỏng mà không kêu, cả ba đều do chính công cụ vận hành gây ra

apt báo 0 gói chờ nâng. Gắn Ubuntu Pro xong, cùng máy đó báo 25 bản vá bảo mật. Ghi lại một buổi rà soát ba máy chủ, và ba chỗ hỏng mà không có gì kêu lên.

Published: 2026-09-05 · Language: vi · Tags: Docker, Ubuntu, vận hành, giám sát · Canonical: https://hecigo.com/blog/ba-thu-hong-ma-khong-keu-do-chinh-cong-cu-van-hanh-gay-ra/

---

Buổi sáng hôm đó `apt list --upgradable` trên máy production trả về **0 gói chờ nâng**. Buổi chiều, sau khi gắn Ubuntu Pro vào đúng cái máy đó, không cài thêm gì, không đổi cấu hình gì, lệnh y hệt trả về **25 bản vá bảo mật đang chờ**.

Hai phép đo cùng một hệ, cách nhau vài tiếng, cho hai kết quả ngược nhau. Không cái nào sai. Bài này ghi lại buổi rà soát định kỳ đội ba máy chủ nhỏ của chúng tôi, và ba chỗ hỏng tìm thấy trong đó. Điểm chung của cả ba là thứ làm nó đáng viết ra: **không cái nào do phần mềm bên ngoài hỏng, cả ba đều do chính công cụ vận hành gây ra.**

## Định thử gì

Đội máy gồm ba cái: một máy production chạy n8n với một dịch vụ scraping tự dựng, một máy R&D để thử luồng tích hợp trước khi đưa vào dự án, và một máy lab ARM làm đích diễn tập khôi phục. Cả ba đều Ubuntu, đều Docker Compose, đều đã bật `unattended-upgrades`.

Câu hỏi ban đầu rất hẹp: có bản vá nào đang chờ không. Bộ rà soát viết ra để trả lời câu đó, chạy chung một script trên cả ba máy, chỉ đọc chứ không sửa gì ngoài `apt-get update`.

Nó trả lời được câu hỏi ban đầu trong ba phút. Phần còn lại của buổi chiều là những thứ nó lộ ra mà tôi không đi tìm.

## Cron dọn dẹp ăn mất ảnh mà service cần để sống

Lệnh `docker compose up -d` trên máy production dừng lại với một dòng:

```text
Error response from daemon: No such image: <ten-anh>:latest
```

Container đó vẫn đang chạy bình thường. Nó chạy được vì container giữ tham chiếu tới **image ID**, còn cái mất là **tag**. Chừng nào không ai đụng vào thì không có gì kêu, và không có gì trong log nói rằng chỉ cần container này dừng một lần là hết đường dựng lại.

Tag mất vì một dòng cron chúng tôi tự đặt:

```bash
8 0 * * * root docker image prune -af --filter "until=24h"
```

Tài liệu Docker viết rõ về cờ `-a`: nó *"remove all unused images, not just dangling ones"*, và *"also remove all images not referenced by any container"*. Phần "not referenced by any container" là chỗ tôi đọc lướt. Ảnh **có tag** vẫn bị xoá, miễn là không container nào tham chiếu tới nó.

Chi tiết thứ hai đắt hơn: `--filter until=` lọc theo **thời điểm tạo ảnh**, không phải thời điểm gắn tag. Nên khi tôi giữ một ảnh cũ làm điểm lùi bằng `docker tag`, ảnh đó vẫn mang ngày tạo gốc từ hai tuần trước, vẫn khớp `until=24h`, và biến mất trong lần cron chạy kế tiếp. Điểm lùi tôi vừa tạo sống được đúng một đêm.

Cách sửa là bỏ `-a`, để bản daily chỉ dọn ảnh mồ côi:

```bash
8 0 * * * root docker image prune -f --filter "until=24h"
```

Việc thu hồi dung lượng vẫn còn một job hàng tuần với cửa sổ `until=168h` lo. Và quy ước mới, ghi thẳng vào runbook: **giữ ảnh cũ bằng `docker tag` không phải là điểm lùi bền.** Muốn lùi được sau một đêm thì `docker save` ra tarball, vì tarball nằm ngoài tầm với của mọi lệnh prune.

> Related: [Đổi host và bốn hành vi nền tảng đang làm hộ mà bạn không sở hữu](https://hecigo.com/blog/doi-host-va-bon-hanh-vi-nen-tang-lam-ho-ma-ban-khong-so-huu/): Chuyển hecigo.com sang một host khác. Adapter mất mười lăm dòng. Bốn hành vi nền tảng vẫn làm hộ thì mỗi cái hỏng một kiểu, và không cái nào làm...

## Ảnh không dựng lại được vì compose không nói nó dựng từ đâu

Vì sao mất tag lại thành chặn được cả `compose up`? Vì khối service đó trông như thế này:

```yaml
services:
  scraper-browser:
    image: scraper-browser-custom:latest
    pull_policy: never
```

`pull_policy: never` nghĩa là *"Compose doesn't pull the image from a registry and relies on the platform cached image. If there is no cached image, a failure is reported."* Không có khối `build:`, cũng không có registry để kéo về. Ảnh nằm sẵn trên máy thì chạy, không nằm sẵn thì hết cách.

Điều đáng nói không phải là `pull_policy: never` sai. Nó đúng cho ảnh dựng tại chỗ. Điều sai là **compose không nói ảnh đó dựng từ đâu**, nên người đọc file không có đường nào lần ra mã nguồn.

Mã nguồn hoá ra vẫn còn, nằm trong thư mục của một service khác đã ngừng dùng từ lâu, cùng chỗ chỉ vì lịch sử chứ không vì liên quan. Không ai đọc compose mà đoán được điều đó.

Cách sửa là để mã nguồn cạnh chính file dùng nó, và để file tự khai:

```yaml
services:
  scraper-browser:
    build:
      context: ./scraper-browser
    image: scraper-browser-custom:latest
```

Dựng lại thật từ đầu để kiểm chứ không chỉ sửa file: container `healthy` sau 15 giây, và một lượt scrape thật qua trình duyệt trả 200 với 9.276 ký tự.

Câu tự kiểm cho hệ của bạn: **mở file compose ra, có service nào mà bạn không lần được từ đó tới mã nguồn hoặc tới một registry không?** Nếu có, nó đang ở cùng trạng thái với cái này trước hôm nay.

## 0 gói chờ nâng, khi không có nguồn nào để so

Cam kết bảo trì mặc định của Ubuntu LTS phủ repository **Main**, khoảng 2.300 gói, trong năm năm. `universe` không nằm trong đó. Tài liệu Ubuntu tách hai luồng ESM rõ ràng: *"The Infra pockets cover packages in the Main component, while the Apps pockets cover packages in the Universe component."*

Máy production của chúng tôi chạy 24.04, còn trong hạn hỗ trợ tiêu chuẩn tới 2029. Nên `apt` báo 0 gói chờ nâng là **đúng** với những nguồn nó đang có. Nó không sai, nó chỉ không có gì để so ở phần universe.

Gắn Ubuntu Pro xong, `noble-apps-security` xuất hiện trong danh sách nguồn, và cùng lệnh đó trả về 25 gói. Đây là những gì có trong đó:

```text
ffmpeg + toàn bộ họ libav*    imagemagick + libmagick*
python3-pip                   node-lodash
libmbedcrypto7t64             libopenexr, libcjson1, libzvbi
```

Cài hết trong một lượt: không cần reboot, không dịch vụ nào phải restart, không container nào bị đụng. Đây là gói của host, còn các container mang bản riêng trong ảnh của chúng, nên đợt vá này không chạm tới chúng và cũng không làm gián đoạn chúng. Đó vừa là tin tốt vừa là một nhắc nhở: vá host xong không có nghĩa là đã vá xong những gì chạy trong container.

Tôi cần tách hai mức phát biểu ở đây, vì chúng khác nhau về thẩm quyền. **Tài liệu Ubuntu nói** cam kết tiêu chuẩn phủ Main năm năm, và Pro mở rộng ra toàn bộ archive trong mười năm. **Chúng tôi đo được** là trên một máy 24.04 còn trong hạn tiêu chuẩn, việc gắn Pro làm lộ ra ngay 25 bản vá universe đang chờ. Phép đo đó đúng cho ba máy này; tôi không có dữ liệu để nói nó đúng cho mọi cấu hình.

Bản Ubuntu Pro cá nhân *"is and always will be free for personal use on up to 5 physical machines"*, đủ chỗ cho một đội ba máy.

Bài học đưa thẳng vào bộ rà soát, và nó là câu đáng nhớ nhất trong cả buổi: **"0 gói chờ nâng" chỉ có nghĩa khi biết mình đang so với những nguồn nào.** Script rà soát nay in trạng thái Pro chứ không chỉ đếm gói.

## Chỗ tôi làm sập production

Phần này không nằm trong kế hoạch.

Cùng buổi đó tôi phát hiện n8n tải khoảng 1,2 GB trình duyệt Playwright mỗi lần khởi động, vào lớp ghi của container chứ không phải volume. Nghĩa là mỗi lần `compose up` tạo lại container là tải lại từ đầu, và vì có hai container nên 2,8 GB cho cùng một bộ trình duyệt.

Cách sửa hiển nhiên là mount cache ra volume dùng chung:

```yaml
volumes:
  - n8n_cache:/home/node/.cache/ms-playwright   # dòng này làm sập production
```

Thư mục `/home/node/.cache` **không tồn tại sẵn trong ảnh**. Mount vào thư mục con của nó khiến Docker tự tạo thư mục cha, và tạo bằng `root`. n8n chạy bằng user `node`, mất quyền ghi `/home/node/.cache/n8n`, và crash-loop:

```text
EACCES: permission denied, mkdir '/home/node/.cache/n8n'
```

Production n8n chết vài phút trước khi tôi nhìn ra. Cách đúng là mount cả thư mục cha, sau khi đã đặt sở hữu cho volume:

```bash
docker volume create n8n_cache
docker run --rm -v n8n_cache:/c alpine chown 1000:1000 /c
```

```yaml
volumes:
  - n8n_cache:/home/node/.cache
```

Số đo sau khi sửa: worker sẵn sàng trong **18 giây** thay vì hơn hai phút, **0 dòng** "Downloading Chrome", lớp ghi của worker từ **1,37 GB xuống 0 B** và của n8n từ **1,45 GB xuống 67 MB**.

Lý do viết ra: nếu bài này chỉ có ba chỗ hỏng do người khác gây ra và không có chỗ nào do tôi, nó là quảng cáo chứ không phải lab note.

## Ý phản đối mạnh nhất với chính bài này

Ý phản đối tôi thấy khó cãi nhất là: **cả ba thứ này đều do chính chúng tôi tự đặt ra.** Cron `prune -af` là chúng tôi viết. `pull_policy: never` không kèm `build:` là chúng tôi để lại. Không gắn Pro là lựa chọn của chúng tôi. Vậy bài học có thật sự tổng quát, hay chỉ là ghi lại việc dọn nợ của chính mình?

Tôi nghiêng về vế thứ hai nhiều hơn tôi muốn thừa nhận. Ba cái này không phải quy luật của Docker hay của Ubuntu, chúng là hệ quả của những lựa chọn cụ thể trên ba máy cụ thể.

Nhưng có một điểm chung tôi nghĩ đi xa hơn ba máy này: **cả ba đều là công cụ vận hành làm đúng thứ nó được bảo làm.** Cron dọn ảnh đã dọn ảnh. `pull_policy: never` đã không kéo ảnh về. `apt` đã báo cáo đúng những nguồn nó có. Không cái nào là lỗi, nên không cái nào phát ra tín hiệu lỗi, và đó là lý do cả ba sống sót qua nhiều tháng giám sát.

Còn một ý phản đối nữa tôi không có số liệu để cãi: có thể bốn tiếng bỏ ra hôm đó lẽ ra nên dùng để dựng thêm giám sát thay vì rà tay. Tôi không đo được cái nào rẻ hơn, nên không nói được.

> **Thấy bài này dùng được?** Theo dõi hecigo trên [Zalo OA](https://zalo.me/3108963776852260798) để nhận bài kỹ thuật mới, hoặc [nhắn cho chúng tôi](https://hecigo.com/#contact) nếu hai hệ thống của bạn đang phải nói chuyện với nhau và có gì đó hỏng ở khoảng giữa.

> Related: [Đọc tiếp: Middleware: phần việc n8n, OpenClaw và mọi nền tảng tự động hóa không làm hộ bạn](https://hecigo.com/blog/middleware-chia-khoa-mo-khoa-toan-bo-tiem-nang-cua-n8n-openclaw-va-moi-nen-tang-/): Nối được API là phần dễ. Phần khó lộ ra sau vài tuần chạy thật: sự kiện gửi lại hai lần, webhook rơi mất một giao dịch, hóa đơn bị hủy nhưng hệ...

## Sau buổi đó

Bộ rà soát nay có thêm hai mục: một mục liệt kê service nào khai `pull_policy: never` mà không có `build:`, và một mục in trạng thái Ubuntu Pro trước khi đếm gói. Cả hai đều là câu hỏi mà buổi chiều đó dạy tôi phải hỏi.

Một chỗ vẫn chưa xong, và nó đáng ghi ra vì nó không sửa được bằng cấu hình. Máy lab của chúng tôi chạy Ubuntu 24.04 trên ARM, và livepatch báo `n/a` ở đó. Tôi tưởng lý do là kernel flavour, nhưng bảng kernel được phủ nói khác: flavour `oracle` có trong danh sách, chỉ là cho x86 64-bit. Với 24.04 thì arm64 chưa được phủ, và tài liệu ghi coverage cho arm64 bắt đầu từ bản 26.04. Nghĩa là hai máy x86 vá kernel không cần reboot, còn máy ARM thì vẫn phải, cho tới khi nâng bản.

Câu tôi chưa trả lời được, và cũng đang muốn biết: cả ba chỗ này chỉ lộ ra vì có người ngồi rà tay. Một cái cron ăn mất tag, một ảnh không dựng lại được, một danh sách bản vá rỗng vì thiếu nguồn - chúng đều không sinh ra tín hiệu nào để bắt. **Vậy loại hỏng "công cụ vận hành làm đúng thứ nó được bảo làm" thì giám sát được bằng cách nào, hay nó vốn chỉ bắt được bằng mắt người theo chu kỳ?**

## Nguồn tham khảo

- [docker image prune](https://docs.docker.com/reference/cli/docker/image/prune/) - Docker Docs
- [Services top-level element: pull_policy](https://docs.docker.com/reference/compose-file/services/) - Docker Docs
- [Expanded Security Maintenance (ESM)](https://documentation.ubuntu.com/security/security-updates/esm/) - Ubuntu security documentation
- [Ubuntu Pro](https://ubuntu.com/pro) - Ubuntu
- [Kernels covered by Livepatch](https://ubuntu.com/security/livepatch/docs/client/reference/platform/supported-kernels/) - Ubuntu

---

Published by hecigo, middleware & integration lab. https://hecigo.com · hi@hecigo.com
