☰
Caddy与Docker Compose组合实现单机HTTPS自动部署与证书续期
2026/10/22 8:45:11 网站建设 项目流程

手上一台单机服务器,想给业务上HTTPS,又不想手动配Nginx证书、写续期脚本。我把Caddy和Docker Compose这套组合用了一段时间,可以说它已经成了我所有轻量级服务的标配。Caddy负责自动申请证书、自动续期、自动配置HTTPS,Docker Compose负责把反向代理和业务服务编排在一起,一条docker compose up -d就能让整站从HTTP变成HTTPS。这篇文章适合三类人:刚接触服务器部署的新手、被Nginx加Certbot方案折腾过的运维同学,以及想在单机上快速跑多个HTTPS站点的人。整套方案不复杂,但坑不少,下面我把设计思路、完整配置和常见问题一次性讲清楚。

1. 项目拆解:为什么是Caddy+Docker Compose这套组合

1.1 单机HTTPS部署的痛点到底在哪

先说一个很常见的场景:你在云服务商那里买了一台单机服务器,打算跑一个博客、一个接口服务或者一个演示系统。只要面向用户打开浏览器,就绕不开HTTPS。没有HTTPS,浏览器地址栏直接提示“不安全”,用户心理上先打折扣;如果涉及表单提交、登录口令,这些数据在链路上就是明文传输的,风险更大。

传统做法是怎么样的呢?装一个Nginx,写server块,指定SSL证书路径,再用Certbot申请证书,写一个cron定时任务在证书快到期时续期,续期完成后还要reload Nginx。这套流程最多能跑通,但真正的痛点在于“证书生命周期管理”是一个有状态的过程。你申请的时候信誓旦旦记得三个月后续期,但真到了第80天,你很可能已经忘了还有这回事。更不要说还有这些幺蛾子:

  • 申请时80端口被另一个服务占着,ACME验证直接失败;
  • 服务器迁移,证书文件没跟着走,新机器上一堆证书路径报错;
  • 域名多了以后,每个域名都要单独配一遍证书申请和续期任务;
  • 一旦证书过期,用户访问时浏览器弹出红色警告,网站的信任度瞬间崩塌。

所以单机HTTPS部署,难点不是“配置Nginx加载证书”,而是“如何让证书的申请、续期、重载全过程自动化”。手动步骤越多,越容易在某个环节漏掉,最后变成半夜爬起来救火。

1.2 Caddy凭什么能把证书续期做成默认能力

Caddy是一个用Go写的开源Web服务器,它有一个很突出的设计理念,就是把HTTPS变成默认能力而不是可选项。只要你在配置文件里写了域名,Caddy启动时会先去检查这个域名有没有可用证书;没有证书,它自己就会通过ACME协议向公共CA申请,拿到证书后立即加载;证书快到期时,它会在后台自动续期,全程不需要你写一行cron。

这个设计在做单机部署时非常舒服。对比Nginx,你需要额外安装Certbot、配置systemd timer、处理Nginx reload权限;而Caddy把这些都收进了进程内部。配置文件有多简单呢?一个站点块就两行:

example.com { reverse_proxy app:8080 }

这一句配置同时完成了三件事:为example.com申请并自动管理HTTPS证书、把HTTP请求跳转到HTTPS、把请求反向代理给名为app的容器8080端口。Caddy还顺带开了HTTP/2和HTTP/3,对现代Web应用来说属于白赚的性能收益。

1.3 Docker Compose在单机场景里的定位

有人可能会问,单机服务器上直接装Caddy不就行了,为什么还要套一层Docker Compose?我的实际体验是,Docker Compose的价值在“编排”和“隔离”上。

单机上你不可能只跑一个服务。一个容器是Caddy负责流量入口和证书,另几个容器是业务应用、数据库、缓存。用Compose定义一个项目,它们共享一个自定义网络,容器之间用服务名互相访问,不用关心IP地址是不是变了。Caddy配置里写reverse_proxy app:8080,这个app就是其他容器在Compose网络里的主机名,这种写法比写死IP要优雅得多。

另一个好处是环境可复现。换一台新机器,把compose文件和Caddyfile拷过去,docker compose up -d,整套环境就起来了,不用再手动装Nginx、配系统服务、导证书。再加上数据卷持久化,Caddy的证书和账号信息都存在命名卷里,即使镜像升级、容器重建,证书也不会丢。对于单机服务器场景,这套组合比直接裸装Caddy多了环境一致性和迁移便捷性,比上Kubernetes又轻得多,正合适。

