Cách dùng Traefik v3 chạy 5 app trên 1 VPS
Hướng dẫn dùng Traefik v3 và Docker Compose để chạy 5 ứng dụng trên một IP duy nhất. Cách tránh lỗi acme.json và tự động hóa Let's Encrypt qua Docker labels.
Một IP, năm ứng dụng, một port 443
VPS của bạn chỉ có một địa chỉ IPv4 công cộng và một port TCP 443 duy nhất. Bạn muốn chạy Gitea, một bản staging của ứng dụng, 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 chủ. Reverse proxy là quy trình chiếm giữ port :80 và :443, đọc header Host trong mỗi request và chuyển nó đến đúng container. Traefik làm việc này, nó tự lấy và tự gia hạn certificate cho mỗi hostname mà bạn không cần phải chạy certbot bằng tay.
Điểm khác biệt giữa Traefik và một nginx server {} block là nguồn gốc cấu hình. Với nginx, bạn chỉnh sửa một file rồi reload, còn vòng đời certificate là một công việc riêng biệt — quy trình mà bạn thường làm khi issue Let's Encrypt certificates with certbot on nginx, nơi 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 các labels từ container của bạn: chỉ cần chạy một container có chứa label quy tắc Host() là nó có thể được định tuyến ngay lập tức; dừng container đó và route sẽ biến mất. Đó cũng chính là một cái bẫy. Cấu hình nằm trong labels sẽ nằm ở năm nơi cùng một lúc, và một label sai sẽ không báo lỗi — container đơn giản là không được định tuyến, và Traefik sẽ không nói gì cả.
Bốn danh từ chính
- Entrypoints là các listening socket. Bạn sẽ định nghĩa hai entrypoints:
webtrên:80vàwebsecuretrên:443. - Routers thực hiện khớp một request (
Host(...)) và gắn nó vào một service. Các certificate được yêu cầu theo từng router thông quatls.certresolver. - Services là backend — bao gồm một container và port mà nó lắng nghe bên trong mạng Docker.
- Middlewares nằm giữa router và service: bao gồm basic auth, IP allow-lists, header rewrites, hoặc redirects.
Cấu hình tĩnh (entrypoints, providers, ACME) được truyền qua command line của Traefik hoặc trong traefik.yml; mọi thay đổi đều yêu cầu restart Traefik. Cấu hình động (routers, services, middlewares) được lấy từ container labels và được hot-reload. Việc nhầm lẫn giữa hai loại này là nguyên nhân phổ biến dẫn đến lỗi "flag của tôi không có tác dụng".
File compose
Một Docker network chung tên là proxy đóng vai trò là xương sống. Traefik chỉ có thể kết nối tới container nếu cả hai cùng nằm trong 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=3000docker compose up -d, sau đó là docker compose logs -f traefik. Mỗi ứng dụng bổ sung là một bản sao của block gitea với router name, Host() và port nội bộ riêng. Một cài đặt Nextcloud chạy trong Docker với TLS và backup cũng được chèn vào tương tự — bỏ các port đã publish, kết nối nó với proxy, và để các router labels xử lý hostname và certificate.
Có năm chi tiết quan trọng cần lưu ý.
exposedByDefault=false khiến container trở nên vô hình với Traefik cho đến khi nó có traefik.enable=true. Nếu bỏ qua, mọi container bạn khởi chạy — kể cả các container tạm thời như postgres dùng để kiểm tra — đều sẽ bị tự động tạo route.
providers.docker.network=proxy chỉ định cho Traefik network nào cần dùng khi một container kết nối với nhiều network. Nếu thiếu, Traefik có thể chọn sai IP của container, dẫn đến lỗi 502 trông giống như lỗi ứng dụng.
loadbalancer.server.port=3000 là port bên trong container; ví dụ Gitea lắng nghe tại port 3000. Lưu ý rằng không có container ứng dụng nào publish port cả — chỉ có Traefik làm việc đó.
Tính năng redirect trên entrypoint web sẽ chuyển các request plaintext thành 308 sang HTTPS. Port 80 vẫn cần mở: ACME HTTP challenge cần nó, và cả người dùng khi gõ hostname trực tiếp cũng vậy.
Việc nhân đôi $$ trong hash basic-auth là do Compose escaping, không phải lỗi đánh máy. Hãy tạo nó bằng htpasswd -nbB admin 'your-password' (package apache2-utils), sau đó nhân đôi mọi ký tự $.
Certificate và lỗi thường gặp với acme.json
tlschallenge=true chọn TLS-ALPN-01: Let's Encrypt kết nối tới server của bạn qua port 443 và Traefik sẽ trả lời challenge ngay trong quá trình TLS handshake. Lựa chọn khác là HTTP-01 qua port 80 — hãy thay thế dòng tlschallenge trong danh sách command: của Traefik bằng hai dòng sau:
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=webCả hai cách đều hoạt động. Cả hai đều yêu cầu DNS public của hostname phải trỏ về VPS của bạn — certificate authority sẽ phân giải tên miền và kết nối từ bên ngoài. Hãy tạo bản ghi A (và AAAA) trước, kiểm tra bằng dig +short git.example.com, sau đó mới khởi chạy Traefik.
Dưới đây là lỗi khiến nhiều người mất cả buổi tối để xử lý. Traefik lưu trữ ACME account key và mọi certificate đã cấp trong một file acme.json. Nếu file này cho phép group hoặc world-readable, Traefik sẽ in ra một dòng thông báo tương tự như sau và dừng hoạt động:
error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600Cách khắc phục triệt để là phương pháp ở 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 đã đặt quyền là 644. Hãy sửa lại trên host:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikHãy backup directory đó cùng với các app volumes của bạn. Nếu mất file này thì vẫn có thể khắc phục được — bằng cách cấp lại certificate — nhưng việc cấp lại cùng lúc năm hostname sẽ khiến bạn dính rate limits.
Hãy dùng staging CA trong quá trình thử nghiệm. Bỏ comment dòng caserver để chạy thử mọi route, sau khi xong hãy comment lại và xóa acme.json để yêu cầu cấp certificate production mới. Let's Encrypt production chỉ cho phép tối đa năm certificate trùng lặp mỗi tuần cho cùng một tập hợp hostname, và họ sẽ giới hạn (throttle) nếu bạn thực hiện validation thất bại liên tục cho cùng một tên miền. Staging sẽ cấp các certificate không đáng tin cậy — trình duyệt sẽ cảnh báo, và chính cảnh báo đó là dấu hiệu cho thấy mọi thứ đã hoạt động — với các giới hạn nới lỏng hơn nhiều.
Dashboard là giao diện điều khiển, không phải bản demo
Hầu hết các hướng dẫn nhanh đều thiết lập --api.insecure=true, dịch vụ này chạy dashboard trên port 8080 và không có xác thực. Nếu dùng trên một máy có IP công khai, nó sẽ tiết lộ topology routing, hostname, tên middleware và các backend port cho bất kỳ ai thực hiện scan.
Các label trên service traefik ở trên là giải pháp thay thế: dashboard được định tuyến như các app khác, chạy trên hostname thật, qua TLS và nằm sau basicauth. service=api@internal là thành phần kết nối router với API tích hợp sẵn của Traefik. Bạn có thể thắt chặt bảo mật hơn bằng cách dùng IP allow-list, áp dụng theo thứ tự từ trái sang phải. Nếu địa chỉ văn phòng của bạn là IP động, hãy thiết lập dải IP theo subnet được cấp bởi một WireGuard VPN bạn tự host trên cùng VPS đó và chỉ truy cập dashboard thông qua tunnel:
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-authDocker socket có quyền root
/var/run/docker.sock là một API có thể tạo container và mount / từ host. Quyền truy cập vào nó tương đương với quyền root trên máy, và Traefik cần nó để đọc các label.
Giữ nguyên :ro trong mount, nhưng cần hiểu rõ tác dụng của nó: nó chỉ đặt socket file ở chế độ read-only. Nó không ngăn chặn các request POST gửi đến Docker API thông qua socket đó. Cách giảm thiểu rủi ro thực sự là không cấp socket trực tiếp cho Traefik, mà hãy đặt một proxy lọc ở 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:
- proxyXóa volume socket khỏi Traefik và trỏ provider về phía proxy:
--providers.docker.endpoint=tcp://dockerproxy:2375Traefik vẫn giữ quyền read đối với các container và network, nhưng mất khả năng tạo mới bất kỳ thứ gì.
Firewall, ports, và quy tắc mà mọi người đều nhầm lẫn
Hai port đang mở, cộng với SSH:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableCác port được publish bởi Docker sẽ bypass ufw. Docker chèn các rule iptables riêng của nó, được kiểm tra trước các chain của ufw. Do đó, một container được khởi chạy với ports: ["3000:3000"] vẫn có thể truy cập được từ internet ngay cả khi ufw đang đặt rule deny. Đây là vấn đề về cấu trúc hệ thống, không phải do cấu hình firewall: chỉ nên publish port từ Traefik, và thiết lập networks: [proxy] cho tất cả các container khác. Nếu một dịch vụ thực sự cần kết nối tới host, hãy bind nó vào loopback — "127.0.0.1:3000:3000".
Troubleshooting: 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. Các nguyên nhân theo thứ tự khả năng xảy ra: container thiếu traefik.enable=true (với exposedByDefault=false đã được thiết lập); rule Host() không khớp với tên bạn đã nhập; tên router trong label này khác với tên router trong label khác (routers.gitea.rule và routers.gitea.entrypoints phải là cùng một từ); hoặc bạn dùng dấu ngoặc kép thay vì dấu backtick cho hostname. Traefik v3 yêu cầu dùng dấu backtick bên trong các matcher.
502 Bad Gateway. Một router đã khớp nhưng backend không thể kết nối. Hầu hết các trường hợp là do container không nằm trong network proxy — hãy kiểm tra docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. Một nguyên nhân khác là sai loadbalancer.server.port: bạn đang dùng port đã publish, hoặc ứng dụng đang lắng nghe ở một port khác. Log sẽ ghi lại nỗ lực kết nối: dial tcp 172.18.0.5:8080: connect: connection refused.
Trình duyệt cảnh báo, và certificate được cấp cho TRAEFIK DEFAULT CERT. Không có certificate nào tồn tại cho hostname đó và Traefik đã trả về certificate tự ký (self-signed) mặc định. Hãy đọ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.comDNS vẫn chưa trỏ về máy chủ. Hãy sửa bản ghi, đợi hết thời gian TTL, sau đó restart Traefik.
Invalid response from http://git.example.com/.well-known/acme-challenge/... khi dùng HTTP challenge: port 80 không thể kết nối tới Traefik từ bên ngoài — thường là do firewall ở cấp nhà cung cấp đứng trước VPS, không phải do ufw.
Không thể cấp certificate, và DNS của bạn đang dùng Cloudflare với chế độ orange cloud (proxied). Cloudflare kết thúc TLS tại edge của nó và TLS-ALPN-01 không thể hoàn tất qua đó. Hãy chuyển bản ghi sang chế độ DNS-only trong khi chờ cấp certificate, hoặc chuyển sang DNS-01 challenge với một API token. DNS-01 cũng là challenge duy nhất có thể cấp wildcard certificate.
Redirect loop. Một thành phần đứng trước Traefik đã kết thúc TLS và chuyển tiếp plaintext tới port :80; lệnh redirect của entrypoint lại gửi nó ngược lại HTTPS. Hãy xóa bỏ một trong hai lệnh redirect này.
Duy trì hoạt động
Docker unit phải được bật ở chế độ boot (systemctl is-enabled docker), và restart: unless-stopped sẽ khôi phục stack sau khi reboot. Để kiểm soát tường minh, một systemd unit nhỏ chạy docker compose -f /srv/edge/compose.yml up -d với RemainAfterExit=yes sẽ giúp bạn kiểm soát systemctl status edge và thứ tự khởi chạy.
Sử dụng tag Traefik cố định (traefik:v3.5, đừng bao giờ dùng latest). Bản nâng cấp từ v2 lên v3 đã thay đổi cú pháp rule và tên provider; nếu để latest tự động, nó sẽ reload một config mà nó không còn hiểu nữa. Hãy nâng cấp có chủ đích: đọc ghi chú migration, cập nhật tag, docker compose up -d traefik, và theo dõi log. Nếu bạn vẫn đang dùng tag v2, hướng dẫn migration Traefik v2 sang v3 sẽ hướng dẫn chi tiết từng thay đổi tên, chế độ tương thích, và cách rollback mà vẫn giữ được certificate của bạn.
Hãy backup ./letsencrypt và volume dữ liệu của từng app. Traefik không lưu trữ state nào khác mà bạn không thể rebuild từ file compose.
Những gì sẽ gặp lỗi khi mở rộng quy mô
Giới hạn đầu tiên không phải là throughput, mà là giới hạn của một máy đơn lẻ: một Traefik trên một VPS là một điểm lỗi duy nhất (single point of failure) cho năm ứng dụng, và acme.json là lưu trữ dạng file — hai instance Traefik cùng ghi vào file đó sẽ làm hỏng dữ liệu. Mở rộng quy mô (scaling out) nghĩa là phải chuyển việc lưu trữ certificate ra khỏi file, hoặc kết thúc TLS ở một nơi khác.
Thứ hai là các kết nối duy trì lâu (long-lived connections). Server-sent events, các tác vụ upload lớn và các client chậm sẽ gặp lỗi timeout phản hồi tại entrypoint; --entryPoints.websecure.transport.respondingTimeouts.readTimeout cùng các sibling writeTimeout và idleTimeout của nó chính là các thông số để điều chỉnh. WebSockets sẽ được truyền qua (pass through) mà không cần cấu hình thêm.
Thứ ba là ổ đĩa. --accesslog=true ghi dữ liệu ra stdout, và Docker json-file driver sẽ giữ dữ liệu đó mãi mãi trừ khi bạn giới hạn dung lượng. Hãy thiết lập logging.options.max-size cho Traefik service, hoặc ghi access log vào một file và thực hiện rotate nó.
Tất cả những điều này không cần đến một orchestrator. Bạn chỉ cần một server do bạn kiểm soát, có IP thật và mở sẵn port 80 và 443 cho thế giới — một VPS nhỏ duy nhất là toàn bộ danh sách các thành phần phụ thuộc.
FAQ
Tôi có cần dùng certbot nếu đã chạy Traefik không?
Không. ACME resolver của Traefik tự động yêu cầu và gia hạn certificate cho mọi hostname mà nó route, sau đó lưu trữ chúng trong acme.json. Certbot vẫn là công cụ phù hợp khi nginx hoặc các server khác tự xử lý TLS; chạy cả hai cho cùng một hostname sẽ làm lãng phí rate limit của Let's Encrypt.
Tại sao container của tôi trả về lỗi 404 khi chạy qua Traefik?
Lỗi 404 từ Traefik nghĩa là không có router nào khớp với request. Hãy kiểm tra xem container đã có traefik.enable=true chưa (bắt buộc sau khi đã set exposedByDefault=false), giá trị Host() có khớp với tên bạn đã nhập không, và tên router phải đồng nhất trong mọi label của app đó. Traefik v3 yêu cầu dùng dấu backtick bên trong matcher thay vì dấu ngoặc kép.
Sự khác biệt giữa lỗi 404 và 502 ở đây là gì?
404 nghĩa là việc routing chưa hề xảy ra; 502 nghĩa là router đã khớp nhưng backend từ chối kết nối. Các nguyên nhân gây lỗi 502 phổ biến là container chưa được kết nối vào network proxy, hoặc loadbalancer.server.port đang trỏ vào port đã publish thay vì port mà app đang listen bên trong container. access log sẽ hiển thị chính xác địa chỉ mà Traefik đã kết nối tới.
Mount Docker socket ở chế độ read-only đã đủ chưa?
Flag :ro chỉ làm cho socket file ở chế độ read-only, không phải API phía sau nó — các request POST vẫn truyền qua đó, và quyền truy cập Docker API tương đương với quyền root trên host. Giải pháp an toàn hơn là dùng container docker-socket-proxy như ví dụ trên, nó chỉ cấp quyền đọc container và network cho Traefik và chặn hoàn toàn quyền ghi.
Traefik có thể cấp wildcard certificate không?
Chỉ có thể thông qua DNS-01 challenge, với một API token của nhà cung cấp DNS. TLS-ALPN-01 và HTTP-01 chỉ xác thực một hostname duy nhất và không thể tạo wildcard. DNS-01 cũng là giải pháp khi bạn dùng CDN như Cloudflare để terminate TLS trước VPS và hai phương thức challenge còn lại không thực hiện được.