公司官网跑了三年,一直是用 Docker 里的 nginx 镜像来承载,但访问方式还停留在最原始的 http。客户那边反馈得最多的一句话就是:你们官网打开的时候浏览器提示“不安全”。销售团队压力大,这个需求最终转到我这边,要求就一句话:把官网迁到 HTTPS,别影响线上访问。
我起初以为这就是几行配置的事——申请证书、改一下 nginx 配置、reload 容器,收工。实际上手才发现,容器环境下的 HTTPS 部署和裸机装 nginx 完全是两种思路,证书怎么放、要不要重启、前端路由刷新为什么 404、安全组放行后为什么本地还是连不上,这些坑一个接一个。这篇文章不是照搬官方文档的流水账,而是把这次上线过程里真正值得注意的设计和排障思路完整梳理一遍,给正在做同样事情的人做个参考。
1. 官网这种轻量站点,为什么要塞进 Docker 里
1.1 换机器不重来,这才是容器化的核心收益
很多公司官网其实就是一个静态站:前端打包完的 html、css、js 文件扔到服务器上,由 nginx 指个目录。传统做法是直接在宿主机上apt install nginx,把配置文件丢到/etc/nginx/conf.d/里,然后手动重启服务。这套流程能跑,但有个隐患:一旦以后要换服务器、加一台备用机,或者把环境复制给另一个项目组,所有依赖都得重新装一遍。我今天在这个机器上改了什么配置、装了哪个模块,没人记得清楚。
Docker 解决的就是这件事。镜像把 nginx 的运行时环境固定住了,配置和静态文件通过挂载的方式进容器,整个服务从“一台服务器的状态”变成了“一组文本文件的状态”。换机器时只需要把项目目录原样拷过去,docker compose up -d,新的环境就起来了。这种可复现性对没有人专职维护基础设施的小团队来说,价值比性能参数重要得多。
这里补充一个个人观点:如果官网跑在一些可视化控制面板上,那确实也能配置 HTTPS,但控制面板生成的配置和证书路径往往被平台私有化,一旦要迁移或者换部署方式,处理起来比手工维护的 compose 文件麻烦不少。所以我更倾向于用 Docker + 自己维护的配置目录来做,原因就一个:以后接手的人能看懂,能接手。
1.2 镜像选型:nginx:stable-alpine 是大多数情况的合理起点
官方 nginx 镜像有两个常见标签,nginx:stable和nginx:stable-alpine,区别在于底层操作系统不同。前者基于 Debian,后者基于 Alpine Linux。Alpine 的镜像体积小得多,大概只有 20 多 MB,启动也更快,而且 nginx 本身不依赖复杂的动态链接库,Alpine 环境完全够用。
有人担心 Alpine 的 musl libc 会有兼容问题,但在 nginx 这种官方镜像的范围内基本不存在。官方镜像里的 nginx 二进制是专门针对该基础镜像构建的,不是用户自己编译的那种野路子产物,所以稳定性是有保障的。如果你的官网有特殊模块需求,比如编译了第三方流媒体模块,那才需要考虑换其他基础镜像;纯粹做静态站和反向代理,nginx:stable-alpine就够。
1.3 目录规划:什么进镜像,什么用挂载
容器设计里有句老话:镜像里放不变的,挂载放要变的。官网场景下我建议这样划分:
company-site/ ├── docker-compose.yml # 编排文件 ├── html/ # 前端打包产物,可以整个替换 ├── conf.d/ # nginx 的站点配置 └── ssl/ # 证书文件目录html、conf.d、ssl三个目录全部以只读方式挂载进容器。这样做的好处是:更新官网版本时,把新的静态文件传到html/里,然后执行一次 reload 就能生效,不需要重新构建镜像;证书续期也一样,替换文件后 reload 即可。容器本体始终不需要动,保持了稳定。
有些教程喜欢把静态文件和证书在构建镜像时用COPY打进去,这么做的坏处是每次更新都必须重打镜像、重推镜像、再重建容器,轻量站点的运维成本反而变高了。只读挂载的方式对官网这类场景明显更合理。
2. HTTPS 成不成的第一关:证书这条线怎么安排
2.1 证书来源三选一,别上来就整自动化
证书获取有几种途径,适合的场景不太一样,我按推荐程度排一下:
- 云厂商免费证书:域名在哪个平台买的,就去对应平台的证书服务里申请。一般提供一年或三个月的免费证书,申请后可以下载 nginx 格式的证书文件。对不折腾的小公司来说,这是最轻量的方案,缺点是证书到期后要记得重新申请和替换。
- 自动化签发的免费证书:通过 ACME 协议自动申请和续期的免费证书,可以做到证书到期前自动更新。它避免了手动续期遗漏导致的访问事故,但需要一个运行 ACME 客户端的组件,以及域名解析配合。如果团队有能力维护,这个方案体验最好。
- 自签名证书:只适合本地联调,浏览器会直接拦截,不适合对外发布,我这里就不展开了。
云厂商免费证书最大的问题是到期时间不长,而且不能自动续期。我见过不少站点因为证书过期导致浏览器直接拒绝访问,用户还以为是网站被攻击了。建议不管选哪种,都在日历里设置一个提前两周的提醒,或者干脆接受自动化方案,把证书续期变成无人值守的流程。
2.2 证书文件与容器目录的落位方式
证书下载回来通常是两个文件:一个是证书链文件(包含域名证书和中间证书),一个是私钥文件。以 nginx 格式为例,对应的是fullchain.pem和privkey.pem。
在宿主机上建议这样放:
company-site/ssl/ ├── fullchain.pem └── privkey.pem然后在 compose 文件里把整个ssl目录挂载到/etc/nginx/ssl,nginx 配置直接引用容器内的路径。这里有一个关键点:证书文件所在目录及文件本身的权限要让容器能读到。私有密钥建议保持 600 或 640 权限,证书链文件可以 644。
提示:修改证书后一定不要忘记执行 reload。nginx 不会自动感知挂载目录里文件的变化,这和使用宿主机 nginx 时是一样的道理。
2.3 续期时是 reload 还是 restart
很多人一谈到更新配置或更新证书,第一反应是docker compose restart。在容器场景里,重启会触发容器停止、再启动,期间会有一小段不可用窗口,虽然通常只有一到两秒,但对官网这种对外服务来说,完全没必要冒这个风险。
正确的做法是给容器内的 nginx 进程发送重载信号:
docker exec company-web nginx -s reload这个命令会让 nginx 重新读取配置文件并加载最新的证书文件,而不会中断当前连接,正在访问官网的用户完全无感。我在实际维护中已经养成了一个习惯:凡是改配置文件或替换证书文件,一律先nginx -t校验一遍,再 reload;绝对不在配置有语法错误的情况下强行 reload,那样会导致 nginx 拒绝重载,服务继续跑旧配置,问题会被延迟到下一次重启才暴露。
3. compose 与 Nginx 配置:边界划分最容易被忽视
3.1 docker-compose.yml 的字段逐个说清楚
先给一个可以直接参考的编排文件:
services: web: image: nginx:stable-alpine container_name: company-web restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./html:/usr/share/nginx/html:ro - ./conf.d:/etc/nginx/conf.d:ro - ./ssl:/etc/nginx/ssl:ro logging: driver: json-file options: max-size: "10m" max-file: "3"几个值得注意的字段:
restart: unless-stopped:容器异常退出后会自动拉起,但手动停止则不会强制拉起。官网这种服务没有运行状态依赖,用这个策略很合适。ports:把宿主机 80 和 443 都映射到容器里。如果服务器上还跑了其他 Web 服务,要注意端口占用冲突,一般官网就是独立的整台机器。volumes的:ro:只读挂载能避免容器在运行中意外修改配置或证书文件。logging:限制日志文件大小和数量,防止官网长期运行后日志把磁盘占满。这是个很多人忽略的细节,我接手过的站点里,因此磁盘满导致服务不可用的不在少数。
如果你的官网还有独立的 API 服务,建议在同一个 compose 项目里再定义一个后端服务,nginx 通过容器网络访问后端。这样域名和端口的管理都集中在这一个文件里,比让 nginx 直接代理宿主机上某个动态端口要规范得多。
3.2 nginx 配置:强制跳转、TLS 参数和前端路由
conf.d/目录下的默认配置需要清掉或者显式写自己的站点配置。官方镜像自带的default.conf会占用 80 端口,不处理的话会出现配置冲突。我习惯直接把default.conf删除,换成自己的文件,比如www.conf:
server { listen 80; server_name www.example.com example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name www.example.com example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } gzip on; gzip_types text/css application/javascript application/json image/svg+xml; gzip_min_length 1024; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; }这里几个细节展开说说:
- 80 端口直接 301 跳转:用户输域名不带 www 或者直接敲 http 时,都能统一到 HTTPS 上,避免出现“有的页面能打开、有的跳不过去”的割裂感。
ssl_prefer_server_ciphers off:现代浏览器和 TLS 1.3 已经不太需要服务端强行指定密码套件顺序,关掉反而能减少一些老旧设备和开发工具联调时的兼容问题。try_files那行:这是给前端路由用的。官网如果基于 Vue 或 React 并采用 history 路由模式,刷新某个子页面时 nginx 找不到对应的物理文件,必须回退到index.html,由前端路由接管。没有这一行,刷新子页面就是一片 404。- HSTS 头:
Strict-Transport-Security告诉浏览器在一段时间内强制使用 HTTPS 访问,避免中间人把请求降级成 HTTP。设置之前要确保 HTTPS 已经稳定可用,否则会导致该域名在一段时间内无法通过 HTTP 访问。
3.3 如果官网里还有 API 反向代理
不是所有“公司官网”都是纯静态页面。很多官网的在线留言、表单提交、甚至产品体验页,需要向后端服务发请求。容器编排里建议把后端服务也写进同一个 compose 文件,然后用服务名代理,例如:
location /api/ { proxy_pass http://backend:8080/; 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; }proxy_pass里写的backend是 compose 中后端服务的服务名,Docker 自带的 DNS 会解析到对应容器。X-Forwarded-Proto这个头很关键,后端程序要通过它来判断用户用的是 HTTPS 还是 HTTP,否则重定向逻辑会生成错误的 http 链接。
4. 从容器启动到线上 HTTPS:一整套上线动作
4.1 上线前的准备动作
我这里默认已经把前端构建产物准备好了。上线的完整流程是这样的:
- 创建项目目录,把 compose 文件、
html静态目录、conf.d配置、ssl证书目录准备好。 - 用
docker compose config校验编排文件语法是否正确,这个命令不会启动容器,但能发现大部分编排配置错误。 - 拉取镜像并启动容器:
docker pull nginx:stable-alpine docker compose up -d- 确认容器状态:
docker compose ps状态是Up不代表 HTTPS 就成功了,先看日志确认 nginx 有没有报错:
docker logs --tail 50 company-web如果证书路径配错或者端口冲突,nginx 会在日志里直接把错误打出来。这一步比直接 curl 域名更能快速定位问题。
4.2 验证 HTTPS 是否真的生效
上线前如果域名的 DNS 已经指向服务器,可以直接验证。如果还在切换 DNS,可以在本机临时改 hosts 文件来提前验证,避免线上出现意外。
最基本的验证是看响应头:
curl -I https://www.example.com正常响应会返回HTTP/2 200,并且包含 HSTS 头。如果返回证书相关错误,用下面的命令查看证书链是否完整:
openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates这条命令能显示证书的颁发者和有效期,很方便确认新证书是否已经生效。重点检查subject里的域名是否与访问域名一致,以及有效期是否已经在服务端生效。
还需要确认 HTTP 到 HTTPS 的跳转是否正常:
curl -I http://www.example.com正常返回应该是301 Moved Permanently,Location指向 HTTPS 地址。
4.3 回滚思路:先留后路再上 HTTPS
首次启用 HTTPS 时,最稳妥的做法是把原 HTTP 配置完整保留一份,新配置只做“追加”而不是“覆盖”。用我的文件结构来说,就是把旧配置文件备份为www.conf.bak,然后把新配置上线。万一 HTTPS 出现问题,执行两条命令就能回到原状态:
mv conf.d/www.conf conf.d/www.conf.https mv conf.d/www.conf.bak conf.d/www.conf docker exec company-web nginx -s reload整个过程不重建容器,不重启服务,只需要 nginx 重载配置。这也是我为什么一直强调:配置尽量用挂载而不是打进镜像,留后路会方便很多。
4.4 云安全组和服务器防火墙要同步检查
有时候容器起来了,本地 curl 却超时,而 80 端口是正常的。这个现象一般是云平台的安全组规则只放行了 80,忘了加 443。登录云控制台,找到这台服务器关联的安全组,添加入站规则,放行 TCP 443 端口和 80 端口。
服务器本机如果还启用了 iptables,也要检查:
iptables -L -n | grep 443本地防火墙和云安全组需要同时放行,任何一个环节没放开,外部都无法访问。注意连接超时和连接拒绝的区别:超时大概率是安全组或防火墙没放行,拒绝则多半是端口没有被进程监听。
5. 上线首月排障实录:证书、路由、安全组的三连坑
5.1 “页面正常但浏览器报证书无效”
上线第二天,同事反馈公司内部一台电脑访问官网时提示证书无效,但手机用流量访问却是正常的。这种“部分设备有问题”的现象,我第一反应不是证书本身,而是那台电脑的系统时间错了。证书有效性依赖系统当前时间和证书有效期做比对,如果系统时间超前或落后,浏览器会认为证书还没生效或已经过期。
让同事校准系统时间后再访问,问题消失。这类情况在办公局域网里很常见,因为部分终端没有配置时间同步服务器。如果证书本身的问题,通常所有设备都会报错,不会是孤例。
另一个值得排查的是证书链不完整。有些证书下载包里有多个文件,分别对应域名证书、中间证书、根证书。nginx 需要的是把域名证书和中间证书拼在一起的fullchain.pem,如果只填了域名证书,部分浏览器会因为找不到中间证书而报“不受信任”。替换成完整证书链文件,reload 后即可解决。
5.2 换证书后访问正常,老设备却频繁掉线
这其实是“会话缓存”造成的假象。Nginx 默认会在内存中缓存 SSL 会话参数,客户端和服务器之间已经建立过的 TLS 会话会被复用,即使证书已经替换,某些保持长连接的老客户端在会话过期前仍然沿用旧参数握手。正常情况下过一段时间就会自动消失,不需要额外处理。
如果想在证书替换后让旧的会话立刻失效,可以重启容器,但这会带来短暂的不可用窗口。我的建议是:证书替换本身不要求所有客户端立即切换到新证书,reload 已经足够;只有发生密钥泄露这类紧急事件才需要强制重启。
5.3 前端路由刷新 404 的排查过程
官网切到 HTTPS 后,有人反馈从首页点进去的子页面一切正常,但直接刷新子页面地址就 404。原因是前端用了 History 路由,URL 路径不是真实存在的文件路径,nginx 默认会尝试按路径找文件,找不到就返回 404。
看这个问题时我先用curl访问子页面地址,确认服务端确实返回了 404,再检查 nginx 配置里location /块有没有try_files回退规则。加上try_files $uri $uri/ /index.html;后刷新即恢复正常。
5.4 TLS 检测报告说服务还在用 TLS 1.0/1.1
部分第三方检测工具报告站点支持旧版 TLS 协议,但这个结论可能不是因为配置里显式允许了旧协议,而是 nginx 版本较老时默认启用了这些协议。在当前配置里显式声明ssl_protocols TLSv1.2 TLSv1.3;之后,重新检测就会通过。
关于加密套件,我用的是系统默认集,没有手动写一大串ssl_ciphers。对官网这种以静态内容为主、访问量不大的场景,现代默认配置已经在性能和安全性之间做到了比较好的平衡,手动收紧套件列表的收益有限,反而可能引入兼容问题。
上线多年之后回头看
这次把官网迁到 HTTPS,前后用了不到半天,大部分时间都在等域名解析生效和证书审核。真正让我花时间去想的,其实是怎么让这个 HTTPS 在未来两三年内都不折腾——证书能稳定续期、配置改动能快速回滚、换服务器不至于从零开始。
我个人在维护这类轻量官网时的经验是:坚持把配置文件纳入版本管理,每次改动先nginx -t再 reload,证书替换后立刻用openssl s_client验证一遍,再让同事从外部网络访问确认。这三步做完,这个部署基本就是能长期稳定跑的状态了。