SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-22

Traefik Docker Compose: 5 app, một IP, một cổng 443

Chạy 5 app sau một IP với Traefik v3 và Docker Compose: route bằng Host rule, tự cấp TLS Let's Encrypt, tránh lỗi acme.json khiến Traefik không khởi động.

Một IP, năm app, một cổng 443

VPS của bạn có một địa chỉ IPv4 public và một cổng TCP 443. Bạn muốn chạy Gitea, một bản staging của app, một dashboard nội bộ, một trang trạng thái và một webhook receiver trên đó: năm hostname, một máy. Reverse proxy là process sở hữu :80:443, đọc header Host trong mỗi request rồi chuyển request đến đúng container. Traefik làm việc đó, đồng thời tự lấy và gia hạn certificate cho từng hostname mà bạn không cần tự chạy certbot. Nginx và Caddy cũng có thể TLS termination cho cả năm hostname này, nên nếu bạn vẫn chưa chọn proxy thì nên so sánh ba proxy về cách xử lý certificate và chi phí cấu hình cho từng app trước khi kết nối mọi thứ vào một proxy duy nhất.

Điểm khác giữa Traefik và một block server {} của nginx là nguồn cấu hình. Với nginx, bạn sửa file rồi reload; vòng đời certificate vẫn là một công việc riêng. Đây là workflow bạn dùng khi cấp certificate Let's Encrypt bằng certbot trên nginx, trong đó timer gia hạn nằm hoàn toàn bên ngoài web server. Docker provider của Traefik theo dõi Docker event stream và đọc label từ các container: khởi động một container có label chứa rule Host() thì container đó có thể được route trong vòng một giây; dừng container thì route biến mất. Đây cũng là điểm dễ gây lỗi. Cấu hình nằm trong label sẽ bị phân tán ở năm nơi cùng lúc. Một label sai sẽ không báo lỗi; container đơn giản là không được route và Traefik không nói gì.

Bốn thuật ngữ

  • Entrypoint là socket đang listen. Bạn sẽ định nghĩa hai entrypoint: web trên :80websecure trên :443.
  • Router so khớp request (Host(...)) và gắn request đó với một service. Traefik yêu cầu certificate cho từng router thông qua tls.certresolver.
  • Service là backend, gồm một container và cổng mà container đó listen bên trong Docker network.
  • Middleware nằm giữa router và service: basic auth, danh sách IP được phép, rewrite header và redirect.

Bốn thuật ngữ này là tên Traefik dùng cho những cấu hình mà trước đây bạn phải viết thủ công: router là một server_name, service là target proxy_pass, còn middleware là các chỉ thị về header và auth mà bạn tự đặt khi từng bước tạo server block reverse proxy trên nginx.

Cấu hình tĩnh (entrypoint, provider, ACME) được truyền cho Traefik qua command line hoặc trong traefik.yml. Khi thay đổi cấu hình này, bạn phải restart Traefik. Cấu hình động (router, service, middleware) được lấy từ container label và hot-reload. Nhầm lẫn hai loại cấu hình này là nguyên nhân thường gặp của lỗi “flag của tôi không có tác dụng”.

Tệp compose

Một Docker network dùng chung có tên proxy là nền tảng của cấu hình này. Traefik chỉ truy cập được một container khi cả hai cùng nằm trên network đó.

name: edge

networks:
  proxy:
    name: proxy

services:
  traefik:
    image: traefik:v3.5
    restart: unless-stopped
    command:
      - --providers.docker=true
      - --providers.docker.exposedByDefault=false
      - --providers.docker.network=proxy
      - --entryPoints.web.address=:80
      - --entryPoints.websecure.address=:443
      - --entryPoints.web.http.redirections.entryPoint.to=websecure
      - --entryPoints.web.http.redirections.entryPoint.scheme=https
      - --certificatesresolvers.le.acme.email=you@example.com
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true
      # while you iterate, point at staging so a mistake costs nothing:
      # - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
      - --api.dashboard=true
      - --log.level=INFO
      - --accesslog=true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
      - traefik.http.routers.dashboard.entrypoints=websecure
      - traefik.http.routers.dashboard.tls.certresolver=le
      - traefik.http.routers.dashboard.service=api@internal
      - traefik.http.routers.dashboard.middlewares=dashboard-auth
      - traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS

  gitea:
    image: gitea/gitea:1  # major-only pin keeps this demo copy-pasteable; pin an exact release in production
    restart: unless-stopped
    volumes:
      - ./gitea:/data
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.gitea.rule=Host(`git.example.com`)
      - traefik.http.routers.gitea.entrypoints=websecure
      - traefik.http.routers.gitea.tls.certresolver=le
      - traefik.http.services.gitea.loadbalancer.server.port=3000

