Phân tích
6 phút đọc15 tháng 6, 2026283

Docker Image hoạt động như thế nào bên dưới?

Deep dive vào layer architecture, Union File System, và những thứ thực sự xảy ra khi bạn pull hoặc build một Docker image.

N

Nguyễn Nhật Long

@nguyennhatlong1303

Docker Image hoạt động như thế nào bên dưới?

Nếu bạn đã đọc hai bài trước trong series này, giờ bạn đã biết cách cài Docker và chạy container rồi. Nhưng mỗi lần gõ docker run nginx hay docker pull node:20, bạn có tự hỏi cái "image" đó thực ra là gì không? Nó được lưu ở đâu? Tại sao pull lần đầu thì lâu, nhưng lần sau lại nhanh hơn? Bài này mình sẽ đào sâu vào phần đó.

Image không phải file, nó là một stack các layers

Cái hay nhất của Docker image và cũng là thứ nhiều người không để ý là image không phải một file đơn lẻ. Nó là một tập hợp các layers xếp chồng lên nhau, mỗi layer là một snapshot của filesystem tại một thời điểm.

Hãy nghĩ theo kiểu này: image giống như một class trong OOP, còn container là object được khởi tạo từ class đó. Một image có thể spawn ra hàng chục container, và mỗi container có state riêng của nó mà không ảnh hưởng gì đến image gốc hay các container khác.

Mỗi instruction trong Dockerfile tạo ra một layer mới:

Dockerfile
1FROM node:20-alpine # layer 1 base image
2WORKDIR /app # layer 2
3COPY package*.json ./ # layer 3
4RUN npm install # layer 4 layer nặng nhất
5COPY . . # layer 5
6CMD ["node", "index.js"] # layer 6

Bạn có thể tự verify điều này bằng lệnh:

Terminal
1docker history node:20-alpine

Output sẽ liệt kê từng layer với size và instruction tương ứng. Mình hay dùng lệnh này để debug xem cái gì đang chiếm nhiều dung lượng nhất trong image.

Union File System thứ kết dính tất cả lại

Vậy Docker gộp đống layers đó thành một filesystem duy nhất như thế nào? Đây là lúc Union File System (cụ thể là OverlayFS trên Linux hiện đại) xuất hiện.

OverlayFS hoạt động theo cơ chế overlay: nó lấy nhiều directories (gọi là "branches") và mount chúng lại thành một view thống nhất duy nhất. Với Docker:

  • Tất cả layers của image là read-only không ai được sửa
  • Khi bạn start một container, Docker thêm vào một writable layer mỏng ở trên cùng
  • Mọi thứ bạn làm trong container (tạo file, sửa file, cài thêm package) đều ghi vào writable layer này
  • Khi container bị xóa, writable layer biến mất, image gốc vẫn nguyên vẹn

Cơ chế này gọi là Copy-on-Write (CoW). Nếu container cần sửa một file từ layer bên dưới, nó sẽ copy file đó lên writable layer trước, rồi mới sửa. Image gốc không bao giờ bị động đến.

Theo kinh nghiệm của mình, hiểu CoW giúp ích rất nhiều khi debug performance. Ví dụ nếu container của bạn đang làm nhiều thao tác write vào filesystem (như ghi log, generate file), bạn cần tính đến overhead của CoW đặc biệt với các file lớn.

Tại sao layers lại quan trọng với build time

Đây là phần thực tế nhất. Docker cache layer theo từng instruction. Nếu một layer không thay đổi, Docker sẽ reuse cache thay vì rebuild. Điều này có nghĩa là thứ tự các instruction trong Dockerfile ảnh hưởng trực tiếp đến build time.

Xem ví dụ này:

Dockerfile
1# ❌ Bad mỗi lần thay code là phải npm install lại
2FROM node:20-alpine
3WORKDIR /app
4COPY . . # copy toàn bộ code trước
5RUN npm install # layer này bị invalidate mỗi khi code thay đổi
Dockerfile
1# ✅ Good npm install chỉ chạy lại khi package.json thay đổi
2FROM node:20-alpine
3WORKDIR /app
4COPY package*.json ./ # copy package.json trước
5RUN npm install # layer này được cache, chỉ rebuild khi dependencies thay đổi
6COPY . . # copy code sau

Cái trick đơn giản này có thể giảm build time từ vài phút xuống còn vài giây trong daily workflow. Anh em lưu ý điểm này mình thấy đây là một trong những quick win dễ nhất khi optimize CI/CD pipeline.

Chọn base image đừng mặc định dùng latest

