Chào mừng bạn đến với series bài viết về Docker. Với tư cách là một chuyên gia đã làm việc với container trong nhiều năm, tôi sẽ không chỉ chỉ cho bạn “cái gì” mà còn là “tại sao”. Chúng ta sẽ đi từ những viên gạch nền móng đầu tiên cho đến việc điều phối (orchestration) các hệ thống phức tạp.
Khái niệm: Docker là gì?
Docker là một nền tảng mở (open platform) giúp các lập trình viên và quản trị hệ thống xây dựng, vận chuyển và chạy các ứng dụng phân tán.
Hiểu một cách đơn giản, Docker cho phép bạn “đóng gói” ứng dụng cùng với tất cả các thành phần cần thiết để nó chạy (như thư viện, file cấu hình, dependencies) vào một khối duy nhất gọi là Container. Nhờ đó, ứng dụng có thể hoạt động một cách trơn tru và đồng nhất trên bất kỳ môi trường nào, từ máy laptop của lập trình viên đến máy chủ production hay trên cloud.
Tại sao lại là Docker? Container vs. Máy ảo (VM)
Trước đây, chúng ta dùng Máy ảo (Virtual Machines). Một VM giả lập *toàn bộ phần cứng* (CPU, RAM, Ổ cứng) và chạy một hệ điều hành khách (Guest OS) đầy đủ. Điều này rất nặng nề, tốn tài nguyên và khởi động chậm.
Container (như Docker) thì khác. Nó “chia sẻ” nhân (kernel) của hệ điều hành máy chủ (Host OS). Nó chỉ đóng gói ứng dụng và các thư viện/dependencies cần thiết. Kết quả:
- Siêu nhẹ: Chỉ vài MB (so với hàng GB của VM).
- Siêu nhanh: Khởi động trong vài mili giây (so với hàng phút của VM).
- Nhất quán: Môi trường “It works on my machine” (Chạy trên máy tôi) giờ đây đảm bảo chạy được ở mọi nơi.
Phần 1: Kiến trúc và Các khái niệm Cốt lõi
Để làm chủ Docker, bạn cần hiểu 3 thành phần cơ bản và mối quan hệ của chúng.