docker compose up -d, sau đó là docker compose logs -f traefik. Mỗi app bổ sung là một bản sao của block gitea, với tên router riêng, Host() riêng và port nội bộ riêng. Một bản cài Nextcloud chạy trong Docker với TLS và backup cũng được thêm theo cách này: xóa các port được publish, nối nó vào proxy và để các router label xử lý hostname cùng certificate.

Năm chi tiết dưới đây rất quan trọng.

exposedByDefault=false khiến container không hiển thị với Traefik cho đến khi container có traefik.enable=true. Nếu bỏ mục này, mọi container bạn khởi động sau đó, kể cả postgres tạm thời dùng để kiểm tra một việc gì đó, đều sẽ được tạo route.

providers.docker.network=proxy cho Traefik biết phải dùng network nào khi container được nối vào nhiều network. Nếu bỏ mục này, Traefik có thể chọn sai IP của container. Lỗi này hiển thị thành 502 và dễ bị nhầm là lỗi của ứng dụng.

loadbalancer.server.port=3000 là port bên trong container; Gitea lắng nghe tại port 3000 ở đó. Lưu ý rằng không container app nào publish port; chỉ Traefik làm việc đó.

Redirect trên entrypoint web chuyển các request dạng plaintext thành mã 308 đến HTTPS. Port 80 vẫn phải mở: ACME HTTP challenge cần port này, và người dùng gõ hostname không kèm scheme cũng cần nó.

Hai ký tự $$ trong hash basic-auth là cách Compose escape, không phải lỗi đánh máy. Tạo hash bằng htpasswd -nbB admin 'your-password' (package apache2-utils), sau đó nhân đôi mọi $.

Chứng chỉ và bẫy acme.json

tlschallenge=true chọn TLS-ALPN-01: Let's Encrypt kết nối đến VPS của bạn qua 443, còn Traefik trả lời challenge trong quá trình TLS handshake. Lựa chọn còn lại là HTTP-01, trên port 80. Hãy thay dòng tlschallenge trong danh sách command: của Traefik bằng 2 dòng sau:

      - --certificatesresolvers.le.acme.httpchallenge=true
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web

Cả 2 cách đều hoạt động. Cả 2 đều yêu cầu DNS public của hostname đã trỏ đến VPS của bạn, để certificate authority phân giải được tên và kết nối từ bên ngoài. Trước tiên hãy tạo bản ghi A (và AAAA), xác nhận bằng dig +short git.example.com, rồi khởi động Traefik.

Tiếp theo là cái bẫy khiến nhiều người mất cả buổi tối. Traefik lưu ACME account key và mọi chứng chỉ đã cấp trong một acme.json. Nếu file này có quyền đọc cho group hoặc mọi user, Traefik sẽ in ra một dòng gần giống như sau rồi dừng:

error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600

Cách sửa đúng là cách đã nêu ở trên: bind-mount directory và để Traefik tự tạo file với mode phù hợp. Nếu bạn đã tạo acme.json bằng touch, umask đã khiến file có quyền 644. Hãy sửa trên host:

chmod 600 ./letsencrypt/acme.json
docker compose restart traefik

Hãy backup directory đó cùng với các app volume. Nếu mất directory này thì vẫn có thể khôi phục vì chứng chỉ sẽ được cấp lại, nhưng việc cấp lại cho 5 hostname cùng lúc có thể khiến bạn chạm rate limit.

Hãy dùng staging CA trong quá trình thử cấu hình. Bỏ comment dòng caserver, làm cho mọi route hoạt động, sau đó comment lại dòng này và xóa acme.json để yêu cầu cấp mới chứng chỉ production. Let's Encrypt production cho phép 5 chứng chỉ trùng lặp mỗi tuần với cùng một tập hostname, đồng thời giới hạn các lần validation thất bại liên tiếp cho cùng một tên. Staging cấp chứng chỉ không được tin cậy, nên trình duyệt sẽ cảnh báo. Cảnh báo đó cho biết cấu hình đã hoạt động và staging có rate limit rộng hơn nhiều.

Dashboard là điểm điều khiển, không phải bản demo

Hầu hết quickstart đều đặt --api.insecure=true, khiến dashboard được phục vụ trên port 8080 mà không có authentication. Trên máy chủ có public IP, cấu hình này để lộ topology định tuyến, hostname, tên middleware và port backend cho bất kỳ ai quét mạng.

Các label trên service traefik ở trên là phương án thay thế: định tuyến dashboard như mọi app khác, qua hostname thực, dùng TLS và đứng sau basicauth. service=api@internal kết nối router với API tích hợp sẵn của Traefik. Có thể tăng mức bảo vệ bằng cách nối thêm IP allow-list theo thứ tự từ trái sang phải. Nếu địa chỉ văn phòng của bạn là dynamic, hãy đặt range thành subnet được cấp bởi một WireGuard VPN tự host trên cùng VPS và chỉ truy cập dashboard qua tunnel:

- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-auth

Một password basicauth dùng chung sẽ không còn phù hợp khi 5 app cần các account riêng. Cùng vị trí middleware đó có thể nhận forwardauth để chuyển việc quyết định cho Authentik, một single sign-on server tự host. Nhờ vậy, dashboard và mọi route khác đều nằm sau một lần đăng nhập duy nhất, có thể thu hồi quyền tại một nơi.

Docker socket có quyền root

/var/run/docker.sock là một API có thể tạo container mount / từ host. Có quyền truy cập vào API này tương đương với có quyền root trên máy, và Traefik cần nó để đọc labels.

Giữ :ro trên mount, nhưng cần hiểu rõ tác dụng của tùy chọn này: nó chỉ làm cho socket file ở chế độ chỉ đọc. Nó không ngăn các request POST đến Docker API đi qua socket đó. Biện pháp giảm thiểu thực sự là không bao giờ cấp socket cho Traefik, mà đặt một filtering proxy ở giữa:

  dockerproxy:
    image: tecnativa/docker-socket-proxy   # pin the current tag
    restart: unless-stopped
    environment:
      CONTAINERS: 1
      NETWORKS: 1
      POST: 0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - proxy

Xóa socket volume khỏi Traefik và trỏ provider đến proxy:

--providers.docker.endpoint=tcp://dockerproxy:2375

Traefik vẫn có quyền đọc container và network, nhưng không còn khả năng tạo bất kỳ tài nguyên nào.

Firewall, port và rule mà mọi người thường cấu hình sai

Mở 2 port, cộng thêm SSH:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Các port được Docker publish sẽ bypass ufw. Docker chèn rule riêng vào iptables. Các rule này được xử lý trước các chain của ufw. Vì vậy, container khởi động với ports: ["3000:3000"] vẫn có thể được truy cập từ Internet dù đã có rule deny của ufw. Cách bảo vệ nằm ở cấu trúc triển khai, không phải ở cấu hình firewall: chỉ publish port từ Traefik, còn mọi container khác chỉ được cấp networks: [proxy] và không thêm gì khác. Nếu một dịch vụ thực sự phải truy cập host, hãy bind nó vào loopback, "127.0.0.1:3000:3000".

Xử lý sự cố: các lỗi bạn sẽ thực sự gặp

404 page not found, do Traefik trả về. Không có router nào khớp. Theo thứ tự khả năng xảy ra: container thiếu traefik.enable=true (trong khi đã đặt exposedByDefault=false); rule Host() không khớp với hostname bạn đã nhập; tên router trong một label khác với tên router trong label khác (routers.gitea.rulerouters.gitea.entrypoints phải là cùng một từ); hoặc bạn đặt hostname trong dấu ngoặc kép thay vì dấu backtick. Traefik v3 yêu cầu dùng dấu backtick bên trong matcher.

502 Bad Gateway. Một router đã khớp nhưng không thể truy cập backend. Gần như luôn là do container không nằm trên network proxy; hãy kiểm tra docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. Khả năng còn lại là loadbalancer.server.port sai: bạn đã khai báo port được publish, hoặc app đang listen ở nơi khác. Log sẽ ghi rõ lần thử kết nối: dial tcp 172.18.0.5:8080: connect: connection refused.

Trình duyệt cảnh báo và chứng chỉ được cấp cho TRAEFIK DEFAULT CERT. Không có chứng chỉ cho hostname đó nên Traefik đã trả về chứng chỉ self-signed tạm thời. Đọc các dòng ACME:

unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.com

DNS chưa trỏ đến máy chủ. Sửa record, chờ hết TTL rồi restart Traefik.

Invalid response from http://git.example.com/.well-known/acme-challenge/... trong HTTP challenge: port 80 không chuyển được đến Traefik từ bên ngoài. Nguyên nhân thường là firewall ở cấp provider phía trước VPS, không phải ufw.

Chứng chỉ không bao giờ được cấp, và DNS của bạn đang dùng Cloudflare với đám mây màu cam được bật. Cloudflare thực hiện TLS termination tại edge của họ, nên TLS-ALPN-01 không thể hoàn tất qua đó. Đặt record về DNS-only trong khi cấp chứng chỉ, hoặc chuyển sang DNS-01 challenge với API token. DNS-01 cũng là challenge duy nhất có thể cấp wildcard certificate.

Vòng lặp redirect. Một thành phần phía trước Traefik đã thực hiện TLS termination và chuyển tiếp plaintext đến :80; redirect của entrypoint lại chuyển request về HTTPS. Xóa một trong hai redirect.

Duy trì hoạt động

