Vấn đề: Tại sao Docker Container của bạn luôn mất 10 giây để tắt?
Nếu bạn chạy docker stop và phải đợi đúng 10 giây máy mới chịu dừng, đó không phải vì ứng dụng của bạn quá nặng. Thực tế, bạn đang gặp vấn đề về Signal Handling. Docker gửi tín hiệu SIGTERM yêu cầu ứng dụng đóng kết nối và dừng lại êm đẹp. Nếu ứng dụng “phớt lờ” quá 10 giây, Docker sẽ mất kiên nhẫn và dùng SIGKILL để ép chết process ngay lập tức.
Nhiều người lầm tưởng lỗi do database ngắt kết nối chậm. Tuy nhiên, nguyên nhân gốc rễ thường nằm ở cách Linux quản lý PID 1 bên trong Container. Phần lớn các runtime phổ biến như Node.js, Python hay Java không được thiết kế để đảm nhận vai trò của một Init Process chuyên nghiệp.
Trong một dự án cũ, mình từng đau đầu vì server cứ sau 1 tuần lại báo cạn kiệt tài nguyên PID dù RAM vẫn trống. Sau khi kiểm tra bằng lệnh ps aux, mình tá hỏa thấy hàng trăm tiến trình ở trạng thái <defunct>. Đó chính là những Zombie Process tích tụ do không được dọn dẹp đúng cách.
Tại sao PID 1 lại đặc biệt quan trọng?
Trách nhiệm của “Tiến trình tổ tiên”
Trong Linux, tiến trình có ID bằng 1 (PID 1) là gốc rễ của mọi tiến trình khác. Nó gánh vác hai trọng trách mà bạn không thể bỏ qua:
- Chuyển tiếp tín hiệu (Signal Forwarding): Khi bạn nhấn Ctrl+C hoặc gọi lệnh stop, hệ thống gửi tín hiệu
SIGINT/SIGTERM. PID 1 phải nhận và “truyền tin” xuống các tiến trình con bên dưới. - Thu dọn tàn dư (Reaping): Khi một tiến trình con kết thúc, nó không biến mất ngay mà trở thành thây ma (Zombie). PID 1 có nhiệm vụ xác nhận để nhân Linux giải phóng tài nguyên cho thây ma đó.
Khi bạn khai báo ENTRYPOINT ["node", "index.js"], Node.js sẽ chiếm giữ PID 1. Ngặt nỗi, Node.js không tự động dọn dẹp con của nó hoặc chuyển tiếp tín hiệu như cách systemd hay init vẫn làm trên OS thông thường.
Hệ lụy từ Zombie Process
Hãy tưởng tượng ứng dụng của bạn gọi một script Shell để xử lý ảnh hoặc gửi mail. Nếu script đó chạy xong mà PID 1 không thực hiện thao tác “reap”, tiến trình đó sẽ tồn tại mãi trong bảng quản lý của nhân Linux. Với các hệ thống chạy hàng nghìn task ngắn hạn mỗi giờ, số lượng thây ma này có thể làm treo toàn bộ server vì tràn bảng PID.
Tini – Giải pháp “nhỏ mà có võ”
Tini là một Init Process siêu nhẹ, chỉ khoảng 20-30 KB và được viết bằng C. Nó sinh ra để làm đúng một việc: làm PID 1 chuyên nghiệp cho container. Tini sẽ thay mặt ứng dụng của bạn nhận tín hiệu và dọn dẹp mọi thây ma phát sinh.
# Cơ chế hoạt động của Tini:
[tini] (PID 1) --> [app] (PID 2) --> [child] (ZOMBIE!)
# Tini phát hiện child kết thúc --> thực hiện Reaping --> Giải phóng bảng PID.
3 cách tích hợp Tini vào quy trình làm việc
Cách 1: Sử dụng flag –init (Nhanh và tiện nhất)
Kể từ phiên bản 1.13, Docker đã tích hợp sẵn Tini. Bạn không cần sửa code hay Dockerfile, chỉ cần thêm cờ --init khi khởi chạy container.
docker run --init -d my-app:latest
Đây là cách tốt nhất để kiểm tra nhanh xem ứng dụng của bạn có đang gặp vấn đề về Signal Handling hay không mà không tốn công build lại image.
Cách 2: Đóng gói trực tiếp vào Dockerfile (Chuẩn Production)
Để đảm bảo container chạy ổn định trên mọi môi trường như Kubernetes hay AWS ECS, bạn nên cài Tini trực tiếp vào Image. Ví dụ với Alpine Linux:
FROM python:3.9-alpine
RUN apk add --no-cache tini
WORKDIR /app
COPY . .
# Luôn dùng Tini làm ENTRYPOINT
ENTRYPOINT ["/sbin/tini", "--"]
# Ứng dụng chính đặt trong CMD
CMD ["python", "app.py"]
Mẹo nhỏ: Luôn dùng exec form (dạng mảng ["..."]). Nếu dùng shell form (python app.py), Docker sẽ bọc lệnh trong /bin/sh -c. Lúc này Shell lại thành PID 1 và vấn đề cũ lại tái diễn.
Cách 3: Cấu hình qua Docker Compose
Với các dự án dùng Compose, bạn chỉ cần thêm một dòng thuộc tính đơn giản trong file docker-compose.yml:
services:
api:
image: my-node-app
init: true
ports:
- "8080:8080"
Cảnh báo khi dùng Bash Script làm Entrypoint
Rất nhiều bạn viết file entrypoint.sh để chạy migration trước khi start app. Nếu bạn viết /usr/bin/python main.py ở dòng cuối, script shell sẽ chiếm PID 1. Khi nhận SIGTERM, script này chết ngay nhưng tiến trình Python bên trong vẫn bị treo lại thành “mồ côi” (orphan). Hãy dùng lệnh exec để thay thế tiến trình shell bằng tiến trình app: exec python main.py.
Lời kết
Dùng Tini không chỉ là để tránh việc phải đợi 10 giây mỗi khi deploy. Đây là tiêu chuẩn để xây dựng các hệ thống container bền bỉ, tránh rò rỉ tài nguyên hệ điều hành. Nếu bạn đang quản lý các dịch vụ quan trọng trên Production, hãy dành 5 phút để kiểm tra lại PID 1. Một thay đổi nhỏ này sẽ giúp hệ thống của bạn chuyên nghiệp và ổn định hơn rất nhiều.