1. Image
Image là một **template chỉ đọc (read-only)**, chứa mọi thứ bạn cần để chạy ứng dụng: source code, runtime (Node.js, .NET, Java), thư viện hệ thống, và cấu hình.
- Cấu trúc Layer: Image được tạo thành từ nhiều “lớp” (layers) xếp chồng lên nhau. Khi bạn thay đổi 1 file và build lại, Docker chỉ build lại lớp đó (nhờ cơ chế cache), giúp tốc độ build cực nhanh.
- Bất biến (Immutable): Bạn không thể “sửa” một image. Bạn chỉ có thể tạo ra một image mới dựa trên image cũ.
# Liệt kê các image bạn có
docker image ls
# Tải (kéo) một image từ Docker Hub
docker pull nginx:latest
# Xóa một image
docker image rm nginx:latest
2. Container
Container là một **phiên bản thực thi (runnable instance)** của một Image. Bạn có thể tạo, chạy, dừng, di chuyển, và xóa container.
- Tính khả biến (Ephemeral): Container về bản chất là “tạm bợ”. Khi bạn xóa một container, mọi dữ liệu mới được tạo *bên trong* nó sẽ mất vĩnh viễn (trừ khi bạn dùng Volumes).
- Cách ly: Mỗi container chạy trong một môi trường cách ly (namespace) của riêng nó. Nó không nhìn thấy các tiến trình hay file của máy chủ hoặc của container khác (trừ khi bạn cho phép).
# Chạy container từ image 'ubuntu' và mở terminal
docker run -it --name my-ubuntu ubuntu:latest
# Liệt kê container đang chạy
docker ps
# Liệt kê TẤT CẢ container (kể cả đã dừng)
docker ps -a
# Dừng một container
docker stop my-ubuntu
# Xóa một container (phải dừng trước)
docker rm my-ubuntu
# Xóa "cưỡng bức" một container đang chạy
docker rm -f my-ubuntu
3. Volumes
Vì container có tính “tạm bợ”, chúng ta cần một cơ chế để lưu trữ dữ liệu vĩnh viễn. Đó là lúc Volumes vào cuộc. Có 2 loại chính:
a) Named Volumes
Đây là cách làm “chuẩn” nhất. Bạn tạo ra một “ổ đĩa ảo” do Docker quản lý. Nó được đặt tên và bạn có thể “gắn” nó vào bất kỳ container nào.
- Ưu điểm: Độc lập với máy chủ, dễ dàng backup, di chuyển, và được quản lý hoàn toàn bởi Docker.
# Tạo 1 volume tên 'db-data'
docker volume create db-data
# Chạy container Postgres và gắn 'db-data' vào thư mục data của nó
docker run -d --name my-postgres \
-v db-data:/var/lib/postgresql/data \
postgres:latest
b) Bind Mounts
Đây là cách bạn “mount” (ánh xạ) một thư mục từ máy chủ (host) của bạn trực tiếp vào bên trong container.
- Ưu điểm: Rất tiện cho lập trình. Bạn sửa code trên máy host, container sẽ thấy thay đổi ngay lập tức mà không cần build lại.
# Ánh xạ thư mục code của bạn vào container
docker run -it --name my-app \
-v /home/user/my-project:/app \
node:latest
4. Network
Docker tạo ra các mạng ảo để các container có thể giao tiếp với nhau hoặc với thế giới bên ngoài.
- bridge (Mặc định): Các container trên cùng 1 mạng bridge có thể “gọi” nhau bằng tên container. Đây là loại mạng phổ biến nhất.
- host: Container dùng chung mạng của máy chủ. Nhanh nhưng mất đi tính cách ly.
- overlay: Mạng ảo “phủ” lên nhiều máy chủ, dùng cho Docker Swarm.
# Tạo một mạng bridge tùy chỉnh
docker network create my-app-net
# Chạy 2 container trên cùng mạng đó
docker run -d --name db --network my-app-net postgres
docker run -d --name api --network my-app-net my-api-image
# (Giờ đây, 'api' có thể kết nối tới 'db' qua tên 'db')
Phần 2: Cài đặt và Cấu hình Docker
1. Cài đặt Docker trên Windows
Trên Windows, cách tốt nhất là dùng Docker Desktop. Nó tích hợp với WSL 2 (Windows Subsystem for Linux 2) để chạy các container Linux một cách native.
Yêu cầu hệ thống:
| Yêu cầu | Hệ thống |
|---|---|
| Hệ điều hành | Windows 10 hoặc Windows 11 (64-bit) |
| CPU | Hỗ trợ ảo hóa (VTx / AMD-V, bật trong BIOS) |
| RAM | Tối thiểu 4GB (Khuyến nghị 8GB+) |
| Tính năng | Đã bật WSL 2 |
Bước 1: Cài đặt WSL 2
Mở PowerShell (với quyền Administrator) và gõ:
wsl --install
Bước 2: Tải Docker Desktop tại: https://docker.com/products/docker-desktop
Bước 3: Chạy file .exe và làm theo hướng dẫn. Nó sẽ tự động cấu hình để dùng WSL 2.

2. Cài đặt Docker trên Linux (Ubuntu)
Đây là môi trường “chuẩn” cho production. Chúng ta sẽ cài từ repository chính thức của Docker.
Bước 1-4: Thêm GPG key và repository của Docker
# 1. Cập nhật và cài các gói hỗ trợ
sudo apt update
sudo apt install apt-transport-https ca-certificates curl software-properties-common -y
# 2. Thêm khóa GPG chính thức của Docker
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# 3. Thêm Docker repository
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Bước 5: Cài Docker Engine (và Docker Compose)
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose -y
Bước 6: Kiểm tra trạng thái Docker
sudo systemctl status docker

