首页 / 资讯中心 / 文章详情

Caddy与Docker Compose组合实现单机HTTPS自动部署与证书续期

Caddy与Docker Compose组合实现单机HTTPS自动部署与证书续期 ★ FEATURED ARTICLE
手上一台单机服务器想给业务上HTTPS又不想手动配Nginx证书、写续期脚本。我把Caddy和Docker Compose这套组合用了一段时间可以说它已经成了我所有轻量级服务的标配。Caddy负责自动申请证书、自动续期、自动配置HTTPSDocker Compose负责把反向代理和业务服务编排在一起一条docker compose up -d就能让整站从HTTP变成HTTPS。这篇文章适合三类人刚接触服务器部署的新手、被Nginx加Certbot方案折腾过的运维同学以及想在单机上快速跑多个HTTPS站点的人。整套方案不复杂但坑不少下面我把设计思路、完整配置和常见问题一次性讲清楚。1. 项目拆解为什么是CaddyDocker 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验证。这个容错能力很实用比如某些网络环境只放行了443Caddy也能工作。但为了稳妥我还是建议公网上把80和443都打开因为HTTP-01是最通用、兼容性最好的验证方式。这里有一个容易踩的误区很多人以为只要配置了443Caddy就能申请证书然后嫌弃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_AGREEtrue 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_AGREEtrue这个环境变量表示同意ACME服务条款避免容器启动时出现交互式确认。新版Caddy里如果全局配置写了email通常会自动视为同意条款但加上这个变量更保险网络驱动用默认的bridge不需要特殊配置。Compose会自动为web网络分配子网并让容器之间通过服务名互相解析。3.2 Caddyfile的站点配置与反向代理规则在项目目录下新建Caddyfile内容先写一个全局块和一个站点块{ email your-emailexample.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/Caddyfilevalidate先检查语法没问题再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 --tail50如果看到类似dial tcp: lookup app: no such host的错误说明Caddy无法解析名为app的服务。常见原因有两个。一是业务容器和Caddy不在同一个Compose网络里。检查一下compose文件里是不是给两个服务都写了networks: [web]少写一边都会导致网络隔离。可以用docker network inspect web查看网络里的容器列表。二是业务容器监听的端口不是写进去的那个端口。比如你的应用在容器内监听的是3000Caddyfile里写的却是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_AGREEtrue - ACME_CA_DIRECTORYhttps://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证书源的调整整个体系会更灵活。
阅读完成 · 觉得有帮助?
咨询建站