Vấn đề thực tế trước khi có Docker
Mình còn nhớ những ngày đầu làm việc trong team, build xong app trên máy local rồi push lên server — kết quả: crash ngay lập tức. Lý do? Server chạy Python 3.8, máy mình Python 3.11. Thư viện version khác, biến môi trường thiếu, cấu hình lệch… Mỗi lần deploy là một lần “cầu may”.
Docker sinh ra để giải quyết đúng vấn đề này: đóng gói toàn bộ ứng dụng — code, dependencies, cấu hình — vào một đơn vị chạy được ở bất kỳ đâu. Không còn “chạy được trên máy mình mà” nữa.
Docker không phải Virtual Machine (VM). VM tạo một máy tính ảo hoàn chỉnh với OS riêng, nặng hàng GB. Container dùng chung kernel của host OS, chỉ cô lập ở tầng process — khởi động trong vài giây, tiêu thụ RAM tính bằng MB thay vì GB.
Cài đặt Docker trên Ubuntu/Linux
Phần lớn VPS hiện nay chạy Ubuntu, nên mình hướng dẫn trên Ubuntu 22.04/24.04. Quy trình này cũng tương tự trên Debian.
Bước 1: Gỡ version cũ nếu có
sudo apt remove docker docker-engine docker.io containerd runc
Bước 2: Thêm Docker repository chính thức
sudo apt update
sudo apt install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Bước 3: Cài đặt Docker Engine
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Bước 4: Cho phép chạy Docker không cần sudo
sudo usermod -aG docker $USER
newgrp docker
Logout rồi login lại để group thay đổi có hiệu lực. Kiểm tra nhanh:
docker --version
# Docker version 26.x.x, build xxxxxxx
docker run hello-world
Nếu thấy thông báo “Hello from Docker!” là cài đặt thành công.
Các khái niệm cốt lõi cần nắm chắc
Trước khi gõ lệnh đầu tiên, dành 5 phút hiểu đúng 4 khái niệm này — tiết kiệm hơn nhiều so với mò lỗi sau. Mình thấy nhiều người nhầm Image với Container rồi xóa nhầm, restart sai chỗ, mất cả buổi debug mà không biết vấn đề nằm ở đâu.
Image: Bản thiết kế bất biến
Image là file read-only chứa mọi thứ để chạy ứng dụng: OS base, thư viện, code, biến môi trường. Hãy nghĩ như file ISO để cài Windows — bạn không chỉnh sửa ISO, bạn chỉ dùng nó để tạo ra phiên bản chạy.
# Tải image nginx từ Docker Hub
docker pull nginx:1.25
# Xem danh sách image đang có
docker images
# Xóa image
docker rmi nginx:1.25
Container: Instance đang chạy từ Image
Container là instance cụ thể khi bạn “chạy” một Image. Tương tự file ISO có thể cài lên 10 máy ảo khác nhau — một Image tạo được bao nhiêu Container cũng được, mỗi cái hoàn toàn cô lập với nhau và với host OS.
# Chạy container nginx, map port 8080 host → 80 container
docker run -d -p 8080:80 --name my-nginx nginx:1.25
# Xem container đang chạy
docker ps
# Xem tất cả container (cả đã stop)
docker ps -a
# Dừng và xóa container
docker stop my-nginx
docker rm my-nginx
Volume: Lưu dữ liệu bền vững
Container bản chất là ephemeral — khi xóa container, mọi dữ liệu bên trong mất theo. Volume là giải pháp: mount thư mục từ host vào bên trong container, dữ liệu tồn tại độc lập với vòng đời container.
# Mount thư mục /data/mysql từ host vào /var/lib/mysql trong container
docker run -d \
-v /data/mysql:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
--name mysql-db \
mysql:8.0
Dữ liệu giờ nằm ở /data/mysql trên host — xóa container đi rồi tạo lại vẫn giữ nguyên data.
Network: Container giao tiếp với nhau
Mặc định mỗi container có IP riêng trong mạng bridge, nhưng IP này thay đổi mỗi lần restart. Kết nối qua hostname ổn định hơn nhiều. Tạo một custom network để các container trong cùng stack tìm thấy nhau bằng tên:
# Tạo network
docker network create my-app-net
# Chạy container trong network đó
docker run -d --network my-app-net --name db mysql:8.0
docker run -d --network my-app-net --name web my-webapp
# Container "web" kết nối đến "db" qua hostname "db" — không cần biết IP
Viết Dockerfile đầu tiên
Dockerfile là file text hướng dẫn Docker tạo Image từ đầu — mỗi instruction trong file tương ứng một layer. Ví dụ thực tế cho app Python Flask:
FROM python:3.12-slim
WORKDIR /app
# Copy requirements trước — tận dụng Docker layer cache
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
Build và chạy thử:
# Build image từ Dockerfile trong thư mục hiện tại
docker build -t my-flask-app:v1 .
# Chạy thử
docker run -d -p 5000:5000 my-flask-app:v1
Mẹo nhỏ: luôn COPY requirements.txt và RUN pip install trước khi COPY code. Docker cache layer theo thứ tự — nếu code thay đổi nhưng requirements không đổi, bước cài thư viện sẽ dùng cache, build nhanh hơn nhiều.
Kiểm tra và Monitoring Container
Deploy xong là lúc công việc thật sự bắt đầu. Container có thể crash lặng lẽ, rò rỉ RAM, hoặc phát sinh lỗi mà bạn không hay — nên hãy nắm vài lệnh cơ bản này ngay từ đầu.
Xem logs container
# Xem log của container
docker logs my-nginx
# Follow log realtime (như tail -f)
docker logs -f my-nginx
# Chỉ xem 50 dòng cuối
docker logs --tail 50 my-nginx
Theo dõi resource usage
# Xem CPU/RAM đang dùng realtime
docker stats
# Snapshot không follow
docker stats --no-stream
Trên production cluster chạy 30+ container, mình đã dùng docker stats để phát hiện container nào đang “ngốn” tài nguyên bất thường. Sau khi phân tích và tối ưu lại Dockerfile — áp dụng multi-stage build và giảm số layer không cần thiết — mình giảm được 40% resource usage mà không cần thêm server. Tool monitoring như Prometheus hay Grafana thì hay, nhưng docker stats đã có sẵn ngay sau khi cài Docker, không cần cài thêm gì.
Vào bên trong container để debug
# Mở shell bash trong container đang chạy
docker exec -it my-nginx bash
# Chạy lệnh đơn trong container
docker exec my-nginx nginx -t
# Xem thông tin chi tiết của container
docker inspect my-nginx
Dọn dẹp tài nguyên định kỳ
Sau một thời gian, image cũ, container stopped, volume không dùng tích lũy và chiếm disk. Một lệnh dọn dẹp nhanh:
# Xóa resource không dùng (container stopped, image dangling, network unused)
docker system prune
# Thêm -a để xóa cả image không có container nào dùng
docker system prune -a
# Xem dung lượng Docker đang chiếm
docker system df
Bước tiếp theo sau khi nắm vững cơ bản
Nắm được các lệnh trên rồi? Thứ bạn sẽ dùng hàng ngày tiếp theo là Docker Compose. Thay vì gõ 5–6 lệnh docker run với đủ loại flags, bạn định nghĩa toàn bộ stack trong một file YAML rồi chỉ cần docker compose up -d:
services:
web:
build: .
ports:
- "5000:5000"
depends_on:
- db
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
volumes:
pgdata:
docker compose up -d
docker compose logs -f
docker compose down
Khi Docker Compose trở nên quen thuộc, con đường đến Docker Swarm, Kubernetes hay tự động hóa CI/CD pipeline ngắn hơn bạn nghĩ — vì tất cả đều dùng lại đúng 4 khái niệm bạn vừa học: Image, Container, Volume, Network.
