Docker Deev Dive #2: Image & Layer — Cơ chế đóng gói

10 phút đọc

Overview

Docker Image là một read-only template chứa mọi thứ cần thiết để tạo container: OS base, runtime, dependencies, app code, config. Image được xây dựng từ Dockerfile — mỗi instruction tạo ra một layer. Các layer được stack lên nhau theo thứ tự, tạo thành filesystem hoàn chỉnh thông qua Union Filesystem.

First-principles: Image = tập hợp các layer read-only xếp chồng. Container = image + 1 writable layer trên cùng.


Key Concepts

1. Image Layer là gì?

Mỗi layer chứa một tập hợp thay đổi filesystem (additions, deletions, modifications) so với layer trước đó.

  • Mỗi instruction trong Dockerfile thay đổi filesystem sẽ tạo một layer mới (FROM, RUN, COPY, ADD)
  • Các instruction chỉ thay đổi metadata thì không tạo layer (CMD, LABEL, ENV, EXPOSE, ENTRYPOINT)
  • Mỗi layer sau khi tạo ra là immutable (bất biến, không thể sửa)

2. Dockerfile → Layer mapping

Xét Dockerfile mẫu cho PHP/Laravel:

# Layer 1: Base OS (FROM tạo layer đầu tiên)
FROM php:8.3-fpm-alpine

# Metadata only — KHÔNG tạo layer
LABEL maintainer="trung@example.com"

# Layer 2: Cài system dependencies
RUN apk add --no-cache \
    libpng-dev \
    libjpeg-turbo-dev \
    libzip-dev \
    && docker-php-ext-install pdo_mysql zip gd

# Layer 3: Cài Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Layer 4: Copy composer files (tận dụng cache!)
COPY composer.json composer.lock ./

# Layer 5: Install PHP dependencies
RUN composer install --no-dev --no-scripts --no-autoloader

# Layer 6: Copy toàn bộ source code
COPY . .

# Layer 7: Generate autoloader
RUN composer dump-autoload --optimize

# Metadata only — KHÔNG tạo layer
EXPOSE 9000
CMD ["php-fpm"]
Instruction Tạo Layer? Giải thích
FROM ✅ Có Tạo base layer từ image gốc
LABEL ❌ Không Chỉ thêm metadata
RUN ✅ Có Thực thi lệnh → ghi thay đổi filesystem vào layer mới
COPY / ADD ✅ Có Copy file từ build context vào image
ENV ❌ Không Set biến môi trường (metadata)
EXPOSE ❌ Không Khai báo port (metadata, không mở port thực tế)
CMD / ENTRYPOINT ❌ Không Khai báo lệnh chạy mặc định (metadata)
WORKDIR ✅ Có (nếu tạo dir mới) Set working directory — tạo layer nếu directory chưa tồn tại

3. Content-Addressable Storage

Mỗi layer được định danh bằng SHA256 hash của nội dung. Điều này có nghĩa:

  • Hai layer có cùng nội dung → cùng hash → chỉ lưu 1 lần trên disk
  • Nhiều image có thể share layer giống nhau
  • Khi pull image, Docker chỉ tải về layer chưa có locally
# Xem các layer của image
docker image inspect --format '.RootFS.Layers' php:8.3-fpm-alpine

# Xem chi tiết từng layer và size
docker image history php:8.3-fpm-alpine

How It Works — Union Filesystem & Copy-on-Write

Union Filesystem (UnionFS)

Docker sử dụng Union Filesystem (mặc định là overlay2) để xếp chồng các layer:

  1. Mỗi layer sau khi download được giải nén vào directory riêng trên host (/var/lib/docker/overlay2/)
  2. Khi tạo container, Union Filesystem merge tất cả layer thành một unified view duy nhất
  3. Root directory của container được set bằng chroot vào unified directory này
┌─────────────────────────────────┐
│   Writable Container Layer      │  ← Chỉ container này có
├─────────────────────────────────┤
│   Layer 7: composer dump-auto   │  ← Read-only
├─────────────────────────────────┤
│   Layer 6: COPY . .             │  ← Read-only
├─────────────────────────────────┤
│   Layer 5: composer install     │  ← Read-only
├─────────────────────────────────┤
│   Layer 4: COPY composer.*      │  ← Read-only
├─────────────────────────────────┤
│   Layer 3: COPY composer binary │  ← Read-only
├─────────────────────────────────┤
│   Layer 2: RUN apk add + ext   │  ← Read-only
├─────────────────────────────────┤
│   Layer 1: php:8.3-fpm-alpine   │  ← Read-only (base)
└─────────────────────────────────┘