Unit của Docker phải được bật khi boot (systemctl is-enabled docker), và restart: unless-stopped sẽ khởi động lại stack sau khi reboot. Nếu cần kiểm soát rõ ràng hơn, hãy dùng một systemd unit nhỏ chạy docker compose -f /srv/edge/compose.yml up -d cùng với RemainAfterExit=yes để có systemctl status edge và kiểm soát thứ tự khởi động.

Hãy cố định tag của Traefik (traefik:v3.5, không dùng latest). Bản nâng cấp từ v2 lên v3 đã thay đổi cú pháp rule và tên provider. Một latest không được giám sát sẽ vẫn tự reload một config mà nó không còn hiểu. Hãy nâng cấp có chủ đích: đọc migration notes, cập nhật tag, docker compose up -d traefik, rồi monitor log. Nếu bạn vẫn đang dùng tag v2, hướng dẫn migration Traefik v2 lên v3 trình bày mọi thay đổi tên, compatibility mode và cách rollback mà vẫn giữ nguyên certificates.

Hãy backup ./letsencrypt và data volume của từng app. Traefik không lưu state nào khác mà bạn không thể dựng lại từ compose file.

Điều gì bị giới hạn khi mở rộng

Giới hạn đầu tiên không nằm ở throughput mà ở một máy duy nhất: một Traefik trên một VPS là điểm lỗi duy nhất cho 5 ứng dụng, còn acme.json là bộ nhớ dạng flat file nên 2 instance Traefik ghi vào đó sẽ làm hỏng dữ liệu. Muốn mở rộng sang nhiều máy, bạn phải chuyển nơi lưu certificate ra khỏi file hoặc thực hiện TLS termination ở một lớp khác.

Giới hạn thứ 2 là các kết nối tồn tại lâu. Server-sent events, file upload lớn và client chậm sẽ gặp timeout phản hồi của entrypoint; --entryPoints.websecure.transport.respondingTimeouts.readTimeout cùng với các tùy chọn liên quan là writeTimeoutidleTimeout. WebSocket đi qua mà không cần cấu hình thêm.

Giới hạn thứ 3 là disk. --accesslog=true ghi vào stdout, còn Docker json-file driver sẽ giữ log vô thời hạn nếu không đặt giới hạn. Đặt logging.options.max-size trên service Traefik hoặc ghi access log vào file rồi rotate file đó.

Không điều gì trong số này cần orchestrator. Bạn chỉ cần một server do mình kiểm soát, có IP public thực và mở cổng 80 cùng 443 cho Internet; một VPS nhỏ duy nhất là toàn bộ danh sách dependency.

FAQ

Tôi vẫn cần certbot nếu chạy Traefik không?

Không. ACME resolver của Traefik yêu cầu và gia hạn certificate cho mọi hostname mà nó định tuyến, rồi lưu toàn bộ vào acme.json. Certbot vẫn là công cụ phù hợp khi nginx hoặc server khác tự xử lý TLS termination; chạy cả hai cho cùng hostname chỉ làm hao rate limit của Let’s Encrypt.

Vì sao container trả về 404 qua Traefik?

404 do Traefik trả về nghĩa là không có router nào match request. Kiểm tra container có traefik.enable=true hay không (bắt buộc khi đã đặt exposedByDefault=false), giá trị Host() có khớp với tên bạn nhập hay không, và tên router có giống hệt nhau trong mọi label của app đó hay không. Traefik v3 cũng yêu cầu dùng backtick bên trong matcher, không dùng dấu nháy.

Trong trường hợp này, 404 khác 502 như thế nào?

404 nghĩa là việc định tuyến chưa xảy ra; 502 nghĩa là router đã match nhưng backend từ chối connection. Các nguyên nhân 502 thường gặp là container chưa được gắn vào network proxy, và loadbalancer.server.port trỏ đến published port thay vì port mà app đang listen bên trong container. Access log ghi rõ địa chỉ chính xác mà Traefik đã dial.

Mount Docker socket ở chế độ read-only có đủ an toàn không?

Flag :ro chỉ làm file socket ở chế độ read-only, không làm API phía sau socket thành read-only; các request POST vẫn đi qua socket đó, và quyền truy cập Docker API tương đương với root trên host. Cách an toàn hơn là dùng container docker-socket-proxy được nêu ở trên. Container này chỉ cung cấp cho Traefik quyền đọc thông tin container và network, đồng thời chặn hoàn toàn các thao tác ghi.

Traefik có thể cấp wildcard certificate không?

Chỉ thông qua DNS-01 challenge, với API token của nhà cung cấp DNS. TLS-ALPN-01 và HTTP-01 chỉ xác thực một hostname riêng lẻ nên không thể tạo wildcard certificate. DNS-01 cũng là lựa chọn phù hợp khi CDN như Cloudflare xử lý TLS termination phía trước VPS của bạn và 2 challenge còn lại không thể hoàn tất.