2. 环境准备与核心概念拆解

2.1 前置条件与版本选择

动手之前先确认几件事:

  • 一台公网可访问的单机服务器,建议Linux系统,Debian或Ubuntu都行,CentOS也能跑,只是部分命令略有差异;
  • 服务器上装好Docker Engine和Docker Compose v2插件。现在主流发行版用docker compose子命令,而不是老版的docker-compose;
  • 一个已经解析到这台服务器IP的域名。证书申请时域名解析必须生效,否则ACME验证会失败;
  • 云安全组和系统防火墙中放行80和443端口。80用于HTTP挑战和HTTP跳转,443用于HTTPS访问和TLS挑战。

镜像方面,我推荐使用caddy:2-alpine而不是默认的caddy:latest。Alpine基础镜像体积小,启动快,攻击面也小。这里要注意版本号,Caddy 1和Caddy 2的Caddyfile语法差别很大,现在写配置都按Caddy 2来,别拿老教程套。

2.2 Caddy自动HTTPS背后的挑战机制

要弄懂Caddy为什么能自动拿到证书,一定要知道ACME协议的两个常见验证模式。

第一种是HTTP-01挑战。Caddy收到申请请求后,会把一个随机token放在http://你的域名/.well-known/acme-challenge/路径下。CA会从公网发起请求,访问这个URL,如果能成功拿到token,就证明你控制了该域名的80端口,从而签发证书。

第二种是TLS-ALPN-01挑战。CA直接与你服务器的443端口建立TLS连接,在TLS握手阶段检查ALPN扩展是否包含acme-tls/1这个协议标识。如果匹配,说明你控制了该域名的443端口。

Caddy的默认策略是先尝试HTTP-01;如果80端口不通或者被占用,它会自动尝试TLS-ALPN-01验证。这个容错能力很实用,比如某些网络环境只放行了443,Caddy也能工作。但为了稳妥,我还是建议公网上把80和443都打开,因为HTTP-01是最通用、兼容性最好的验证方式。

这里有一个容易踩的误区:很多人以为只要配置了443,Caddy就能申请证书,然后嫌弃80端口“多余”,干脆不放行。实际上一段时间内你会看到证书申请失败,或者浏览器一直提示证书无效,原因可能就是80端口被防火墙挡了,Caddy没能完成初始验证。这个问题后面的排查部分还会展开。

2.3 目录挂载与网络规划

写Compose文件前,先理清Caddy在容器里用到哪些路径:

  • /etc/caddy/Caddyfile:配置文件源文件,最好以只读方式从宿主机挂载;
  • /data:存放ACME账号私钥、证书以及证书申请状态;
  • /config:存放Caddy运行时自动生成的JSON配置。

/data和/config都必须用命名卷持久化,不能指望容器文件系统。原因很简单:容器的可写层在镜像升级、容器重建时很容易被清掉。证书一旦重新申请,先不说慢,公共CA对同一主域是有频率限制的,反复删除卷重建等于给自己找麻烦。

网络规划方面,我在Compose文件里会单独定义一个web网络,把Caddy和业务服务都放进去。Caddy通过这个网络访问业务服务,业务服务不需要暴露到宿主机,也就不必映射端口,外部流量统一从Caddy的80/443进来。这一步能有效减少端口占用冲突,也方便之后往内网加更多服务。

3. 从零搭建:Caddyfile与docker-compose.yml的完整实现

3.1 docker-compose.yml示例

给一个可以直接抄作业的Compose文件。我把业务服务用一个示例容器代替了,你只需替换成自己的镜像或build上下文:

services: caddy: image: caddy:2-alpine container_name: caddy restart: unless-stopped ports: - "80:80" - "443:443" environment: - ACME_AGREE=true volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config networks: - web app: image: your-app-image:latest container_name: app restart: unless-stopped networks: - web volumes: caddy_data: caddy_config: networks: web: driver: bridge

解释几个细节:

  • restart: unless-stopped:服务器重启后容器自动拉起,不用手动干预;
  • ports只写在Caddy服务上,业务服务不暴露端口,外部只能通过Caddy访问;
  • ACME_AGREE=true:这个环境变量表示同意ACME服务条款,避免容器启动时出现交互式确认。新版Caddy里,如果全局配置写了email,通常会自动视为同意条款,但加上这个变量更保险;
  • 网络驱动用默认的bridge,不需要特殊配置。Compose会自动为web网络分配子网,并让容器之间通过服务名互相解析。