Bước 7: (Rất quan trọng) Cho phép chạy Docker không cần sudo
Mặc định, bạn phải dùng sudo cho mọi lệnh docker. Rất bất tiện. Lệnh này sẽ thêm user của bạn vào group ‘docker’.
sudo usermod -aG docker ${USER}
# Bạn cần CHẠY LỆNH NÀY hoặc ĐĂNG XUẤT/ĐĂNG NHẬP LẠI để áp dụng
newgrp docker
Phần 3: Chuyên sâu về Dockerfile
Dockerfile là một file văn bản chứa các chỉ thị (instructions) để docker build tự động tạo ra một image. Đây là bản “công thức” cho ứng dụng của bạn.
Các Chỉ Lệnh Dockerfile Phổ Biến
- FROM: Bắt đầu một image mới từ một “image nền” (base image). Mọi Dockerfile phải bắt đầu bằng
FROM.
- WORKDIR: Đặt thư mục làm việc cho các lệnh
RUN,CMD,COPYtheo sau.
- COPY: Sao chép file/thư mục từ máy host vào image.
- RUN: Chạy một lệnh *bên trong image* tại thời điểm build (ví dụ:
RUN npm install).
- EXPOSE: Báo cho Docker biết container sẽ lắng nghe ở cổng nào (chỉ mang tính thông tin, không thực sự mở cổng).
- CMD vs. ENTRYPOINT:
- CMD: Cung cấp lệnh chạy mặc định khi container khởi động. Có thể bị ghi đè.
- ENTRYPOINT: Cấu hình container để chạy như một file thực thi. Khó bị ghi đè hơn, thường dùng để tạo “lệnh chính”.
Best Practice: Multi-Stage Builds
Đây là một kỹ thuật *cực kỳ quan trọng*. Tại sao? Vì image dùng để *build* (ví dụ: golang:1.21, maven:latest, dotnet sdk) rất nặng, chứa nhiều công cụ (compiler, SDK…). Image để *chạy* (runtime) chỉ cần file thực thi và các thư viện cần thiết.
Multi-stage build cho phép bạn dùng một image “nặng” để build, sau đó chỉ COPY file kết quả (file .jar, file binary, file .dll) sang một image nền “siêu nhẹ” (như alpine, aspnet:8.0).
Ví dụ Dockerfile cho các ngôn ngữ phổ biến
Dưới đây là các mẫu Dockerfile chuẩn Production sử dụng Multi-stage build để tối ưu dung lượng.
1. Ví dụ Node.js (Tối ưu dung lượng)
# --- Giai đoạn 1: Build ---
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
# Cài đặt tất cả dependencies (bao gồm devDependencies nếu cần build)
RUN npm ci
COPY . .
# Nếu bạn dùng TypeScript hoặc cần build steps, chạy ở đây
# RUN npm run build
# --- Giai đoạn 2: Runtime ---
FROM node:18-alpine
WORKDIR /app
# Chỉ copy các file cần thiết từ builder
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/src ./src
EXPOSE 3000
CMD ["node", "src/index.js"]
2. Ví dụ Java (Spring Boot)
# --- Giai đoạn 1: Maven Build ---
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
# Build ra file .jar và bỏ qua test để nhanh hơn
RUN mvn clean package -DskipTests
# --- Giai đoạn 2: Run với JDK rút gọn ---
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# Chỉ lấy file .jar đã build xong
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
3. Ví dụ .NET Core (C#)
# --- Giai đoạn 1: Build với SDK (Nặng) ---
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApp/MyApp.csproj", "MyApp/"]
RUN dotnet restore "MyApp/MyApp.csproj"
COPY . .
WORKDIR "/src/MyApp"
# Publish ra file DLL đã tối ưu
RUN dotnet publish -c Release -o /app/publish
# --- Giai đoạn 2: Run với ASP.NET Runtime (Nhẹ) ---
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "MyApp.dll"]
Dockerfile Best Practices Khác
- Sử dụng
.dockerignore: Tương tự.gitignore, nó ngănCOPY . .sao chép các file không cần thiết (nhưnode_modules,.git,bin,obj) vào image, giúp build nhanh hơn.
- Tận dụng Cache: Sắp xếp các chỉ thị từ ít thay đổi (
RUN apt-get install) đến hay thay đổi (COPY . .). Điều này tối ưu hóa cache của Docker.
- Không chạy với quyền
root: Thêm một user không phải root và dùng chỉ thịUSERđể tăng cường bảo mật.
Phần 4: Docker Compose – Điều phối Development
Docker Compose là công cụ để định nghĩa và chạy các ứng dụng Docker *đa-container*. Thay vì gõ 5 lệnh docker run khác nhau cho (API, DB, Cache, Worker…), bạn định nghĩa tất cả chúng trong một file docker-compose.yml.
Đây là công cụ *không thể thiếu* cho môi trường development.
Ví dụ: Spring Boot + Postgres
version: '3.8'
services:
# Dịch vụ 1: Ứng dụng
app:
build: . # Tự build từ Dockerfile trong thư mục hiện tại
ports:
- "8080:8080"
volumes:
- .:/app # Mount code để live-reload
depends_on:
- db # Khởi động 'app' SAU KHI 'db' đã sẵn sàng
environment:
# 'db' là tên service, Docker Compose tự động phân giải DNS
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mydb
- SPRING_DATASOURCE_USERNAME=postgres
- SPRING_DATASOURCE_PASSWORD=123456
# Dịch vụ 2: Cơ sở dữ liệu
db:
image: postgres:15
restart: always
environment:
POSTGRES_DB: mydb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 123456
volumes:
# Dùng 'named volume' để lưu dữ liệu database
- pgdata:/var/lib/postgresql/data
# Khai báo named volume
volumes:
pgdata:
Lệnh sử dụng Docker Compose
# Xây dựng (nếu cần) và khởi chạy tất cả service (chạy nền)
docker-compose up -d
# Dừng và XÓA tất cả container, network, volume (nếu có --volumes)
docker-compose down
# Chỉ build lại image của service 'app'
docker-compose build app
# Xem log của tất cả service
docker-compose logs -f
Phần 5: Docker Swarm – Điều phối Production
Khi bạn muốn chạy ứng dụng trên nhiều máy chủ (nodes) để đảm bảo tính sẵn sàng cao (High Availability) và cân bằng tải (Load Balancing), bạn cần một “bộ điều phối” (Orchestrator). Docker Swarm là giải pháp điều phối “chính chủ” từ Docker, nổi tiếng là đơn giản và dễ dùng.

