Hướng dẫn · 19/09/2026

Triển khai ứng dụng Docker với Caddy và HTTPS tự động

Một ứng dụng chạy tốt trong container chưa đồng nghĩa đã sẵn sàng phục vụ Internet. Bạn còn cần domain, TLS, chuyển hướng HTTP sang HTTPS, lưu chứng thư bền vững và một reverse proxy đứng trước ứng dụng.

Triển khai ứng dụng Docker với Caddy và HTTPS tự động

Một ứng dụng chạy tốt trong container chưa đồng nghĩa đã sẵn sàng phục vụ Internet. Bạn còn cần domain, TLS, chuyển hướng HTTP sang HTTPS, lưu chứng thư bền vững và một reverse proxy đứng trước ứng dụng.

Trong hướng dẫn này, Caddy nhận traffic công khai trên cổng 80/443, tự quản lý chứng thư HTTPS cho domain và chuyển request tới ứng dụng qua mạng nội bộ Docker Compose. Kết quả là chỉ Caddy lộ ra Internet; cổng ứng dụng không cần publish trực tiếp lên máy chủ.

Mô hình triển khai

Internet
   |
   |  HTTP :80 / HTTPS :443
   v
Caddy container
   |
   |  Docker network: http://app:8080
   v
Application container

Caddy hỗ trợ Automatic HTTPS khi site address là một hostname hợp lệ. Với domain công khai, DNS phải trỏ đúng máy chủ và cổng 80/443 phải tới được Caddy để quá trình cấp chứng thư và phục vụ traffic hoạt động.

1. Điều kiện chuẩn bị

  • Một máy chủ Linux có Docker Engine và Docker Compose plugin.
  • Một domain hoặc subdomain bạn quản lý, ví dụ app.example.com.
  • Quyền chỉnh DNS và firewall/security group.
  • Ứng dụng có image container và lắng nghe HTTP trên 0.0.0.0:8080.

Kiểm tra Docker:

docker version
docker compose version

Ví dụ dùng Linux shell. Trên Windows hoặc macOS, cú pháp đường dẫn/quyền file có thể khác, nhưng nguyên tắc network và volume của Compose vẫn giống nhau.

2. Trỏ DNS về máy chủ

Tạo record A cho IPv4 và AAAA chỉ khi máy chủ thực sự phục vụ IPv6:

app.example.com  A     203.0.113.10
app.example.com  AAAA  2001:db8::10

Các địa chỉ trên chỉ là dải ví dụ, phải thay bằng IP thật. Sau khi DNS cập nhật, xác minh từ một mạng bên ngoài:

dig +short app.example.com A
dig +short app.example.com AAAA

Một record AAAA trỏ sai thường gây hiện tượng máy dùng IPv6 không truy cập được dù IPv4 hoạt động. Nếu chưa cấu hình IPv6 end-to-end, đừng tạo AAAA chỉ để “đủ bộ”.

3. Mở cổng mạng cần thiết

Cho phép inbound TCP 80 và TCP 443. Nếu muốn HTTP/3, cho phép thêm UDP 443. Giữ SSH giới hạn theo IP quản trị khi có thể.

# Ví dụ với UFW; kiểm tra chính sách hiện tại trước khi áp dụng
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status

Cloud firewall/security group và firewall trong hệ điều hành là hai lớp riêng. Router/NAT cũng phải forward đúng nếu máy chủ nằm sau mạng riêng. Không tắt firewall toàn bộ để xử lý nhanh lỗi cấp chứng thư.

4. Tạo cấu trúc dự án

mkdir -p caddy-docker/caddy
cd caddy-docker
touch compose.yaml caddy/Caddyfile .env

Đặt domain vào .env:

APP_DOMAIN=app.example.com

.env trong ví dụ không cần chứa bí mật, nhưng thói quen tốt là không commit file môi trường production. Nếu ứng dụng cần secret, dùng secret manager hoặc cơ chế secret phù hợp thay vì nhúng vào image hay Caddyfile.

5. Viết compose.yaml

