从零搭个人网站时绕不开的两件事:ICP 备案,以及让微信小程序能安全调用你的 API。记录一次完整走通的过程,以及踩过的坑。
给自己的项目搭了个个人网站,顺手把「ICP 备案」和「小程序用个人域名」这两件事完整走了一遍。
这两件事在技术教程里通常被讲得很轻,实际做起来卡点不少。记录下来,给同样在做独立项目的朋友参考。
先说结论:域名和服务器是两件独立的事。
我最终的结构:
个人主站 wanglipingresume.com → 静态文件 自研项目 projects.wanglipingresume.com → 静态文件 技术文章 writing.wanglipingresume.com → 静态文件 API 服务 api.wanglipingresume.com → 反代到 Python 服务 微信小程序后端 ielts.wanglipingresume.com → 反代到 Python 服务
所有子域共用一台阿里云服务器,对外只暴露 80/443。
只需要注册一个主域名。子域名(api、writing、projects)不需要单独注册、不需要单独付费,在 DNS 里各加一条 A 记录就行。
类比:你买了一套房子(主域),里面的卧室书房厨房(子域)不另外收钱。
技术上你完全可以让用户访问 https://api.xxx.com:8001,但:
https://api.xxx.com)正确做法是用域名区分服务,端口统一 443:
server {
listen 443 ssl http2;
server_name api.wanglipingresume.com;
ssl_certificate /etc/nginx/ssl/api.wanglipingresume.com/api.wanglipingresume.com.pem;
ssl_certificate_key /etc/nginx/ssl/api.wanglipingresume.com/api.wanglipingresume.com.key;
location / {
proxy_pass http://127.0.0.1:8001; # 转发到本地端口,不对外暴露
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;
}
}
对外永远是干净的 https://api.wanglipingresume.com,内部端口由 Nginx 按域名分发。
如果你的身份证地址和服务器所在地域不一致,备案会被要求提供居住证明。
我的情况是:山东青岛人,服务器在杭州。审核时就被卡在这里了。
可选方案:
建议:个人项目第一台服务器,直接选自己户籍所在地的地域,省掉一大堆麻烦。
Day 1 注册域名 + 阿里云实名 Day 1-3 准备材料 Day 3 提交备案 Day 3-5 阿里云初审(会打电话核实,保持手机畅通) Day 5-7 人脸核验 + 工信部短信核验(24 小时内必须完成) Day 7-20 管局审核 Day 20+ 解析域名 + 配置 Nginx + 上线
全程 3-4 周。备案期间可以用 IP + 端口做开发。
网站名称不能含「中国」「博客」「平台」「公司」等词,会被退回。改成「王莉萍的个人主页」这类就行。
小程序要求请求的域名必须:
所以流程是:先备案 → 再配 DNS → 再签证书 → 最后填到小程序后台。
用 Let's Encrypt 免费证书,配自动续期(90 天有效,必须自动续):
# webroot 模式:只需 DNS 解析到位 + webroot 可访问
certbot certonly --webroot \
-w /var/www/personal \
--domain api.wanglipingresume.com \
--email your-email@example.com \
--agree-tos --no-eff-email --non-interactive
# 续期定时任务(每周日凌晨 3 点)
0 3 * * 0 certbot renew --quiet --pre-hook 'systemctl stop nginx' --post-hook 'systemctl start nginx'
踩坑:签发前如果 80 端口是直接 301 跳 HTTPS,ACME 校验会失败。需要先给待签发的子域加一个临时的 80 端口 server 块,专门放 .well-known/acme-challenge/。
这部分花了我不少时间,值得单独记一笔。
% 是 systemd 的 specifier 语法字符我原本在 unit 文件里写:
Environment="IELTS_BUDDY_WEB_DB_URL=mysql+pymysql://remote_user:Pass%40666@127.0.0.1:3306/db"
结果 systemd 日志报:
Failed to resolve specifiers in IELTS_BUDDY_WEB_DB_URL=..., ignoring: Invalid slot
整行被丢弃,环境变量为空。
实测了几种写法:
Environment="X=...%40..."Environment="X=...%%40..."%40EnvironmentFile= 文件里写 %40%)最终方案:所有配置放独立 env 文件,unit 里只引一行。
EnvironmentFile=-/etc/ielts-web.env
env 文件权限设 600。额外好处:密钥不再出现在 unit 文件里(unit 文件默认全局可读)。
@ 必须 URL 编码修好坑 1 之后,数据库连不上了:
Can't connect to MySQL server on '666@127.0.0.1'
根因:SQLAlchemy URL 里 @ 是「用户信息分隔符」。密码里的字面 @ 会把主机解析成 666@127.0.0.1。
正确写法:
mysql+pymysql://remote_user:Pass%40666@127.0.0.1:3306/db
└─ 密码里的 @ 编码成 %40 ─┘└─ 主机前的分隔符 @ 保留 ─┘
注意:MySQL CLI 不解码 %40,命令行验证要用明文:
mysql -h 127.0.0.1 -u remote_user -p'Pass@666' 库名 # ✅ mysql -h 127.0.0.1 -u remote_user -p'Pass%40666' 库名 # ❌ Access denied
load_dotenv() 在 __main__.py 里我以为服务器上有 .env 文件,应用就会自动加载。并没有。
load_dotenv() 只写在 ielts_buddy/__main__.py 和 ielts_buddy/web/__main__.py 里。而生产环境是用:
uvicorn ielts_buddy.web.main:app
直接启动的,完全绕过了 __main__.py,所以 .env 永远不生效。
生产环境必须靠 systemd 的 EnvironmentFile= 注入,不能指望 .env。
一个值得注意的行为:如果你的 Nginx 配置引用了一个不存在的证书文件,nginx -t 会失败,而且整个配置文件都无法加载——所有站点一起挂掉。
我的处理方式是让部署脚本按证书实际存在情况动态生成 server 块:
if [ -f "/etc/nginx/ssl/$domain/$domain.pem" ]; then
# 生成这个域名的 server 块
else
echo " [跳过] $domain(证书缺失)"
fi
并且在校验失败时自动回滚到上一份配置。这样即使某个子域证书还没签好,也不影响其他站点。
整个过程下来,最深的两个体会:
第一,别把「教程里跳过的步骤」当成简单步骤。
备案的居住证、systemd 的 %、@ 的编码——每一个单独看都是一行文档,实际上是半小时的排查。
第二,把踩坑过程写下来,比记住结论更有价值。
因为结论会过期(工具会升级),但「为什么会这样」是通用的。
本文首发于微信公众号「苹之苹之」。我的个人站正在建设中,欢迎来 wanglipingresume.com 逛逛。