Kiến trúc Swarm
- Manager Node: Tiếp nhận lệnh (ví dụ: “chạy 5 bản sao của image X”), quản lý trạng thái cluster và phân bổ “Task”.
- Worker Node: Nhận và thực thi “Task” (chính là chạy container) từ Manager.
- Service: Là định nghĩa của ứng dụng bạn muốn chạy (ví dụ: service `my-web` là image `nginx` với `replicas=3`).
- Ingress Routing Mesh: Một tính năng cực hay. Bạn publish cổng 80 cho service, Swarm sẽ tự động mở cổng 80 trên *tất cả các node* (cả manager lẫn worker). Dù request đi vào IP của node nào, Swarm cũng sẽ tự động điều hướng nó đến 1 container đang chạy (kể cả khi container đó nằm trên node khác).
Khởi tạo Swarm
# Trên máy chủ Manager
docker swarm init --advertise-addr <MANAGER-IP>
# Lệnh trên sẽ sinh ra một lệnh 'docker swarm join' với token
# Chạy lệnh đó trên các máy chủ Worker
docker swarm join --token <TOKEN> <MANAGER-IP>:2377
Tạo service trong Swarm
# 1. Tạo mạng overlay để các service trên các node có thể nói chuyện
docker network create -d overlay my-swarm-net
# 2. Tạo service 'my-web' với 3 bản sao, publish cổng 8090
docker service create \
--name my-web \
--replicas 3 \
--network my-swarm-net \
--publish published=8090,target=80 \
nginx:latest
Phần 6: Công cụ Quản lý – Portainer & Harbor
1. Portainer
Portainer là một giao diện Web UI gọn nhẹ giúp bạn quản lý mọi thứ: container, image, volume, network… thay vì phải gõ lệnh. Nó đặc biệt mạnh khi quản lý Docker Swarm.
Cài đặt Portainer (Standalone)
# 1. Tạo volume
docker volume create portainer_data
# 2. Chạy Portainer
docker run -d -p 8000:8000 -p 9443:9443 \
--name portainer \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:lts
Sau đó, truy cập https://<IP-CỦA-BẠN>:9443 để thiết lập mật khẩu admin.