services:
  app:
    image: registry.example.com/my-app:<approved-version>
    restart: unless-stopped
    expose:
      - "8080"
    networks:
      - web

  caddy:
    image: caddy:<approved-version>-alpine
    restart: unless-stopped
    depends_on:
      - app
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    environment:
      APP_DOMAIN: ${APP_DOMAIN:?APP_DOMAIN is required}
    volumes:
      - ./caddy:/etc/caddy:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - web

networks:
  web:

volumes:
  caddy_data:
  caddy_config:

Thay <approved-version> bằng version hoặc digest đã được kiểm thử; không để tag latest trôi tự do trên production. Image ứng dụng phải lắng nghe cổng 8080 bên trong container.

expose mô tả cổng nội bộ nhưng không publish nó lên host. Hai service cùng network web có thể tìm nhau bằng service name, nên Caddy gọi app:8080, không gọi IP container và không dùng localhost:8080.

Volume caddy_data đặc biệt quan trọng vì chứa chứng thư, private key và state cần thiết. Không coi nó là cache và không xóa khi chưa hiểu tác động.

6. Viết Caddyfile

{$APP_DOMAIN} {
    encode zstd gzip

    reverse_proxy app:8080

    log {
        output stdout
        format console
    }
}

Chỉ cần hostname trong site block, Caddy sẽ kích hoạt Automatic HTTPS, lấy/gia hạn chứng thư và thiết lập chuyển hướng HTTP sang HTTPS. reverse_proxy giữ method và URI trừ khi có rewrite.

Không thêm tls_insecure_skip_verify để “sửa nhanh” upstream HTTPS. Trong mô hình một Docker network riêng, HTTP từ Caddy tới app thường đủ. Nếu traffic upstream đi qua mạng không tin cậy, cấu hình TLS/mTLS đúng cách và trust CA cụ thể.

7. Kiểm tra cấu hình trước khi chạy

Compose có thể render cấu hình cuối cùng và phát hiện biến thiếu:

docker compose config

Kiểm tra Caddyfile bằng chính image đã chọn:

docker compose run --rm --no-deps caddy \
  caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile

Nếu dùng ACME staging trong giai đoạn thử nghiệm, nhớ bỏ endpoint staging trước khi phục vụ thật; chứng thư staging không được trình duyệt tin cậy. Tránh lặp đi lặp lại thao tác xin chứng thư production khi DNS/firewall còn sai vì CA có rate limit.

8. Khởi động và theo dõi log

docker compose up -d
docker compose ps
docker compose logs --tail=100 caddy
docker compose logs --tail=100 app

Trong log Caddy, tìm thông tin lấy chứng thư hoặc lỗi challenge. Trong log app, xác nhận service đã lắng nghe ở 0.0.0.0:8080, không chỉ 127.0.0.1 bên trong container.

depends_on chỉ sắp xếp thứ tự khởi động cơ bản, không chứng minh ứng dụng đã sẵn sàng. Ở hệ thống quan trọng, thêm healthcheck cho app và thiết kế app khởi động/reconnect an toàn. Khi upstream tạm chưa sẵn sàng, Caddy có thể trả 502 rồi hoạt động lại khi app lên.

9. Xác minh từ bên ngoài

curl -I http://app.example.com
curl -I https://app.example.com
curl -v https://app.example.com/health

Kỳ vọng request HTTP được redirect sang HTTPS, chứng thư khớp hostname và endpoint ứng dụng trả đúng status. Kiểm tra thêm bằng trình duyệt trên một thiết bị/mạng khác để tránh kết quả bị ảnh hưởng bởi DNS cache hoặc file hosts cục bộ.

Không dùng curl -k trong bước xác minh production; tùy chọn đó bỏ qua lỗi chứng thư mà bạn đang cần phát hiện.

10. Reload cấu hình không dừng container

Sau khi sửa Caddyfile, validate rồi graceful reload:

docker compose exec -w /etc/caddy caddy \
  caddy validate --config Caddyfile --adapter caddyfile