3.2 Caddyfile的站点配置与反向代理规则

在项目目录下新建Caddyfile,内容先写一个全局块和一个站点块:

{ email your-email@example.com } example.com { reverse_proxy app:8080 }

全局块里的email是ACME申请证书时用的联系人邮箱,CA会在证书快过期时发提醒邮件。建议填真实邮箱,虽然Caddy会自动续期,但多一层提醒总归是好的。

站点块里的reverse_proxy就是反向代理指令。app:8080中,app是Compose网络里另一个容器的服务名,8080是服务监听的端口。这里有一个新手常犯的错误,在宿主机上直接写127.0.0.1:8080。在Compose网络里,127.0.0.1指向容器自身,根本连不到业务服务,所以必须写服务名。

多站点场景也很简单,继续追加站点块就行:

api.example.com { reverse_proxy api:3000 } www.example.com { redir https://example.com{uri} }

api.example.com会自动申请独立证书;www.example.com没有业务服务,只需要做一个301跳转到根域,redir指令处理这个场景。Caddy会自动把http://请求重定向到https://,这是内置行为,不需要额外配置;redir只负责域名层面的跳转。

如果你还需要压缩、静态文件服务、路径改写等功能,Caddyfile里都有对应指令。比如给响应加压缩头:

example.com { encode gzip zstd reverse_proxy app:8080 }

这些指令按需添加即可。现在先把最基本的功能跑起来,跑通之后再做定制也不迟。

3.3 一键启动、验证证书与强制跳转

配置写好后,在项目目录执行:

docker compose up -d

第一次启动时,Caddy会经历这样的流程:读取Caddyfile、解析出域名、检查/data里有没有对应证书、如果没有则发起ACME申请、等待CA验证、拿到证书后加载并开始提供HTTPS服务。

查看Caddy的日志,确认证书申请过程:

docker compose logs -f caddy

正常情况下你会看到类似“certificate obtained successfully”的日志,有时还会显示证书有效期和续期计划。如果日志里出现acme: error相关字样,优先检查域名解析、端口放行,具体排查见第4节。

验证证书是否真的生效,直接在浏览器里访问https://example.com看地址栏小锁即可,也可以命令行验证:

openssl s_client -connect example.com:443 -servername example.com

命令会输出证书链信息,能看到证书的签发者、有效期、域名是否匹配。看到有效期接近90天是正常的,因为公共CA签发的证书默认就是这个周期,Caddy会在到期前自动续期。如果你修改了Caddyfile,用以下命令校验并热重载:

docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

validate先检查语法,没问题再reload。这是我在实战中养成的习惯,避免直接在运行中的实例上引入配置错误。up -d仅用于首次启动,日常改配置用reload,不会中断现有连接。

4. 实战中摸爬滚打:常见问题与排查技巧

4.1 证书申请失败的典型原因

证书申请失败是出现频率最高的问题,尤其是第一次搭建时。我整理了一个速查表:

现象可能原因排查命令/工具
日志出现403错误域名没有解析到服务器IP,或解析未生效dig example.com、nslookup example.com
日志出现connection refused宿主机防火墙或云安全组没放行80/443ss -lntp查看监听端口,登录云控制台检查安全组
一直显示临时证书申请过程反复失败或被拦截全量查看Caddy日志,确认80/443入站是否可达
日志出现rate limit短期内对同一域名的申请次数触发公共CA频率限制改用ACME staging环境测试,再切换回生产目录

排查的核心思路是验证“外部能否正常访问到Caddy的验证端口”。域名解析问题用dig就能看出端倪;端口放行问题可以临时用一个外部连通性工具,或者干脆从另一台机器用telnet 域名 80试一下。多数情况下,403都是因为域名解析没生效,而connection refused基本指向安全组。

还有一个容易被忽视的点:申请证书时会用到ACME账号,如果同一台服务器上跑着多个Caddy实例,或者你之前用Certbot申请过同一域名,不同工具之间可能存在验证冲突。单机场景下保持一个Caddy实例统一管理所有站点,是最省心的方式。

4.2 反向代理连不上后端容器的排查

另一个高频问题是:证书申请成功了,HTTPS能打开,但Caddy转发到后端时返回502。

先看Caddy日志:

docker compose logs caddy --tail=50

如果看到类似dial tcp: lookup app: no such host的错误,说明Caddy无法解析名为app的服务。常见原因有两个。

一是业务容器和Caddy不在同一个Compose网络里。检查一下compose文件里是不是给两个服务都写了networks: [web],少写一边都会导致网络隔离。可以用docker network inspect web查看网络里的容器列表。

二是业务容器监听的端口不是写进去的那个端口。比如你的应用在容器内监听的是3000,Caddyfile里写的却是app:8080,这就连不上了。确认端口最直接的方法是进入Caddy容器测试:

docker compose exec caddy wget -qO- http://app:8080

能返回内容说明网络和端口都没问题。如果提示无法解析主机名,先检查网络;如果解析成功但连接被拒绝,检查业务容器内的实际监听端口。还有一种情况是业务容器启动比较慢,Caddy启动时还没就绪,一般等几秒再访问就能恢复。

4.3 测试环境与生产环境切换的注意事项

开发调试时不想频繁消耗公共CA的申请配额,可以给Caddy切换到ACME staging测试环境。在Compose的environment里加一个变量指向公共CA提供的staging测试地址:

environment: - ACME_AGREE=true - ACME_CA_DIRECTORY=https://acme-staging-...

staging环境签发的证书不会被浏览器信任,所以它只适合验证申请流程能否跑通。调试完成,确认没问题后,删掉这个环境变量,重启Caddy,它会重新走生产CA申请流程,得到可信任的正式证书。

切换环境的操作要点是把caddy_data卷里的staging证书清除。因为Caddy发现已经有对应证书就不会重新申请。你可以先docker compose down,再删除caddy_data卷(注意这会同时清掉正式证书,所以只在开发阶段这么做)。更稳妥的做法是给正常环境做卷的快照备份,出了问题直接恢复。

还有一个小细节:邮箱地址尽量保持稳定。ACME账号是基于邮箱和私钥的,如果你每次重新部署都换一个新邮箱,就可能产生多个ACME账号,虽然一般不影响申请,但维护上不如固定一个邮箱干净。

4.4 几个我踩过的小坑

这些坑不一定让你完全跑不通,但碰到时会浪费不少时间。

第一个坑是挂载Caddyfile的格式。Windows下编写Caddyfile如果用了CRLF换行,Caddy解析偶尔会报错。建议在Linux服务器上直接用编辑器创建文件,或者在Windows侧保存为LF。更稳妥的做法是写完用caddy validate先验证一遍。

第二个坑是容器重建时把证书卷删了。有人为了“清理环境”,执行docker compose down -v,这个-v会把所有命名卷一起删除,对应的证书也跟着没了。生产环境里千万别带-v,除非你有备份。我自己的习惯是给关键卷手动做快照,避免手滑。

第三个坑是Caddy镜像版本升级导致配置不兼容。Caddy 2的Caddyfile语法在不同小版本间一般兼容,但如果你从很老的版本直接跳升,建议先看升级文档,不要盲目docker compose pull。尤其涉及全局配置块写法时,老配置极有可能起不来。

第四,注意不要把redir和reverse_proxy写反。比如你想跳转到另一个域名,却用了reverse_proxy,请求就会被后端服务处理,而不是301跳转。这个错误在语法上完全合法,Caddy不会报错,只是行为不符合预期。遇到这类“配置合理但效果不对”的情况,先想想是不是把语义相近的指令搞混了。

写到最后,分享一点我自己的体会。这套“Caddy加Docker Compose”的组合,我用它跑过多个内部工具站点和演示项目。最大的感受是,它把HTTPS从一个需要持续维护的状态,变成了一个启动时就自动解决的基础能力。以前用Nginx加Certbot,每隔一段时间就要惦记证书续期;换到Caddy之后,我几乎可以忘记证书的存在,这种把有状态的事变成无状态体验的转变,正是单机运维里最舒服的地方。

最后再分享一个自己在用的习惯:每次改Caddyfile之前,先做的不是改文件,而是用caddy validate校验即将写入的配置,再执行reload;更换Caddy镜像版本时,提前备份caddy_data卷。这两步看起来简单,但能帮你避开大多数让人头疼的瞬间。如果你后面想继续扩展,可以在这个方案上加入多站点日志隔离、Caddy内置的Basic Auth,以及配合CDN证书源的调整,整个体系会更灵活。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询