Docker部署certbot与nginx来获取ssl证书添加https及自动更新
起因
原先一直有一个腾讯云的1核和1G的服务器在闲着,只是偶尔用下
frp来映射开发演示,这次想再利用下,于是试下搭建bitwarden,转而找到更加小巧的vaultwarden,但实际浏览器插件测试必须是要https才行,所以必须要有域名及证书,顺便就想试下certbot来自动续签。域名随便在阿里云上弄了个
top的,10年¥189,要想解析到服务器是要备案的,所以又在腾讯云上备案,大约有7、8个工作日吧,备案才通过,此时可以正常访问了,然后还要30日内进行公安备案。由于服务器上的
nginx、frp、vaultwarden都是docker来部署的,那certbot也顺便就用docker,官方虽然提供了docker镜像,但实际上并不是很推荐使用,因为容器内比较难触发外部的一些操作,会导致一些自动配置的功能很难使用,参考官方文档:Running with Docker。没实际安装前个人也觉得用docker更方便,但实际操作完成后会发现还是官网说的对,没特殊情况不要用docker来做这些。
certbot
发放证书前需要验证域名的合法性,确保你申请证书的域名是你的,验证方式主要分两种:
通过http请求验证,会向
http://你的域名/.well-known/acme-challenge/发送一条请求。通过dns解析,需要在域名解析中增加一条
_acme-challenge.你的域名的TXT记录用于验证。
certbot的命令有很多种不同的参数,这些参数也主要是为了自动生成http验证文件或自动添加dns解析等功能,主要的大体说下:
--manual,手动配置,你可以自己选择http验证还是dns验证都行,需要自己纯手工去配置http验证文件或是dns解析,个人没有做测试。--nginx,走http验证,如果不用docker且用nginx感觉是最方便的,会自动修改nginx的配置文件,增加http请求的访问配置及后续的ssl配置,但由于我们certbot是在docker容器内,所以要修改另一个nginx容器的配置文件及控制启停不太好实现,所以不采用这种方式。查资料时发现有人会将nginx和certbot通过dockerfile打包到一个镜像内,这样此命令就比较方便的执行了,感觉是个好办法,但个人没有测试了解。--dns-xxx,走dns解析验证,这个也很方便,配置好后会自动在域名解析中增加一条记录进行域名验证,但官方目前只支持国外的一些dns服务商,国内的阿里云、dnspod等貌似也有第三方的插件,可以自己去github上找下。如果想申请通配符泛域名的,则只能通过此方式,目前个人没有测试。--standalone,走http验证,这种方式你就算是没有nginx等服务,仍然可以验证域名并获取证书,它会虚拟一个服务来进行验证,但需要保证你的80端口别被占用,由于我这边有nginx了,所以便没有用此类方法测试。--webroot,走http验证,已有类似nginx等服务的情况下可以用此方法,会自动在你配置的目录下生成http验证文件,与standalone相比好处是不需要预留80端口,也就是原有的网站不需要停止就可以申请证书,而缺点就是你还需要在nginx等服务中手动配置/.well-known/acme-challenge/使其对应到自动生成的验证文件上。后续我们用的就是这种。
通过webroot的方式运行后,会在两个地方产生文件:
一个是在命令行配置的
--webroot-path指定目录下产生临时文件,用于http验证。一个是验证成功会产生存放证书的文件,默认是在
/etc/letsencrypt/live/你的域名目录下,需要注意的是这些证书是个软链接,对应着../archive下,所以我们在做volume映射时不要只映射到live这个目录,而是要映射/etc/letsencrypt这个目录,否则无法找到相关的证书文件。
以上两个文件虽然是certbot容器生成的,但我们nginx容器都会用到,所以我们需要用宿主机做为中转,在certbot和nginx容器中都配置映射到宿主机的同一个目录,从而实现certbot容器和nginx容器共用文件。我的服务器/data/https-certbot下的目录可参照:
├── conf
│ ├── live //存放证书
│ └── archive //存放http验证的临时文件
├── nginx
│ ├── conf
│ │ └── conf.d //配置文件
│ │ └── default.conf //配置文件
├── www // 存放网站web前端服务目录
└── docker-compose.yml //docker-compse文件准备
有个服务器
有个已经备案且解析到服务器上的域名
安装
docker和docker compose,照着官网来就行,很详细:Docker Documentation,docker如果下载镜像慢,可以注册个阿里云账号,然后在【容器镜像服务】中找到【镜像工具】下的【镜像加速器】中按照说明来配置。docker镜像会用到:
开始
主要是把
volume映射弄好,顺便配置好nginx的相关http验证:docker-compose.yml:
version: "3.8" services: nginx: image: nginx:alpine container_name: nginx restart: unless-stopped network_mode: host #ports: # - "80:80" # - "443:443" volumes: - ./nginx/conf.d:/etc/nginx/conf.d - ./www:/usr/share/certbot/www - ./conf:/etc/letsencrypt depends_on: - certbot certbot: image: certbot/certbot container_name: certbot volumes: - ./www:/usr/share/certbot/www - ./conf:/etc/letsencrypt entrypoint: > sh -c " trap exit TERM; while :; do certbot renew --webroot -w /usr/share/certbot/www; sleep 12h; done "nginx配置default.conf:
server { listen 80; server_name www.xxkk.top; location /.well-known/acme-challenge/ { root /usr/share/certbot/www; default_type "text/plain"; try_files $uri =404; } location / { return 200 "Nginx is running.\n"; } } server { listen 443 ssl; server_name www.xxkk.top; ssl_certificate /etc/letsencrypt/live/www.xxkk.top/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/www.xxkk.top/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'HIGH:!aNULL:!MD5'; location /.well-known/acme-challenge/ { root /usr/share/certbot/www; default_type "text/plain"; try_files $uri =404; } root /usr/share/certbot/www; index index.html; location / { try_files $uri $uri/ /index.html; } # 支付宝 API 代理 location /aliPay/ { proxy_pass https://2dshuziren2.xxkk.top/aliPay/; proxy_set_header Host 2dshuziren2.xxkk.top; 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_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } # 微信支付 API 代理 location /wxPay/ { proxy_pass https://2dshuziren2.xxkk.top/wxPay/; proxy_set_header Host 2dshuziren2.xxkk.top; 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_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; } # Gzip 压缩 gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json; } server { listen 80; server_name doc.xxkk.top; location /.well-known/acme-challenge/ { root /usr/share/certbot/www; default_type "text/plain"; try_files $uri =404; } location / { return 301 https://$server_name$request_uri; } } upstream doc { server 127.0.0.1:8090; } server { listen 443 ssl http2; ssl_certificate /etc/letsencrypt/live/doc.xxkk.top/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/doc.xxkk.top/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; server_name doc.xxkk.top; client_max_body_size 1024m; location / { proxy_pass http://doc; proxy_set_header HOST $host; proxy_read_timeout 600; proxy_send_timeout 600; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } server { listen 80; server_name 2dshuziren2.xxkk.top; location /.well-known/acme-challenge/ { root /usr/share/certbot/www; default_type "text/plain"; try_files $uri =404; } location / { return 301 https://$server_name$request_uri; } } upstream 2dshuziren2 { server 127.0.0.1:8088; } server { listen 443 ssl http2; ssl_certificate /etc/letsencrypt/live/2dshuziren2.xxkk.top/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/2dshuziren2.xxkk.top/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; server_name 2dshuziren2.xxkk.top; client_max_body_size 1024m; location / { proxy_pass http://2dshuziren2; proxy_set_header HOST $host; proxy_read_timeout 600; proxy_send_timeout 600; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } upstream gptweb { server 127.0.0.1:8000; } server { listen 443 ssl http2; ssl_certificate /etc/letsencrypt/live/gptweb.xxkk.top/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/gptweb.xxkk.top/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; server_name gptweb.xxkk.top; client_max_body_size 1024m; location / { proxy_pass http://gptweb; proxy_set_header HOST $host; proxy_read_timeout 600; proxy_send_timeout 600; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } upstream buy { server 127.0.0.1:8001; } server { listen 443 ssl http2; ssl_certificate /etc/letsencrypt/live/buy.xxkk.top/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/buy.xxkk.top/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; server_name buy.xxkk.top; client_max_body_size 1024m; location / { proxy_pass http://buy; proxy_set_header HOST $host; proxy_read_timeout 600; proxy_send_timeout 600; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
上方的volumes映射串联关系如下,通过宿主机来实现
certbot和nginx中相关的目录实际上是同一个目录,除了certbot中的/etc/letsencrypt是固定的外,其余的目录都是可以随便找目录放,只要对应起来就行。mkdir -p /data/https-certbot/nginx/conf.d \ /data/https-certbot/www/.well-known/acme-challenge \ /data/https-certbot/conf cd /data/https-certbot/ docker-compose down docker-compose up -d先把nginx开启,然后测试证书发放是否可以:
# --dry-run是只测试不实际生成; --webroot-path对应着certbot内的http验证目录;-d后面是域名;--rm是运行后接着删除,certbot容器不需要一直开启,只是启动下生成证书即可 echo test > /data/https-certbot/www/.well-known/acme-challenge/test.txt curl http://www.xxkk.top/.well-known/acme-challenge/test.txt输入邮箱等信息,如果提示
The dry run was successful则说明成功,可以去掉--dry-run参数来进行实际的获取证书docker run --rm \ -v /data/https-certbot/www:/usr/share/certbot/www \ -v /data/https-certbot/conf:/etc/letsencrypt \ certbot/certbot certonly \ --webroot \ --webroot-path /usr/share/certbot/www \ -d www.xxkk.top \ --non-interactive \ --agree-tos \ -m admin@xxkk.top如果上方正常,则会在
certbot容器下的/etc/letsencrypt下也就是nginx容器下的/usr/share/certbot/ssl下产生证书,此时,只需要配置nginx配置ssl即可,还是在文件default.conf中:server { listen 80; server_name www.xxkk.top; location /.well-known/acme-challenge/ { root /usr/share/certbot/www; default_type "text/plain"; try_files $uri =404; } location / { return 200 "Nginx is running.\n"; } } server { listen 443 ssl; server_name www.xxkk.top; ssl_certificate /etc/letsencrypt/live/www.xxkk.top/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/www.xxkk.top/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'HIGH:!aNULL:!MD5'; location /.well-known/acme-challenge/ { root /usr/share/certbot/www; default_type "text/plain"; try_files $uri =404; } root /usr/share/certbot/www; index index.html; location / { try_files $uri $uri/ /index.html; } # 支付宝 API 代理 location /aliPay/ { proxy_pass https://2dshuziren2.xxkk.top/aliPay/; proxy_set_header Host 2dshuziren2.xxkk.top; 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_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } # 微信支付 API 代理 location /wxPay/ { proxy_pass https://2dshuziren2.xxkk.top/wxPay/; proxy_set_header Host 2dshuziren2.xxkk.top; 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_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; } # Gzip 压缩 gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json; } 以上
docker compose exec nginx nginx -t检测配置nginx文件是否有问题,docker compose exec nginx nginx -s reload或者是直接docker compose restart nginx来重启nginx正常启动的话应该就没问题了,我们接着需要做定期更新证书,主要就是为了这个,否则直接腾讯云、阿里云申请一年的免费证书好了。增加计划任务,
sudo crontab -e打开定时任务编辑,每月1号和15号运行一次#1、切换到docker-compose.yml所在目录;2、然后更新证书,只有距离过期时间30天内才会真正成功;3、然后重载nginx使新证书生效 0 0 1,15 * * cd /data/https-certbot && docker-compose run --rm certbot renew && docker-compose exec nginx nginx -s reload #上方命令测试可以获取新证书,但是reload后实际测试新证书没有生效,可以换成下方的直接重启nginx试试,网上有人说reload可以,有人说必须restart,我这边的情况是第一次没自动生效,手动执行了reload后刷新浏览器可以了,但第二次同样没自动生效,手动reload也不行,restart后可以了,所以就很怪。而且都没自动生效,怀疑nginx的操作并没有等待renew执行完成后,可能在renew时间长在nginx重启之后才执行完成了。总之目前还没结论,所以这里可能还是不应该用docker,而应该用原生的钩子来触发比较好? 0 0 1,15 * * cd /data/https-certbot && docker-compose run --rm certbot renew && docker-compose restart nginx以上是计划任务相关。
certbot容器中renew虽然有--deploy-hook等实际更新后触发钩子的功能,但由于和nginx不在同一个容器,因此无法触发nginx重启,所以上述命令不管是否证书真正更新成功nginx是一定会重启的,最好放到深夜执行。搜索了下从一个容器中控制另一个容器的操作,一种是本容器内装docker,然后映射宿主机内的docker.sock,这样容器内操作docker就和宿主机内操作一致了;还有一种就是ssh来连接另一个容器来操作了,这两种都挺麻烦的,这里就没必要也不做尝试了。
其它
certbot申请证书是有次数限制的,重复申请5次以上,再次请求会报too many certificates already issued...等错误,会停用一周后才能再继续,此时只能再换个域名或者是等一周了,参考速率限制。除官网外的参考文档:HTTPS using Nginx and Let's encrypt in Docker,写的很详细。