docker compose exec -w /etc/caddy caddy caddy reload

Mount cả thư mục ./caddy vào /etc/caddy giúp tránh một số editor thay inode khiến bind mount một file đơn không thấy nội dung mới. Reload tốt hơn restart vì Caddy có thể áp dụng config mà không chủ động làm gián đoạn kết nối đang phục vụ.

11. Cập nhật image có kiểm soát

  1. Đọc release notes và chọn version/digest mới trong môi trường test.
  2. Pull image, validate config và chạy smoke test.
  3. Cập nhật production theo cửa sổ thay đổi.
  4. Xác minh HTTPS, health endpoint và log sau cập nhật.
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --since=10m caddy

docker compose down không xóa named volume mặc định, nhưng down -v sẽ xóa volume. Không dùng -v trên production nếu chưa có chủ đích và backup.

12. Sao lưu những gì?

  • compose.yaml, Caddyfile và tài liệu DNS/firewall.
  • Dữ liệu ứng dụng/database theo quy trình backup riêng.
  • Volume caddy_data nếu muốn giữ state/certificates khi di chuyển máy.

Private key nằm trong dữ liệu Caddy, vì vậy backup phải được mã hóa và giới hạn quyền. Nếu mất volume, Caddy thường có thể xin chứng thư lại khi DNS/cổng đúng, nhưng việc xin lại hàng loạt có thể gặp rate limit và không thay thế chiến lược backup.

13. Xử lý lỗi thường gặp

Không lấy được chứng thư

  • DNS chưa trỏ đúng IP hoặc còn record AAAA sai.
  • Cổng 80/443 bị chặn bởi cloud firewall, UFW hoặc router.
  • Một service khác đang chiếm cổng.
  • Proxy/CDN phía trước cấu hình chế độ TLS/DNS chưa phù hợp.
docker compose logs caddy
sudo ss -lntup | grep -E ':(80|443)\b'

HTTPS hoạt động nhưng trả 502

  • App chưa sẵn sàng hoặc crash loop.
  • App chỉ bind 127.0.0.1 trong container.
  • Sai service name/cổng trong reverse_proxy.
  • Caddy và app không cùng Docker network.
docker compose ps
docker compose logs app
docker compose exec caddy wget -qO- http://app:8080/health

Image Caddy tối giản có thể không chứa mọi công cụ debug. Nếu lệnh kiểm tra không tồn tại, dùng một container chẩn đoán tạm thời gắn cùng network thay vì cài công cụ vào container production đang chạy.

Domain đúng nhưng app redirect loop

Ứng dụng có thể chưa tin proxy headers hoặc tự ép HTTPS sai cách. Caddy gửi các header forward phổ biến; cấu hình framework chỉ tin proxy thực tế, không tin mọi địa chỉ Internet. Tránh chạy đồng thời nhiều lớp redirect mâu thuẫn.

14. Checklist hardening

  • Chỉ publish 80/443 của Caddy; database, cache và app ở network nội bộ.
  • Pin image version/digest và có lịch cập nhật.
  • Không lưu secret trong image, Git hoặc Caddyfile.
  • Giữ caddy_data bền vững, backup được bảo vệ.
  • Giới hạn SSH và theo dõi log, disk, certificate/endpoint health.
  • Đặt upload/body limit và timeout phù hợp tại app hoặc proxy khi cần.
  • Chỉ thêm security header sau khi hiểu ứng dụng; HSTS sai có thể gây tác động dài hạn.
  • Không tắt TLS verification để che lỗi trust của upstream.

Kết luận

Caddy giúp rút ngắn phần TLS của một triển khai Docker, nhưng vận hành an toàn vẫn phụ thuộc vào DNS đúng, cổng mạng rõ ràng, volume bền vững, service discovery bằng tên và quy trình validate/reload. Với cấu trúc trên, ứng dụng được cô lập sau reverse proxy, HTTPS được tự động quản lý và cấu hình đủ đơn giản để kiểm tra, backup và nâng cấp có kiểm soát.

Tài liệu tham khảo

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.