本文档回答一个问题:如何把一个网页应用部署到我们的服务器,再接入飞书应用。照着步骤做即可,末尾附模板库、完整实例、检查清单和踩坑记录。
https://子域名/路径 → 香港机 Nginx(443) → 127.0.0.1:端口 → Docker 容器。| 项目 | 香港服务器(主力,用这台部署) | 大陆服务器(公网中枢,勿动) |
|---|---|---|
| 公网 IP | 43.129.74.108 | 111.229.185.231 |
| 系统 | Ubuntu 24.04 | Ubuntu 24.04 |
| Web | 宝塔面板 + Nginx | Nginx(frps/OpenClaw 等) |
| SSH 别名 | ssh ubuntu@any-g.cn(密钥免密 ~/.ssh/id_ed25519) | ssh cloud(端口 2222) |
| 用途 | 官网 + 各业务应用 + 本文档 | 公网中枢,只读,勿动 |
| 备案 | 香港机免 ICP 备案,域名可直接绑定 | — |
应用需要自带三个东西(缺一不可):
Dockerfile —— 描述怎么构建镜像(Node 应用参考 FROM node:24-alpine + CMD ["node","src/server.js"])。compose.yaml —— 端口映射只绑 127.0.0.1,数据挂载持久化目录。.env —— 密钥和配置,只放服务器,不进 Git、不下发浏览器。在腾讯云 DNSPod 给目标域名加一条 A 记录:主机记录 → 43.129.74.108。
Resolve-DnsName 子域名.njwl.ai -Type A # 生效后应返回 43.129.74.108
scp compose.yaml ubuntu@any-g.cn:~/项目名/
scp .env ubuntu@any-g.cn:~/项目名/.env
scp -r 项目目录 ubuntu@any-g.cn:~/项目名/
ssh ubuntu@any-g.cn 'cd ~/项目名 && docker compose up -d --build && docker compose ps'
本地冒烟测试(还没配域名时先确认容器活着):
ssh ubuntu@any-g.cn "curl -s http://127.0.0.1:18831/ | head"
scp 项目名.conf ubuntu@any-g.cn:/tmp/
ssh ubuntu@any-g.cn 'sudo cp /tmp/项目名.conf /www/server/panel/vhost/nginx/ && \
sudo nginx -t && sudo /www/server/nginx/sbin/nginx -s reload'
HTTPS 证书二选一(飞书可信域名要求 HTTPS):
ssh ubuntu@any-g.cn 'sudo certbot --nginx -d 域名'。https://域名/ 能打开首页。全程浏览器操作,需企业管理员账号。下面示例用 lab.njwl.ai/shidu,换成你自己的域名/路径即可。
https://open.feishu.cn/open.feishu.cn/app).env(FEISHU_APP_ID / FEISHU_APP_SECRET)https://lab.njwl.ai/shidu/https://lab.njwl.ai/shidu/auth/callbacklab.njwl.ai1.0.0)和更新说明feishu.cn/admin)https://lab.njwl.ai/shidu/,且能免登录识别当前用户DATA_MODE=feishu
FEISHU_APP_ID=第 3 步的 App ID
FEISHU_APP_SECRET=第 3 步的 App Secret
FEISHU_BITABLE_APP_TOKEN=多维表格 URL 里 /base/ 后面那串(App Token)
FEISHU_MASTER_TABLE_ID=主档表 URL 里 table= 后面那串(tbl 开头)
FEISHU_HISTORY_TABLE_ID=操作记录表 URL 里 table= 后面那串
PUBLIC_BASE_URL=https://lab.njwl.ai
APP_BASE_PATH=/shidu
FEISHU_LOGIN_ENABLED=true
App Token / Table ID 看多维表格地址栏:https://xxx.feishu.cn/base/AppToken?table=tblXXX&view=vewXXX。
让用户用飞书账号登录,应用就知道「是谁在操作」。飞书 OAuth 2.0:在飞书工作台点应用自带登录态,浏览器直接打开会跳授权页点一下「同意」。
.env(App ID / Secret + 登录开关,SSH 改完要重启容器);③ 应用代码里的 login / callback 接口(通常已写好,只需确认路径对得上)。contact:user.base:readonly)https://lab.njwl.ai/shidu/auth/callbackhttps://accounts.feishu.cn/open-apis/authen/v1/authorize?app_id=…&redirect_uri=…&state=随机串code 跳回 redirect_uri?code=xxxPOST /open-apis/authen/v2/oauth/token 换 user_access_tokenGET /open-apis/authen/v1/user_info,得到 open_id / 名字 / 头像,写入 session// ① 登录入口:跳飞书授权页
app.get('/shidu/auth/login', (req, res) => {
const url = 'https://accounts.feishu.cn/open-apis/authen/v1/authorize'
+ '?app_id=' + process.env.FEISHU_APP_ID
+ '&redirect_uri=' + encodeURIComponent(base + '/shidu/auth/callback')
+ '&state=' + crypto.randomUUID();
res.redirect(url);
});
// ② 回调:code 换 token → 拿用户信息 → 建登录态
app.get('/shidu/auth/callback', async (req, res) => {
const code = req.query.code;
const t = await (await fetch('https://open.feishu.cn/open-apis/authen/v2/oauth/token', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ grant_type: 'authorization_code',
client_id: process.env.FEISHU_APP_ID, client_secret: process.env.FEISHU_APP_SECRET,
code, redirect_uri: base + '/shidu/auth/callback' })
})).json();
const u = await (await fetch('https://open.feishu.cn/open-apis/authen/v1/user_info', {
headers: { Authorization: 'Bearer ' + t.data.access_token }
})).json();
req.session.user = u.data; // u.data.open_id / name / avatar_url
res.redirect('/shidu/');
});
ssh ubuntu@any-g.cn.env:cd ~/shidu-app && nano .envFEISHU_APP_ID=cli_xxxxxxxxxxxx # 开放平台「凭证与基础信息」里的 App ID
FEISHU_APP_SECRET=xxxxxxxxxxxxxxxx # App Secret
FEISHU_LOGIN_ENABLED=true # 开启飞书登录鉴权
PUBLIC_BASE_URL=https://lab.njwl.ai # 登录回调用到的域名
APP_BASE_PATH=/shidu # 应用子路径
docker compose up -d(或 docker compose restart)PUBLIC_BASE_URL + APP_BASE_PATH + /auth/callback = https://lab.njwl.ai/shidu/auth/callback。这个地址必须和第 2 步在飞书「安全设置 → 重定向 URL」里填的一字不差,否则登录回调 404。state 用随机串防 CSRF;改完 .env 必须重启容器才生效;user_access_token 只在后端用,别下发浏览器。两种方式:群 Webhook 机器人(最简单,往群里发);应用机器人(能私聊指定人,复杂一点)。一般「提醒」用群 Webhook 就够了。
https://open.feishu.cn/open-apis/bot/v2/hook/xxxx)拿到 Webhook 地址后,往它 POST 一段 JSON 就能发提醒:
curl -X POST 'https://open.feishu.cn/open-apis/bot/v2/hook/xxxx' \
-H 'Content-Type: application/json' \
-d '{"msg_type":"text","content":{"text":"⚠️ 湿度超标:A区 72%"}}'
塞进应用代码里(Node 示例):
await fetch(process.env.FEISHU_BOT_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ msg_type: 'text', content: { text: '任务完成提醒' } })
});
把 Webhook 地址放到服务器 .env(FEISHU_BOT_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/xxxx),不要写进 Git。常用 msg_type:text(纯文本)、post(富文本)、interactive(卡片)。
tenant_access_token,再调 POST /open-apis/im/v1/messages,receive_id 填目标用户的 open_idopen_id 通过飞书免登录(/open-apis/authen/v1/user_info)拿到# 域名.conf
server {
listen 80;
server_name 域名;
location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; default_type "text/plain"; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
http2 on;
server_name 域名;
ssl_certificate /etc/letsencrypt/live/域名/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/域名/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location = / { return 301 /路径/; } # 根路径跳到应用子路径(若用根路径则删这行)
location /路径/ {
client_max_body_size 25m;
proxy_pass http://127.0.0.1:端口; # 不带尾斜杠,路径原样透传
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_read_timeout 60s;
}
access_log /www/wwwlogs/域名.log;
error_log /www/wwwlogs/域名.error.log;
}
services:
app:
build: .
container_name: 容器名
restart: unless-stopped
env_file: .env
ports:
- "127.0.0.1:端口:3000" # 只绑本机回环
volumes:
- ./data:/app/data
PORT=3000
PUBLIC_BASE_URL=https://域名
APP_BASE_PATH=/路径
DATA_MODE=mock # mock=本地演示 / feishu=正式
FEISHU_APP_ID=
FEISHU_APP_SECRET=
FEISHU_BITABLE_APP_TOKEN=
FEISHU_MASTER_TABLE_ID=
FEISHU_HISTORY_TABLE_ID=
FEISHU_LOGIN_ENABLED=true
| 项 | 值 |
|---|---|
| 域名 / 路径 | lab.njwl.ai / /shidu |
| 容器端口 | 127.0.0.1:18831 |
| 主页 / 回调 | https://lab.njwl.ai/shidu/ · …/shidu/auth/callback |
无子路径的典型:location / 直接 proxy_pass http://127.0.0.1:6806,APP_BASE_PATH 留空。
历史模式:香港机 proxy_pass http://111.229.185.231 反代到大陆机容器。新项目不再这样,统一香港机直部署。
docker compose ps 健康、日志无报错nginx -t 通过.env 已填(App ID/Secret、Token、表 ID)https://域名/路径/,OAuth 回调 = …/auth/callback.env,未进 Git、未下发浏览器Data not ready, please try again later → 指数退避重试。client_max_body_size,别让前端把 HTML 错误页当 JSON。proxy_pass 尾斜杠(带尾斜杠会剥前缀,导致路径错位)。root + index 即可,别用 try_files $uri $uri/ =404(目录请求会直接落到 404)。Resolve-DnsName。