Copy-on-Write (CoW)

Khi container cần đọc file → tìm từ layer trên xuống, dùng file tìm được đầu tiên.

Khi container cần ghi/sửa file:

  1. Tìm file trong các image layer (từ trên xuống)
  2. Copy file lên writable container layer (copy_up operation)
  3. Mọi thay đổi chỉ xảy ra trên bản copy này
  4. File gốc trong image layer không bao giờ bị thay đổi

Ý nghĩa thực tế của CoW

  • Nhiều container cùng image → share 100% read-only layers → tiết kiệm disk
  • docker ps --size cho thấy:
    • size = dung lượng writable layer riêng của container
    • virtual size = read-only layers + writable layer
  • 5 container cùng image 100MB → chỉ tốn ~100MB + (5 × writable layer size), không phải 500MB

Layer Caching — Tối ưu build time

Cơ chế cache

Khi build image, Docker kiểm tra từng instruction theo thứ tự:

  • Nếu instruction + context không thay đổidùng cached layer (cache hit ✅)
  • Nếu có thay đổi → rebuild layer đó + tất cả layer sau (cache invalidation ❌)

Ví dụ: Tại sao COPY composer.json trước COPY . .

❌ Cách tệ — mỗi lần sửa bất kỳ file nào, composer install phải chạy lại:

COPY . .                          # Layer A: cache miss mỗi khi bất kỳ file nào thay đổi
RUN composer install              # Layer B: phải rebuild vì Layer A đã miss

✅ Cách tốt — chỉ rebuild composer install khi dependencies thực sự thay đổi:

COPY composer.json composer.lock ./ # Layer A: chỉ miss khi composer.json/lock thay đổi
RUN composer install                # Layer B: cache hit nếu Layer A hit
COPY . .                            # Layer C: miss khi code thay đổi, nhưng B không bị ảnh hưởng

Thứ tự instruction tối ưu cho Laravel

1. FROM          ← Ít thay đổi nhất (base image)
2. RUN apk add   ← System deps, hiếm khi đổi
3. COPY composer.json composer.lock
4. RUN composer install  ← Chỉ rebuild khi deps thay đổi
5. COPY . .      ← Code thay đổi thường xuyên nhất
6. RUN artisan   ← Post-build commands

Các lệnh Dockerfile chi tiết

FROM

FROM <image>:<tag>
  • Khởi tạo build stage mới, set base image
  • Mỗi Dockerfile phải bắt đầu bằng FROM (trừ ARG trước FROM)
  • Tag nên pin cụ thể để đảm bảo reproducible builds: php:8.3-fpm-alpine thay vì php:latest

RUN

# Exec form (khuyến nghị)
RUN ["apt-get", "install", "-y", "nginx"]

# Shell form
RUN apt-get update && apt-get install -y nginx
  • Thực thi command trong layer mới
  • Gộp nhiều RUN để giảm số layer:
# ❌ 3 layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# ✅ 1 layer + cleanup trong cùng layer
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*

COPY vs ADD

Tiêu chí COPY ADD
Copy file từ build context
Auto-extract .tar.gz
Download từ URL ✅ (không khuyến nghị)
Khuyến nghị Dùng mặc định Chỉ khi cần extract tar

WORKDIR

WORKDIR /var/www/html
  • Set working directory cho RUN, CMD, ENTRYPOINT, COPY, ADD phía sau
  • Nếu directory chưa tồn tại → tự động tạo
  • Luôn dùng WORKDIR thay vì RUN cd /path && ...

CMD vs ENTRYPOINT

Tiêu chí CMD ENTRYPOINT
Mục đích Default command, dễ override Fixed executable, khó override
Override khi docker run Bị thay thế hoàn toàn Args được append vào
Kết hợp CMD cung cấp default args cho ENTRYPOINT ENTRYPOINT là main executable
# CMD: user có thể override hoàn toàn
CMD ["php-fpm"]
# docker run myapp nginx  ← chạy nginx thay vì php-fpm

# ENTRYPOINT + CMD: fixed executable + default args
ENTRYPOINT ["php"]
CMD ["artisan", "serve"]
# docker run myapp artisan migrate  ← chạy php artisan migrate

Kiểm tra Layer trong thực tế

docker image history

$ docker image history my-laravel-app