Một trong những quyết định quan trọng nhất khi viết Dockerfile là chọn base image. Và mình thấy nhiều người hay bỏ qua phần này.

Alpine thường là lựa chọn mặc định khi muốn image nhỏ. Nhưng mình muốn cảnh báo một điểm: Alpine dùng musl libc thay vì glibc như các distro khác. Một số native modules của Node.js hoặc Python packages compiled cho glibc có thể gặp vấn đề khi chạy trên Alpine. Nếu bạn gặp lỗi kỳ lạ khi build, thử switch sang node:20-slim xem có hết không.

Base ImageSize (approx)Đặc điểmNên dùng khi nào
`ubuntu` / `debian`~150MBFull-featured, nhiều tool có sẵnDev environment, cần nhiều system tools
`debian-slim`~80MBDebian stripped, bỏ bớt packages không cầnProduction apps cần compatibility
`alpine`~5MBMinimal Linux, dùng musl libcSize-critical, microservices
`distroless`~20MBGoogle's minimal, không có shellSecurity-critical production workloads

Distroless thì thú vị ở chỗ nó không có shell không có bash, không có sh, không có package manager. Điều này làm giảm attack surface đáng kể cho production. Nhưng debug cũng khó hơn nhiều vì bạn không thể exec vào container và chạy lệnh tùy ý.

Về tag strategy, rule đơn giản là: không bao giờ dùng latest trong production. Tag latest không có gì đảm bảo là stable nó chỉ là alias cho build mới nhất. Hôm nay node:latest là v20, tuần sau có thể là v22 với breaking changes. Luôn pin version cụ thể:

Terminal
1# ❌ Không nên
2FROM node:latest
3FROM nginx
4
5# ✅ Nên làm
6FROM node:20.10-alpine3.18
7FROM nginx:1.25.3-alpine

Quản lý images đừng để disk đầy

Sau một thời gian làm việc với Docker, disk sẽ bắt đầu đầy dần vì images cũ tích tụ. Mình hay dùng mấy lệnh này:

Terminal
1# Xem tất cả images
2docker images
3# hoặc
4docker image ls
5
6# Xem disk usage tổng quan cái này rất hữu ích
7docker system df
8
9# Xóa một image cụ thể
10docker rmi nginx:1.24
11
12# Xóa images không có container nào đang dùng
13docker image prune
14
15# Xóa tất cả unused images (kể cả có tag)
16docker image prune -a

Lệnh docker system df theo mình là underrated nó cho bạn thấy ngay images, containers, và volumes đang chiếm bao nhiêu dung lượng, và bao nhiêu trong số đó là "reclaimable". Mình thường chạy lệnh này trước khi chạy prune để biết mình đang giải phóng bao nhiêu.

Một điểm nữa cần biết: khi bạn xóa image bằng docker rmi, Docker chỉ xóa nếu không có container nào (kể cả stopped containers) đang reference image đó. Nếu gặp lỗi, chạy docker ps -a để xem có container stopped nào còn đó không, rồi xóa container trước.

Layers được share đây là lý do pull nhanh lần hai

Quay lại câu hỏi ban đầu: tại sao pull image lần hai nhanh hơn?

Vì layers được share và cache giữa các images. Nếu bạn đã có node:20-alpine và muốn pull node:20-alpine với một tag khác, Docker chỉ download những layers chưa có trên máy. Các layers trùng nhau sẽ được reuse.

Điều này cũng có nghĩa là nếu nhiều images của bạn cùng dùng chung base image (ví dụ đều FROM node:20-alpine), chúng sẽ share layers đó thay vì mỗi image lưu một bản riêng. Đây là lý do tại sao tổng disk usage thực tế thường nhỏ hơn nhiều so với bạn tính tổng size của từng image.

Bài tiếp theo mình sẽ đi vào multi-stage builds kỹ thuật để giảm image size đáng kể, đặc biệt với các compiled languages như Go hay Java. Đây là thứ mình nghĩ mọi người nên biết trước khi đưa bất kỳ Dockerfile nào lên production.

NN

Nguyễn Nhật Long

@nguyennhatlong1303

Nguyễn Nhật Long is a Senior Frontend Engineer and Frontend Team Leader with 7 years of experience building real-time fintech platforms. Specializing in React, Next.js, TypeScript, and React Native, shipping 10+ products across Web, Mobile, Telegram Mini-Apps, and Web3.

Thấy hay? Chia sẻ cho bạn bè!

Docker Image hoạt động như thế nào bên dưới? — Stacklog