最近开发了一个小程序-智名宝典,主要针对即将成为父母的人能为宝宝起一个心仪的名字。在开发的过程中,经历了很多磨难,主要是小程序的域名备案,部署这块。于是,整理成一篇文章,方便以后查看。
首先,你要拥有一台服务器和一个域名,域名还要备案,同时小程序也要备案,前前后后搞了1个月,小程序备案目前还没有完成。此处主要记录证书申请和服务部署。
我的部署架构为:docker,所有的东西都是依赖于docker容器,比如:mysql,redis,nginx等这些。假如域名为:xxx.xyz,小程序后台多个:a.xxx.xyz,b.xxx.xyz,nginx也是单独的一个docker容器,则:宿主机装acme.sh,Nginx 放在 Docker 里,证书文件放宿主机目录,再挂载进 Nginx 容器。
对你当前域名:

  • 网站:xxx.xyz
  • 小程序后台 A:a.xxx.xyz
  • 小程序后台 B:b.xxx.xyz
    最简单稳定的方案是先申请一张多域名证书(SAN 证书),一次覆盖这 3 个域名。

一:DNS准备

在域名服务商DNS里至少保证下面 3 条记录都解析到你的服务器公网 IP:

1
2
3
xxx.xyz      A      你的服务器IP
a A 你的服务器IP
b A 你的服务器IP

也就是:

  • xxx.xyz -> 服务器IP
  • a.xxx.xyz -> 服务器IP
  • b.xxx.xyz -> 服务器IP
    先确认:
    1
    2
    3
    ping xxx.xyz
    ping a.xxx.xyz
    ping b.xxx.xyz

二:宿主机准备目录

下面我统一用 /opt/xxx 作为宿主机目录。

1
2
3
4
sudo mkdir -p /opt/xxx/nginx/conf.d
sudo mkdir -p /opt/xxx/nginx/ssl/xxx.xyz
sudo mkdir -p /opt/xxx/acme-challenge/.well-known/acme-challenge
sudo chown -R $USER:$USER /opt/xxx

目录用途:

  • /opt/xxx/nginx/conf.d:Nginx 配置
  • /opt/xxx/nginx/ssl/xxx.xyz:证书文件
  • /opt/xxx/acme-challenge:ACME 验证文件

三:安装acme.sh

在宿主机执行:

1
2
3
curl https://get.acme.sh | sh -s email=admin@xxx.xyz
source ~/.bashrc
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt

检查版本:

1
~/.acme.sh/acme.sh --version

四:Docker Compose 示例

假设现在有:

  • 网站容器:site
  • 小程序后台 A:miniapp-a
  • 小程序后台 B:miniapp-b
  • 网关 Nginx:gateway-nginx
    可以参考这个docker-compose.yml:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    version: '3.8'

    services:
    web:
    image: nginx:alpine # 使用 alpine 版本体积更小、更安全
    container_name: my_static_website
    restart: always # 服务器重启时自动启动
    ports:
    - "80:80" # 将宿主机的 80 端口映射到容器的 80 端口
    - "443:443"
    volumes:
    - ./html:/usr/share/nginx/html:ro # 挂载静态文件目录 (ro表示只读)
    - ./conf/default.conf:/etc/nginx/conf.d/default.conf:ro # 挂载 Nginx 配置文件
    - ./ssl:/etc/nginx/ssl:ro
    - ./acme-challenge:/var/www/acme:ro
    networks:
    - app-net

    networks:
    app-net:
    external: true

五:先只配 HTTP,用来申请证书

先不要急着上 443。
先写一个仅 HTTP 的配置文件:
文件:/opt/xxx/nginx/conf.d/00-http.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
server {
listen 80;
server_name xxx.xyz;

location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
default_type "text/plain";
}

location / {
proxy_pass http://site:3000;
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 http;
}
}

server {
listen 80;
server_name a.xxx.xyz;

location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
default_type "text/plain";
}

location / {
proxy_pass http://miniapp-a: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 http;
}
}

server {
listen 80;
server_name b.xxx.xyz;

location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
default_type "text/plain";
}

location / {
proxy_pass http://miniapp-b: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 http;
}
}

启动容器:

1
docker compose up -d

测试Nginx配置:

1
docker exec gateway-nginx nginx -t

如果没问题,重载:

1
docker exec gateway-nginx nginx -s reload

六:先测试 ACME 验证路径是否通

在宿主机创建一个测试文件:

1
echo test-ok > /opt/xxx/acme-challenge/.well-known/acme-challenge/test

分别访问:

1
2
3
curl http://xxx.xyz/.well-known/acme-challenge/test
curl http://a.xxx.xyz/.well-known/acme-challenge/test
curl http://b.xxx.xyz/.well-known/acme-challenge/test

如果都能返回:

1
test-ok

说明验证目录打通了,可以正式申请证书。

七:申请 Let’s Encrypt 证书

用 webroot 方式,一张证书覆盖 3 个域名:

1
2
3
4
5
6
~/.acme.sh/acme.sh --issue \
--server letsencrypt \
-d xxx.xyz \
-d a.xxx.xyz \
-d b.xxx.xyz \
--webroot /opt/xxx/acme-challenge

如果成功,会看到类似:

1
2
3
4
Your cert is in: ...
Your cert key is in: ...
The intermediate CA cert is in: ...
And the full chain certs is there: ...

八:把证书安装到固定目录

这一步很重要。
不要直接让 Nginx 用 ~/.acme.sh/ 里的原始文件,
而是用 –install-cert 输出到你自己的目录。

1
2
3
4
~/.acme.sh/acme.sh --install-cert -d xxx.xyz \
--key-file /opt/xxx/nginx/ssl/xxx.xyz/key.pem \
--fullchain-file /opt/xxx/nginx/ssl/xxx.xyz/fullchain.pem \
--reloadcmd "docker exec gateway-nginx nginx -s reload"

说明:

  • key.pem:私钥
  • fullchain.pem:完整证书链,Nginx 必须用它
  • reloadcmd:续期后自动重载 Nginx

九:切换为 HTTPS 配置

现在证书已经有了,把原来的 HTTP 配置换成 HTTPS 配置。

你可以删除原来的 00-http.conf,换成:

文件:/opt/xxx/nginx/conf.d/10-https.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

server {
listen 80;
server_name xxx.xyz a.xxx.xyz b.xxx.xyz;

location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
default_type "text/plain";
}

location / {
return 301 https://$host$request_uri;
}
}

server {
listen 443 ssl http2;
server_name xxx.xyz;

ssl_certificate /etc/nginx/ssl/xxx.xyz/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/xxx.xyz/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;

client_max_body_size 20m;

location / {
proxy_pass http://site:3000;
proxy_http_version 1.1;
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 https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}

server {
listen 443 ssl http2;
server_name a.xxx.xyz;

ssl_certificate /etc/nginx/ssl/xxx.xyz/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/xxx.xyz/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;

client_max_body_size 20m;

location / {
proxy_pass http://miniapp-a:8080;
proxy_http_version 1.1;
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 https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}

server {
listen 443 ssl http2;
server_name b.xxx.xyz;

ssl_certificate /etc/nginx/ssl/xxx.xyz/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/xxx.xyz/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;

client_max_body_size 20m;

location / {
proxy_pass http://miniapp-b:8080;
proxy_http_version 1.1;
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 https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}

然后检查并重载:

1
2
docker exec gateway-nginx nginx -t
docker exec gateway-nginx nginx -s reload

十:浏览器验证

测试:

  • https://xxx.xyz
  • https://a.xxx.xyz
  • https://b.xxx.xyz
    查看证书是否是 Let’s Encrypt,是否有效。
    也可以命令行检查:
    1
    2
    3
    openssl s_client -connect xxx.xyz:443 -servername xxx.xyz
    openssl s_client -connect a.xxx.xyz:443 -servername a.xxx.xyz
    openssl s_client -connect b.xxx.xyz:443 -servername b.xxx.xyz

十一:小程序侧要注意的事

微信小程序里,除了服务器证书正确,还要注意:
1)配置合法域名
到微信公众平台配置:

  • https://a.xxx.xyz
  • https://b.xxx.xyz
    如果上传、下载、websocket 也用这些域名,也要分别配上。
    2)必须用 HTTPS
    小程序请求不能用 HTTP。
    3)一定要用 fullchain.pem
    别只用单独的 cert.pem,否则有些客户端会报证书链不完整。
    4)TLS 版本
    Nginx 至少保留:
    1
    ssl_protocols TLSv1.2 TLSv1.3;

十二:自动续期

acme.sh 安装后一般已经自动加了 cron。
你可以看一下:

1
crontab -l | grep acme.sh

手工测试一次续期:

1
~/.acme.sh/acme.sh --renew -d xxx.xyz --force

成功后它会自动执行:

1
docker exec gateway-nginx nginx -s reload

附录

文件目录:

1
2
3
4
5
6
7
8
9
10
11
project/
├── docker-compose.yml
├── html/
│ └── index.html
├── conf/
│ └── default.conf
├── ssl/
│ └── xxx.xyz/
├── acme-challenge/
│ └── .well-known/
│ └── acme-challenge/