IMAGE          CREATED        CREATED BY                                      SIZE
a1b2c3d4e5f6   2 min ago      CMD ["php-fpm"]                                 0B
<missing>      2 min ago      COPY . . # buildkit                             15.2MB
<missing>      2 min ago      RUN composer install --no-dev # buildkit         45.3MB
<missing>      2 min ago      COPY composer.json composer.lock ./ # buildkit   3.2KB
<missing>      5 min ago      RUN apk add --no-cache ... # buildkit            12.1MB
<missing>      3 weeks ago    /bin/sh -c #(nop) CMD ["php-fpm"]                0B
<missing>      3 weeks ago    ...                                              85.4MB

Đọc kết quả:

  • Size = 0B → metadata layer, không chiếm dung lượng
  • <missing> trong IMAGE column → layer được build trên hệ thống khác hoặc bởi BuildKit
  • Layer trên cùng = instruction cuối cùng trong Dockerfile

docker image inspect

# Xem tất cả layer SHA256
docker image inspect --format '.RootFS.Layers' my-laravel-app

# Xem tổng size
docker image inspect --format '.Size' my-laravel-app

docker system df

$ docker system df

TYPE            TOTAL   ACTIVE   SIZE      RECLAIMABLE
Images          5       2        1.024GB   756.2MB (73%)
Containers      2       1        5.312MB   5.12MB (96%)
Local Volumes   3       2        256.1MB   128MB (50%)
Build Cache     12      0        312.4MB   312.4MB (100%)

Trade-offs

Pros Cons
Layer sharing giảm disk và bandwidth File xóa ở layer sau vẫn tồn tại ở layer trước → image size phình
Layer caching tăng tốc build đáng kể Cache invalidation lan truyền (1 layer miss → tất cả sau miss)
Immutable layers → reproducible, dễ rollback Nhiều layer quá → tăng pull time và overhead
CoW cho phép nhiều container share image CoW copy_up gây performance hit cho file lớn lần đầu sửa

Thực hành

Bài 1: Build PHP image đơn giản và phân tích layer

# 1. Tạo Dockerfile
cat > Dockerfile <<'EOF'
FROM php:8.3-cli-alpine
RUN apk add --no-cache curl
COPY . /app
WORKDIR /app
CMD ["php", "-S", "0.0.0.0:8000"]
EOF

# 2. Tạo file test
echo '<?php echo "Hello Docker!";' > index.php

# 3. Build image
docker build -t php-layer-test .

# 4. Phân tích layers
docker image history php-layer-test
docker image inspect --format '.RootFS.Layers' php-layer-test

Bài 2: Quan sát cache hit/miss

# Build lần 1 — tất cả layer mới
docker build -t cache-test .

# Sửa index.php
echo '<?php echo "Updated!";' > index.php

# Build lần 2 — quan sát:
# - FROM, RUN apk: CACHED ✅
# - COPY . /app: REBUILD ❌ (file thay đổi)
docker build -t cache-test .

Bài 3: Chứng minh xóa file không giảm size

cat > Dockerfile.fat <<'EOF'
FROM alpine
RUN dd if=/dev/zero of=/bigfile bs=1M count=100
RUN rm /bigfile
EOF

cat > Dockerfile.slim <<'EOF'
FROM alpine
RUN dd if=/dev/zero of=/bigfile bs=1M count=100 && rm /bigfile
EOF

docker build -t fat-image -f Dockerfile.fat .
docker build -t slim-image -f Dockerfile.slim .

# So sánh size
docker image ls | grep -E 'fat|slim'
# fat-image:  ~105MB (bigfile vẫn tồn tại trong layer trước)
# slim-image: ~7MB   (bigfile tạo và xóa trong cùng layer)

Bài 4: Kiểm tra layer sharing giữa nhiều container

# Chạy 3 container cùng image
docker run -d --name c1 php-layer-test
docker run -d --name c2 php-layer-test
docker run -d --name c3 php-layer-test

# Kiểm tra size — writable layer gần bằng 0
docker ps --size --format 'table .Names\t.Size'

Checklist kiểm tra hiểu biết

  • Giải thích được image layer là gì và tại sao immutable
  • Phân biệt được instruction nào tạo layer, instruction nào không
  • Giải thích được cơ chế Copy-on-Write
  • Giải thích được tại sao COPY composer.json trước COPY . . giúp tối ưu cache
  • Chứng minh được xóa file ở RUN riêng không giảm image size
  • Dùng docker image history phân tích layer structure của bất kỳ image nào

References

Bài viết liên quan

Đang cập nhật...