Traefik vs Caddy vs Nginx:Docker反向代理对比
三套可用的Docker Compose配置,分别使用Traefik、Caddy和Nginx作为VPS上的反向代理。相同后端、真实基准测试,以及帮你做出选择的决策框架。
你的VPS上跑着Docker容器,需要HTTPS、按域名路由和统一入口。Traefik、Caddy和Nginx都能解决这个问题,但方式各不相同。
本文提供三套可用的Docker Compose配置,用同一个后端分别部署在三个代理后面。复制适合你场景的那一套。文末的对比表和决策框架帮你做选择。
所有示例都使用专用代理网络、HTTP到HTTPS重定向和生产级默认配置。这些配置针对Virtua Cloud VPS上的Ubuntu 24.04。
反向代理对VPS上的Docker容器有什么作用?
反向代理位于互联网和Docker容器之间。它终止TLS(HTTPS),根据主机名将请求路由到正确的容器,并暴露一对端口(80/443)而不是每个服务一个端口。容器不需要处理证书,也不需要直接绑定公共端口。
没有反向代理,每个容器都需要自己的公共端口。访问者需要通过example.com:3000访问一个服务,example.com:8080访问另一个。反向代理让你可以使用标准端口上的app.example.com和api.example.com。
以下三套配置假设:
- Docker和Docker Compose已安装 VPS上的Docker生产环境:会出什么问题以及如何解决
- DNS A记录已指向VPS的IP
- 防火墙已开放80和443端口 修复Docker绕过UFW防火墙:4种经过测试的VPS解决方案
- 你有非root用户的SSH访问权限(带sudo)
每个示例都部署相同的后端:traefik/whoami镜像,返回HTTP头信息和容器信息。之后替换成你的真实应用即可。
如何将Traefik配置为Docker反向代理并启用自动HTTPS?
Traefik通过读取Docker标签自动发现容器。你在每个服务上以标签形式添加路由规则。容器启动后,Traefik检测到它,请求Let's Encrypt证书并开始路由流量。无需重新加载配置。
创建项目目录:
mkdir -p ~/traefik-proxy && cd ~/traefik-proxy
创建所有代理服务共享的Docker网络:
docker network create proxy
创建docker-compose.yml:
services:
traefik:
image: traefik:v3.6
container_name: traefik
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.letsencrypt.acme.email=you@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--log.level=WARN"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./acme.json:/acme.json
networks:
- proxy
security_opt:
- no-new-privileges:true
whoami:
image: traefik/whoami
container_name: whoami
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`app.example.com`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
networks:
- proxy
networks:
proxy:
external: true
启动前,创建权限受限的证书存储文件:
touch acme.json && chmod 600 acme.json
如果acme.json权限过于开放,Traefik会拒绝启动。600确保只有所有者能读取其中存储的私钥。
启动:
docker compose up -d
确认两个容器都在运行:
docker compose ps
traefik和whoami都应显示Up状态。从本地机器(不是服务器)测试:
curl https://app.example.com
响应包含whoami输出和请求头。响应中的X-Forwarded-For头表明Traefik正在代理流量并终止TLS。
标签的作用:
traefik.enable=true启用此容器(因为设置了exposedbydefault=false)traefik.http.routers.whoami.rule=Host(...)按主机名匹配请求traefik.http.routers.whoami.tls.certresolver=letsencrypt指示Traefik为此域名获取证书
要添加另一个服务,在同一proxy网络上的任意Compose文件中添加带有相应标签的服务。Traefik会自动检测到它。
在Traefik中挂载Docker socket安全吗?
挂载/var/run/docker.sock赋予Traefik对Docker API的完全访问权限。如果攻击者攻破了Traefik,就能创建容器、读取环境变量(包括密钥)并在主机上提权到root。:ro标志只在文件系统层面防止写入,不限制Docker API调用。
在生产环境中,使用Docker socket代理。它位于Traefik和Docker守护进程之间,过滤API调用,只允许对容器元数据的读取操作。
在docker-compose.yml中添加:
services:
socket-proxy:
image: tecnativa/docker-socket-proxy:0.4
container_name: socket-proxy
restart: unless-stopped
environment:
CONTAINERS: 1
NETWORKS: 1
SERVICES: 0
TASKS: 0
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- socket-proxy
security_opt:
- no-new-privileges:true
traefik:
image: traefik:v3.6
container_name: traefik
restart: unless-stopped
depends_on:
- socket-proxy
command:
- "--providers.docker.endpoint=tcp://socket-proxy:2375"
- "--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.letsencrypt.acme.email=you@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--log.level=WARN"
ports:
- "80:80"
- "443:443"
volumes:
- ./acme.json:/acme.json
networks:
- proxy
- socket-proxy
security_opt:
- no-new-privileges:true
networks:
proxy:
external: true
socket-proxy:
driver: bridge
internal: true
Traefik不再直接挂载Docker socket。socket-proxy网络设置为internal: true,意味着没有出站互联网访问。socket代理只允许对containers和networks端点的GET请求。
如何将Caddy配置为Docker反向代理并启用自动HTTPS?
Caddy自动处理HTTPS,除了域名外不需要任何配置。将域名指向服务器,写入Caddyfile,Caddy就会从Let's Encrypt获取并续期证书。不需要resolver配置,不需要ACME设置。这是Docker反向代理获取HTTPS的最短路径。
创建项目目录:
mkdir -p ~/caddy-proxy && cd ~/caddy-proxy
创建共享代理网络(如果已为Traefik创建则跳过):
docker network create proxy
创建Caddyfile:
app.example.com {
reverse_proxy whoami:80
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
这就是全部代理配置。Caddy读取域名,请求证书,将流量代理到端口80上的whoami容器。不需要证书resolver,不需要ACME邮箱(Caddy使用机器默认值,或者你可以全局设置),不需要管理存储路径。
创建docker-compose.yml:
services:
caddy:
image: caddy:2.11
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- proxy
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
whoami:
image: traefik/whoami
container_name: whoami
restart: unless-stopped
networks:
- proxy
networks:
proxy:
external: true
volumes:
caddy_data:
caddy_config:
443:443/udp端口启用Caddy原生支持的HTTP/3(QUIC)。cap_drop: ALL配合cap_add: NET_BIND_SERVICE移除了所有Linux capability,只保留绑定1024以下端口所需的那一个。
启动:
docker compose up -d
检查容器状态:
docker compose ps
两个容器都应显示Up。从本地机器测试,使用详细输出:
curl -v https://app.example.com
在输出中寻找HTTP/2 200。你还应该看到Caddyfile中的安全头(Strict-Transport-Security、X-Content-Type-Options等)。
要添加另一个服务,在Caddyfile中添加新的域名块和reverse_proxy指令,然后重新加载:
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
不需要重启容器。Caddy不需要Docker socket,不会自动发现容器。你在Caddyfile中管理路由。
如何将Nginx配置为Docker反向代理并使用Let's Encrypt?
Nginx让你完全控制每个代理指令、头部、缓冲区大小和缓存规则。代价是手动配置。Nginx不会自己获取TLS证书。你需要搭配Certbot,由它处理ACME挑战和证书续期。
创建项目目录:
mkdir -p ~/nginx-proxy && cd ~/nginx-proxy
创建共享代理网络:
docker network create proxy
在nginx/conf.d/app.conf中创建Nginx配置:
mkdir -p nginx/conf.d
server {
listen 80;
server_name app.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers off;
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
proxy_pass http://whoami:80;
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;
}
}
server_tokens off;隐藏响应头中的Nginx版本号。暴露版本信息会帮助攻击者针对已知漏洞进行攻击。
创建docker-compose.yml:
services:
nginx:
image: nginx:1.28
container_name: nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- certbot_webroot:/var/www/certbot:ro
- certbot_certs:/etc/letsencrypt:ro
networks:
- proxy
depends_on:
- whoami
certbot:
image: certbot/certbot
container_name: certbot
restart: unless-stopped
volumes:
- certbot_webroot:/var/www/certbot
- certbot_certs:/etc/letsencrypt
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
whoami:
image: traefik/whoami
container_name: whoami
restart: unless-stopped
networks:
- proxy
networks:
proxy:
external: true
volumes:
certbot_webroot:
certbot_certs:
Nginx要求证书文件在启动前就存在。上面的配置引用了/etc/letsencrypt/live/app.example.com/fullchain.pem,但它还不存在。首次获取证书时,临时将app.conf替换为纯HTTP版本:
server {
listen 80;
server_name app.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
启动Nginx和后端:
docker compose up -d nginx whoami
请求初始证书:
docker compose run --rm certbot certonly \
--webroot \
--webroot-path=/var/www/certbot \
-d app.example.com \
--email you@example.com \
--agree-tos \
--no-eff-email
证书获取后,恢复完整的app.conf(上面展示的包含SSL服务器块的版本),然后启动完整stack:
docker compose up -d
确认所有容器都在运行:
docker compose ps
从本地机器测试:
curl -v https://app.example.com
响应头中的server:应显示nginx但不带版本号,这说明server_tokens off生效了。
要添加另一个服务,在nginx/conf.d/中创建新的.conf文件,然后重新加载:
docker compose exec nginx nginx -s reload
证书续期方面,Certbot容器每12小时运行certbot renew。续期后重新加载Nginx以使用新证书。可以用cron任务或检查证书修改时间的脚本来自动化这一过程。关于Nginx反向代理配置的深入内容,参见 Nginx反向代理配置教程。
Traefik、Caddy和Nginx在Docker反向代理方面如何对比?
Traefik在自动发现方面胜出。Caddy在简洁性方面胜出。Nginx在控制力方面胜出。下表列出了在VPS上运行Docker容器时需要权衡的要点。
| 特性 | Traefik v3 | Caddy 2.11 | Nginx 1.28 |
|---|---|---|---|
| 自动发现 | 是(Docker标签) | 否(手动Caddyfile) | 否(手动conf文件) |
| TLS自动化 | 内置ACME | 内置ACME | 需要Certbot sidecar |
| 配置方式 | Docker标签 + 静态YAML/CLI | Caddyfile或JSON API | nginx.conf文件 |
| 配置重载 | 容器事件时自动 | caddy reload(零停机) |
nginx -s reload(零停机) |
| 需要Docker socket | 是(或socket代理) | 否 | 否 |
| HTTP/3(QUIC) | 实验性 | 是(默认) | 通过第三方模块 |
| 中间件/插件 | 内置(限流、认证、头部) | 内置 + Go插件 | 通过配置指令 |
| 社区/文档 | 大型、活跃、文档好 | 较小、文档优秀 | 最大、文档全面 |
| 学习曲线 | 中等(标签 + 静态配置) | 低(Caddyfile直观) | 高(指令多) |
哪个反向代理内存占用最少?
空闲内存占用在VPS上很重要,每个MB都很宝贵。这些数据来自Virtua Cloud 4 vCPU / 8 GB RAM VPS上运行Ubuntu 24.04时的docker stats --no-stream。每个代理在测量前无流量空闲运行。
| 代理 | 空闲RAM | 镜像大小 |
|---|---|---|
| Traefik v3.6 | ~17 MB | ~242 MB |
| Caddy 2.11 | ~14 MB | ~88 MB |
| Nginx 1.28 | ~5 MB | ~240 MB |
| Nginx + Certbot | ~5 MB + ~25 MB | ~240 MB + ~298 MB |
Nginx内存占用远远最低。Caddy居中。Traefik较高的内存占用来自于在内存中维护Docker provider状态和路由表。三者都使用默认镜像(基于Debian/Alpine)。Alpine变体可以减小镜像大小,但可能带来某些扩展的兼容性问题。
轻负载下(通过wrk发送100个并发请求),三者在这个VPS规格上都能处理流量,CPU和内存没有明显增加。差异只在大规模场景或最小VPS套餐上才有意义。
如何为Docker配置选择合适的反向代理?
正确的选择取决于你运行多少服务、变更频率以及你已有的知识。
选择Traefik的场景:
- 运行大量频繁变更的容器(每周添加/移除服务)
- 想要零接触路由:部署带标签的容器即可上线
- 使用Docker Swarm或需要跨多节点的服务发现
- 接受Docker socket暴露(生产环境配合socket代理)
选择Caddy的场景:
- 运行少量很少变更的服务
- 想要最简单的自动HTTPS路径
- 不想挂载Docker socket
- 看重小镜像和低内存占用
- 想要无需额外配置的HTTP/3支持
选择Nginx的场景:
- 已经熟悉Nginx配置
- 需要对代理行为进行精细控制(缓冲区、缓存、按location自定义头部)
- 想要尽可能低的内存占用
- 团队已有Nginx工具链和监控
- 不介意单独管理Certbot
决策流程:
- 运行超过5个定期变更的Docker服务?是 -> Traefik
- 需要精细的代理调优或已经在用Nginx?是 -> Nginx
- 想要最少的组件和最快的搭建?是 -> Caddy
对于部署一两个个人项目的独立开发者来说,Caddy是最好的起点。对于管理大量容器的DevOps团队,Traefik的自动发现物有所值。对于已在其他地方使用Nginx的团队,继续用Nginx保持技术栈一致 VPS上的Docker网络:bridge、host和macvlan详解。
三种代理的安全加固
无论选择哪个代理,都要应用这些基本安全实践。
安全头部。 以上三个示例都包含HSTS、X-Content-Type-Options、X-Frame-Options和Referrer-Policy。对于Traefik,通过middleware标签添加:
labels:
- "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.security-headers.headers.frameDeny=true"
- "traefik.http.routers.whoami.middlewares=security-headers"
限流。 Traefik内置限流middleware。Caddy有可作为插件使用的rate_limit指令。Nginx在配置中使用limit_req_zone。限流保护后端免受暴力攻击和滥用。
Docker网络隔离。 每个示例都使用外部proxy网络。后端服务不应在默认bridge网络上。只有需要代理的容器才加入proxy网络。数据库容器和内部服务留在独立的内部网络中 Docker安全加固:VPS上的Rootless模式、Seccomp和AppArmor。
防火墙。 只有80和443端口应该公开访问。Docker直接操作iptables,可能绕过UFW规则。解决方案见 修复Docker绕过UFW防火墙:4种经过测试的VPS解决方案。
日志。 出问题时查看代理日志:
# Traefik
docker logs traefik -f
# Caddy
docker logs caddy -f
# Nginx
docker logs nginx -f
对于Traefik,临时设置--log.level=DEBUG来诊断路由或证书问题。对于Caddy,在Caddyfile中启用全局debug选项。对于Nginx,检查容器内/var/log/nginx/error.log中的error.log。
遇到问题?
| 症状 | 可能原因 | 解决方法 |
|---|---|---|
| 证书未签发 | DNS A记录未指向VPS IP | 用dig app.example.com验证 |
| Traefik所有路由返回404 | 容器不在proxy网络中 |
检查docker network inspect proxy |
| Caddy端口80 "permission denied" | 缺少NET_BIND_SERVICE capability |
添加cap_add: NET_BIND_SERVICE |
| Nginx证书 "no such file" | Certbot尚未运行 | 先运行certbot certonly |
ERR_CONNECTION_REFUSED |
防火墙阻止80/443 | 检查ufw status或iptables -L |
Traefik acme.json权限错误 |
文件权限过于开放 | 运行chmod 600 acme.json |
| 代理在服务器上正常,外部访问失败 | 只在localhost测试 | 从本地机器用curl测试 |
关于反向代理之外的生产加固,参见 Docker Compose资源限制、健康检查与重启策略 了解Compose stack的资源限制和健康检查。
版权所有 2026 Virtua.Cloud。保留所有权利。 本内容为 Virtua.Cloud 团队原创作品。 未经书面许可,禁止复制、转载或再分发。