个人网站怎么备案,以及如何把域名开放给微信小程序

从零搭个人网站时绕不开的两件事: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 记录就行。

类比:你买了一套房子(主域),里面的卧室书房厨房(子域)不另外收钱。

关键点:不要对外暴露非 443 端口

技术上你完全可以让用户访问 https://api.xxx.com:8001,但:

正确做法是用域名区分服务,端口统一 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 按域名分发。


二、ICP 备案:卡点最多的其实是「居住证」

需要的材料

材料要求 身份证正反面清晰、四角完整 手持身份证照片头部完整、证件信息清晰 域名证书阿里云后台直接下载 真实性核验单系统生成,签字后上传 居住证 / 暂住证 / 社保缴纳证明⚠️ 最容易漏的一项

关于居住证这一条

如果你的身份证地址和服务器所在地域不一致,备案会被要求提供居住证明。

我的情况是:山东青岛人,服务器在杭州。审核时就被卡在这里了。

可选方案:

  1. 服务器选身份证所在地地域——最省事。青岛有阿里云地域,选它就不需要居住证
  2. 提供社保证明——如果已经在当地工作并缴社保,让 HR 出一份即可
  3. 办理居住证——周期长,不适合急用
建议:个人项目第一台服务器,直接选自己户籍所在地的地域,省掉一大堆麻烦。

时间预期

Day 1     注册域名 + 阿里云实名
Day 1-3   准备材料
Day 3     提交备案
Day 3-5   阿里云初审(会打电话核实,保持手机畅通)
Day 5-7   人脸核验 + 工信部短信核验(24 小时内必须完成)
Day 7-20  管局审核
Day 20+   解析域名 + 配置 Nginx + 上线

全程 3-4 周。备案期间可以用 IP + 端口做开发。

一个容易踩的坑

网站名称不能含「中国」「博客」「平台」「公司」等词,会被退回。改成「王莉萍的个人主页」这类就行。


三、微信小程序接入:HTTPS + 备案,两个硬门槛

小程序要求请求的域名必须:

  1. 是 HTTPS
  2. 已备案
  3. 在后台配置过(开发设置 → 服务器域名)

所以流程是:先备案 → 再配 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 配环境变量:两个真实的坑

这部分花了我不少时间,值得单独记一笔。

坑 1:% 是 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..."✅ 正确得到 %40 EnvironmentFile= 文件里写 %40✅ 正确(不解析 %)

最终方案:所有配置放独立 env 文件,unit 里只引一行。

EnvironmentFile=-/etc/ielts-web.env

env 文件权限设 600。额外好处:密钥不再出现在 unit 文件里(unit 文件默认全局可读)。

坑 2:密码里的 @ 必须 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

坑 3: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 配置引用了一个不存在的证书文件,nginx -t 会失败,而且整个配置文件都无法加载——所有站点一起挂掉。

我的处理方式是让部署脚本按证书实际存在情况动态生成 server 块:

if [ -f "/etc/nginx/ssl/$domain/$domain.pem" ]; then
    # 生成这个域名的 server 块
else
    echo "  [跳过] $domain(证书缺失)"
fi

并且在校验失败时自动回滚到上一份配置。这样即使某个子域证书还没签好,也不影响其他站点。


六、最后

整个过程下来,最深的两个体会:

第一,别把「教程里跳过的步骤」当成简单步骤。

备案的居住证、systemd 的 %、@ 的编码——每一个单独看都是一行文档,实际上是半小时的排查。

第二,把踩坑过程写下来,比记住结论更有价值。

因为结论会过期(工具会升级),但「为什么会这样」是通用的。


本文首发于微信公众号「苹之苹之」。我的个人站正在建设中,欢迎来 wanglipingresume.com 逛逛。

喜欢这类内容?

我主要在微信公众号「苹之苹之」更新,专注工业 AI 落地、项目管理与研发实践。想聊聊或者有合作想法,欢迎找我。

联系方式 → · 全部文章 →