Docker giúp bạn chạy ứng dụng trong container với đầy đủ thư viện đi kèm, không ảnh hưởng hệ thống chính. Docker Compose cho phép mô tả nhiều container trong một file và khởi chạy bằng một lệnh. Bài này hướng dẫn cài Docker từ kho chính thức trên Ubuntu 24.04, chạy một ứng dụng mẫu bằng Compose, rồi đặt nó sau Nginx reverse proxy có HTTPS. Bạn cũng sẽ biết cách tránh lỗi bảo mật phổ biến: cổng Docker bỏ qua UFW.

Bạn cần chuẩn bị:

  • VPS Ubuntu 24.04 (hoặc 22.04) có user sudo, tối thiểu 1 GB RAM.
  • Nginx đã cài trên máy chủ (không chạy trong Docker).
  • Tên miền phụ app.example.com trỏ bản ghi A về 203.0.113.10.
  • Certbot để cấp SSL.

Bước 1: Gỡ các gói Docker cũ hoặc không chính thức

Các gói như docker.io, podman-docker có thể xung đột với gói chính thức. Lệnh dưới đây gỡ chúng nếu có, báo lỗi "not installed" thì bỏ qua:

for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove -y $pkg; done

Bước 2: Thêm kho apt chính thức của Docker

sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update

Biến UBUNTU_CODENAME trên Ubuntu 24.04 là noble. Khóa GPG giúp apt xác minh gói tải về đúng do Docker phát hành.

Bước 3: Cài Docker Engine và plugin Compose

sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker run --rm hello-world
docker compose version

Nếu thấy dòng "Hello from Docker!", Docker đã chạy đúng. Để chạy lệnh docker không cần sudo:

sudo usermod -aG docker $USER
newgrp docker

Thành viên nhóm docker có quyền tương đương root. Chỉ thêm user bạn tin cậy.

Bước 4: Giới hạn dung lượng log container

Mặc định Docker ghi log container không giới hạn, lâu ngày có thể làm đầy ổ đĩa. Tạo file cấu hình daemon:

sudo nano /etc/docker/daemon.json
{
  "log-driver": "local",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
sudo systemctl restart docker

Cấu hình này chỉ áp dụng cho container tạo mới sau khi restart.

Bước 5: Viết file compose.yaml cho ứng dụng

Ví dụ dưới dùng image traefik/whoami, một web server nhỏ trả về thông tin yêu cầu. Bạn thay bằng image ứng dụng của mình.

mkdir -p ~/apps/whoami && cd ~/apps/whoami
nano compose.yaml
services:
  web:
    image: traefik/whoami:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    environment:
      - TZ=Asia/Ho_Chi_Minh

Điểm quan trọng nhất là 127.0.0.1:8080:80. Container chỉ nhận kết nối từ chính máy chủ, Internet không gọi thẳng được cổng 8080. Nếu viết "8080:80", Docker sẽ mở cổng ra mọi địa chỉ và bỏ qua quy tắc UFW.

Trường version: ở đầu file đã lỗi thời với Compose v2, bạn không cần khai báo.

Với ứng dụng có database, khai báo thêm service và volume để dữ liệu không mất khi tạo lại container:

services:
  app:
    image: ghcr.io/vi-du/ung-dung:1.0
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:3000"
    env_file: .env
    depends_on:
      - db
  db:
    image: mariadb:11
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: app
      MARIADB_USER: app
      MARIADB_PASSWORD: ${DB_PASSWORD}
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql

volumes:
  db_data:

Service db không có ports, nên chỉ các container trong cùng project gọi được qua tên db. Mật khẩu đặt trong file .env cùng thư mục, nhớ chmod 600 .env.

Bước 6: Khởi chạy và quản lý

docker compose up -d
docker compose ps
docker compose logs -f --tail=50
curl -s http://127.0.0.1:8080

Các lệnh hay dùng:

LệnhTác dụng
docker compose pull && docker compose up -dCập nhật image và tạo lại container
docker compose restartKhởi động lại
docker compose downDừng và xóa container (volume vẫn giữ)
docker compose exec web shMở shell trong container
docker system dfXem dung lượng Docker đang dùng

Bước 7: Cấu hình Nginx reverse proxy

sudo nano /etc/nginx/sites-available/app.example.com
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;

    client_max_body_size 50M;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 120s;
    }
}

Khối map và hai header Upgrade, Connection cần cho ứng dụng dùng WebSocket. Khối map phải nằm ngoài server, và chỉ khai báo một lần trong toàn bộ cấu hình Nginx. Nếu đã có ở file khác, hãy bỏ nó ở đây.

sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Bước 8: Bật HTTPS

sudo certbot --nginx -d app.example.com
sudo certbot renew --dry-run

Certbot tự thêm cấu hình SSL và chuyển hướng HTTP sang HTTPS. Một số ứng dụng cần biết URL công khai, ví dụ biến môi trường APP_URL=https://app.example.com. Hãy khai báo trong .env rồi chạy lại docker compose up -d.

Kiểm tra lại

  • curl -I https://app.example.com trả về 200 với chứng chỉ hợp lệ.
  • sudo ss -tlnp | grep 8080 hiển thị 127.0.0.1:8080, không phải 0.0.0.0:8080.
  • Từ máy khác, curl -m 5 http://203.0.113.10:8080 phải thất bại.
  • Sau sudo reboot, docker compose ps cho thấy container tự chạy lại nhờ restart: unless-stopped.

Lỗi thường gặp và cách xử lý

permission denied while trying to connect to the Docker daemon socket

User chưa thuộc nhóm docker hoặc chưa đăng nhập lại sau khi thêm. Chạy groups để kiểm tra, đăng xuất rồi vào lại SSH.

502 Bad Gateway từ Nginx

Container chưa chạy hoặc sai cổng. Kiểm tra docker compose ps, docker compose logs và thử curl http://127.0.0.1:8080 trên máy chủ. Cổng bên phải trong ports phải là cổng ứng dụng lắng nghe bên trong container.

Bind for 127.0.0.1:8080 failed: port is already allocated

Cổng đã có tiến trình khác dùng. Tìm bằng sudo ss -tlnp | grep 8080 và đổi sang cổng khác như 127.0.0.1:8081:80, nhớ sửa cả proxy_pass.

Ổ đĩa đầy dần dù ứng dụng nhỏ

Image cũ và build cache tích tụ sau nhiều lần cập nhật. Xem bằng docker system df, dọn bằng docker image prune -a. Cẩn thận với docker system prune --volumes vì lệnh này xóa cả volume không dùng, có thể chứa dữ liệu.