2. Harbor (Private Registry)
Docker Hub là public. Khi bạn làm cho công ty, bạn cần một Private Registry để lưu trữ các image nội bộ. Harbor là một giải pháp mã nguồn mở hàng đầu, cung cấp:
- Lưu trữ image.
- Quét lỗ hổng bảo mật (Trivy, Clair).
- Quản lý truy cập (RBAC).
- Sao chép (Replication) image giữa các trung tâm dữ liệu.
Cài đặt Harbor (Dùng installer)
Harbor tự nó chạy trên Docker Compose. Quá trình cài đặt phức tạp hơn một chút, yêu cầu SSL.
Bước 1-2: Tải và giải nén Harbor Installer
wget https://github.com/goharbor/harbor/releases/download/v2.13.1/harbor-online-installer-v2.13.1.tgz
tar xvf harbor-online-installer-v2.13.1.tgz
cd harbor
cp harbor.yml.tmpl harbor.yml
Bước 3: Tạo chứng chỉ SSL (tự ký)
Bạn phải dùng HTTPS. Để test, bạn có thể tạo 1 cert tự ký (self-signed cert).
# (Chạy các lệnh openssl để tạo ca.crt, harbor.crt, harbor.key...)
# Đặt chúng vào /opt/harbor/cert/
sudo mkdir -p /opt/harbor/cert
cd /opt/harbor/cert
sudo openssl genrsa -out ca.key 4096
sudo openssl req -x509 -new -nodes -sha256 -days 3650 \
-key ca.key \
-out ca.crt \
-subj "/C=VN/ST=HN/L=Hanoi/O=MyCA/OU=IT/CN=My-Local-CA"
sudo openssl genrsa -out harbor.key 4096
sudo openssl req -new -sha256 \
-key harbor.key \
-out harbor.csr \
-subj "/C=VN/ST=HN/L=Hanoi/O=MyOrg/OU=DevOps/CN=harbor.diendo.pro.vn"
sudo tee harbor.ext > /dev/null <<EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
DNS.1=harbor.diendo.pro.vn
DNS.2=harbor
EOF
sudo openssl x509 -req -sha256 -days 3650 \
-in harbor.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out harbor.crt \ -extfile harbor.ext
Bước 4: Cấu hình harbor.yml
Đây là các trường quan trọng nhất cần sửa:
# Đổi thành IP hoặc FQDN (Fully Qualified Domain Name) của bạn
hostname: 10.100.1.33
...
https:
port: 443
certificate: /opt/harbor/cert/harbor.crt
private_key: /opt/harbor/cert/harbor.key
...
data_volume: /opt/harbor/data
...
Bước 5: Chạy cài đặt
sudo ./prepare
sudo ./install.sh
Sau khi hoàn tất, bạn có thể truy cập https://10.100.1.33 và đăng nhập (mật khẩu admin mặc định nằm trong file harbor.yml).

Kết luận
Docker là một công cụ thay đổi cuộc chơi. Bằng cách nắm vững các khái niệm từ Image, Container, Volume, đến việc điều phối với Compose và Swarm, bạn đã có trong tay bộ kỹ năng nền tảng của một Kỹ sư DevOps hiện đại. Chúc bạn thành công trên hành trình chinh phục container!