圣堂之魂
开始部署

1Panel 面板部署


★ 前置条件

  • 已安装 1Panel 面板
  • 已在 1Panel 中安装 OpenResty 应用
  • 已在 1Panel 中通过“网站”功能创建 PHP 运行环境网站
  • 已完成域名解析和 SSL 证书配置

★ 1Panel 容器内路径

1Panel 的 OpenResty Docker 容器会将宿主机路径映射到容器内路径:

宿主机路径容器内路径
/{1panel安装路径}/www/sites/域名//www/sites/域名/
/{1panel安装路径}/www/conf.d//etc/nginx/conf.d/

所有 Nginx 配置文件中引用的路径,必须使用容器内路径(如 /www/sites/...),绝不能写宿主机路径(如 /{1panel安装路径}/www/sites/...)。因为 Nginx 运行在容器内部,它只能“看”到容器内的目录结构,看不到宿主机的真实路径。

验证你的容器挂载映射:

docker ps --format "table {{.Names}}\t{{.Image}}" | grep -i openresty
{openresty容器名}   1panel/openresty:{openresty版本号}
docker inspect {openresty容器名} --format '{{json .Mounts}}' | python3 -m json.tool

提示:此处的 {openresty容器名} 是第一条指令中的输出。

[
  {
      "Type": "bind",
      "Source": "/{1panel安装路径}/www",
      "Destination": "/www",
      "Mode": "rw",
      "RW": true,
      "Propagation": "rprivate"
  },
  {
      "Type": "bind",
      "Source": "/{1panel安装路径}/www/conf.d",
      "Destination": "/usr/local/openresty/nginx/conf/conf.d",
      "Mode": "rw",
      "RW": true,
      "Propagation": "rprivate"
  }
  ...
]

注意:实际输出条目较多,只需重点关注 Source(宿主机路径)和 Destination(容器内路径)的对应关系即可。


一、上传源码

将项目所有文件上传到网站目录的 index/ 子目录中。

1Panel 网站目录结构:


二、编辑配置

1. internal_host 跨容器通信

1Panel 的 OpenResty 和 PHP 容器可能使用不同的网络模式:

容器典型网络模式说明
OpenRestyhost共享宿主机网络,127.0.0.1 可访问
PHP-FPM1panel-network(bridge)独立网络,127.0.0.1 指向自己

PHP 容器内的 127.0.0.1 指向的是容器自身,访问不到 OpenResty 的 18500 端口。

因此,请先排查是否存在此问题:

docker inspect {OpenResty容器名} --format '{{.HostConfig.NetworkMode}}'
host
docker inspect {PHP容器名} --format '{{.HostConfig.NetworkMode}}'
1panel-network

如果你的输出和我一样(OpenResty 为 Host,PHP 为 1panel-network),请修改 config.php 中的 internal_host 配置,将 http://127.0.0.1:18500 改为 Docker 网桥上宿主机的 IP 地址(例如 http://172.18.0.1:18500)。

查找网关 IP 方法:

docker inspect {PHP容器名} --format '{{range .NetworkSettings.Networks}}{{.Gateway}}{{end}}'
172.18.0.1

警告:此处仅作示例,具体以实际输出为准!

2. config.php

打开 ./api/config.php 文件进行修改:

警告:注意修改高亮行内容。将 http://127.0.0.1:18500 改为 Docker 网桥上宿主机的 IP 地址。

config.php
<?php
return [
    // 短链域名
    'domain'           => 'https://{你的域名}',  // 改为你的短链域名,用于拼接完整短链

    // 前端面板域名(用于 CORS,与短链跳转域名独立)
    // 如果前端面板与短链服务部署在不同域名,请改为面板实际域名(如 https://panel.example.com)
    // 未配置时回退使用 domain,保持向后兼容
    'panel_origin'     => '',

    // 前端面板总开关
    // false 时 PHP 层直接返回 403,前端不再可访问(短链跳转不受影响)
    // 同时建议在 nginx 的 /api/ 块启用 return 403(双层独立生效)
    'panel_enabled'    => true,

    // 时区偏移(ISO 8601 格式)
    // 影响过期时间的显示和计算
    'tz_offset'        => '+08:00',

    // 冷存储 JSON 文件路径
    'perm_path'        => __DIR__ . '/../backend/data/perm.json',  // 永久短链数据文件
    'temp_path'        => __DIR__ . '/../backend/data/temp.json',  // 临时短链数据文件

    // API Key 存储
    'keys_path'        => __DIR__ . '/../backend/data/keys.json',  // Key 存储文件
    'key_ttl_days'     => 7,                                       // 常驻 Key 有效期(天)
    'onetime_pool_size' => 20,                                     // 一次性 Key 池大小

    // 数量限制,本服务最大上限均为 9999
    'perm_limit'       => 9999, // 永久短链
    'temp_limit'       => 9999, // 临时短链

    // TTL 上限,临时短链最长存活时间(秒),默认 1 年
    'ttl_max'          => 365 * 24 * 3600,

    // 保留短码(与 nginx 路由前缀对齐,禁止用户注册为自定义短码)
    // 新增路由时需同步更新此列表
    'reserved_codes'   => ['api', 'stat', 'admin', 'data', 'lua', 'headless', 'internal'],

    // 内部 OpenResty API 地址(仅本地 18500 端口,不对外暴露)
    // 注意!! 如果 PHP 容器使用 bridge 网络,127.0.0.1 指向的是容器自身,访问不到 OpenResty,此时需改为 Docker 网桥的宿主机 IP(例如172.19.0.1)
    //如果你的 PHP 是直接部署的,则无需改网桥IP,保持 127.0.0.1 即可
    'internal_host'    => 'http://127.0.0.1:18500',   //具体内网地址请自行查阅!
    'internal_timeout' => 2.0,                        //内部接口请求超时时间(秒)
    'internal_token_path' => __DIR__ . '/../backend/data/internal_token',
    'internal_token'      => trim(@file_get_contents(__DIR__ . '/../backend/data/internal_token') ?: ''),
];

三、修改 OpenResty 主配置

使用 1Panel 面板的文件管理打开文件:

/{1Panel安装目录}/apps/openresty/{openresty容器名}/conf/nginx.conf

http 块开头(include mime.types; 之前)添加以下内容:

警告:注意修改高亮行内容。

Nginx 主配置
# ═══════════════════════════════════════════════════
# 声明共享字典和限流区域
# ═══════════════════════════════════════════════════

    # ———— 共享字典 ——————————————————————————————————————
    lua_shared_dict su_url       4m;
    lua_shared_dict su_exp       2m;
    lua_shared_dict su_meta      1m;
    lua_shared_dict su_blocklist 1m;

    # ———— API 限流:每 IP 每分钟 30 次请求 ————————————————
    limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;

    # ———— 跳转限流:每个 IP 每秒 2 次,突发 10 次 ——————————
    limit_req_zone $binary_remote_addr zone=redirect_burst:10m rate=2r/s;

    # ———— Worker 初始化:注册定时器(每次 worker 启动/reload 均执行)————
    # 注意:init_worker_by_lua_block 中 ngx.var 不可用,配置通过 config.lua 读取
    # expire_sweep 定时器在此注册,确保 nginx reload 后定时器不丢失
    init_worker_by_lua_block {
        package.path = "/www/sites/{你的域名}/index/backend/lua/?.lua;;;" .. package.path
        local init = require "shorturl.init"
        init.init_worker()
    }

    # ═══════════════════════════════════════════════════
    # 内部 Lua API(仅本地访问)
    # ═══════════════════════════════════════════════════
    server {
        # 仅绑定回环地址;若需 Docker bridge 访问,追加第二行:
        #   listen <docker网桥IP>:18500;
        listen 127.0.0.1:18500;
        server_name _;

        location /internal/ {
            allow 127.0.0.1;
            allow 172.18.0.0/16;  # 请将 172.18.0.0/16 替换为你实际的 Docker 网桥子网
            deny all;

            content_by_lua_block {
                package.path = "/www/sites/{你的域名}/index/backend/lua/?.lua;;;" .. package.path
                local internal = require "shorturl.internal"
                internal.dispatch()
            }
        }
    }
# ═══════════════════════[END]═════════════════════════

插入示例:

    ...前面保持不变...

events {
      use epoll;
      worker_connections 5120;
      multi_accept on;
}

http {
    # ═══════════════════════════════════════════════════
    # 声明共享字典和限流区域
    # ═══════════════════════════════════════════════════
    ....在此处添加...
    #══════════════════════[END]═════════════════════════

    include       mime.types;
    default_type  application/octet-stream;

    ...后面保持不变...

四、修改站点 Nginx 配置

这是最关键的一步。1Panel 创建网站时会自动生成一份 Nginx 配置,我们需要在保留 1Panel 自动生成的 Nginx 配置基础上添加 Short_NURL 所需的配置。

请从 1Panel 面板的“网站”界面进入你的网站,编辑 Nginx 配置。

警告:注意修改高亮行内容。

站点 Nginx 配置
    # ═══════════════════════════════════════════════════
    # ★ 新增配置
    # ═══════════════════════════════════════════════════

    # ★ 业务参数
    set $su_php_fpm          "127.0.0.1:9000";   # PHP-FPM 监听地址,按实际环境修改
    set $su_domain           "https://{你的域名}";
    set $su_tz_offset        "+08:00";
    set $su_perm_path        "/www/sites/{你的域名}/index/backend/data/perm.json";
    set $su_temp_path        "/www/sites/{你的域名}/index/backend/data/temp.json";
    set $su_expire_interval  3600;
    set $su_redirect_code    302;

    # ★ 安全响应头(补充 ShortURL 需要的)
    add_header X-Content-Type-Options  "nosniff"          always;
    add_header X-Frame-Options         "DENY"             always;
    add_header Referrer-Policy         "no-referrer"      always;

    # ★ 屏蔽敏感目录
    location ^~ /data/ {
        deny all;
        return 404;
    }
    location ^~ /lua/ {
        deny all;
        return 404;
    }
    location ^~ /backend/ {
        deny all;
        return 404;
    }
    location ^~ /api/common/ {
        deny all;
        return 404;
    }
    location ^~ /api/key/ {
        deny all;
        return 404;
    }
    location ^~ /api/lua/ {
        deny all;
        return 404;
    }
    location ^~ /api/storage/ {
        deny all;
        return 404;
    }
    location ^~ /headless/ {
        deny all;
        return 404;
    }
    # ^~ 前缀匹配,拦截所有 /internal/* 路径(不仅是精确的 /internal/)
    location ^~ /internal/ {
        deny all;
        return 404;
    }

    # ★ API 路由 → PHP-FPM
    # 覆盖下方原有的 PHP location,API 请求不走默认 PHP 处理
    location ^~ /api/ {
        limit_req zone=api burst=5 nodelay;

        location = /api/create {
            limit_except POST { deny all; }
            fastcgi_pass $su_php_fpm;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME /www/sites/{你的域名}/index/api/routes/create.php;
            include fastcgi_params;
        }

        location = /api/delete {
            limit_except POST { deny all; }
            fastcgi_pass $su_php_fpm;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME /www/sites/{你的域名}/index/api/routes/delete.php;
            include fastcgi_params;
        }

        location = /api/list {
            limit_except GET { deny all; }
            fastcgi_pass $su_php_fpm;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME /www/sites/{你的域名}/index/api/routes/list.php;
            include fastcgi_params;
        }

        location = /api/stat {
            limit_except GET { deny all; }
            fastcgi_pass $su_php_fpm;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME /www/sites/{你的域名}/index/api/routes/stat.php;
            include fastcgi_params;
        }

        location = /api/ping {
            limit_except GET { deny all; }
            fastcgi_pass $su_php_fpm;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME /www/sites/{你的域名}/index/api/ping.php;
            include fastcgi_params;
        }
    }

    # ★ 无头链路路由 → PHP-FPM(nurl-key 远程管理工具使用)
    location ^~ /headless/api/ {
        limit_req zone=api burst=5 nodelay;

        location = /headless/api/create {
            limit_except POST { deny all; }
            fastcgi_pass $su_php_fpm;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME /www/sites/{你的域名}/index/headless/create.php;
            include fastcgi_params;
        }

        location = /headless/api/delete {
            limit_except POST { deny all; }
            fastcgi_pass $su_php_fpm;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME /www/sites/{你的域名}/index/headless/delete.php;
            include fastcgi_params;
        }

        location = /headless/api/list {
            limit_except GET { deny all; }
            fastcgi_pass $su_php_fpm;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME /www/sites/{你的域名}/index/headless/list.php;
            include fastcgi_params;
        }

        location = /headless/api/stat {
            limit_except GET { deny all; }
            fastcgi_pass $su_php_fpm;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME /www/sites/{你的域名}/index/headless/stat.php;
            include fastcgi_params;
        }

        # ~* 大小写不敏感,与其他接口对齐;PHP 侧已做 strtolower
        location ~* "^/headless/api/get/([0-9a-zA-Z-]{1,4})$" {
            limit_except GET { deny all; }
            fastcgi_pass $su_php_fpm;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME /www/sites/{你的域名}/index/headless/get.php;
            fastcgi_param PATH_INFO       /$1;
            include fastcgi_params;
        }
    }

    # ————★ 一次性数据加载(惰性触发)————————————————————————————————
    # 每个 worker 首次请求时执行冷启动(从 JSON 加载到 shared dict),后续请求跳过
    # 注意:定时器注册已移至 init_worker_by_lua_block(见主配置),此处仅负责数据加载
    access_by_lua_block {
        package.path = "/www/sites/{你的域名}/index/backend/lua/?.lua;;;" .. package.path
        local su_meta = ngx.shared.su_meta
        if not su_meta:get("inited_" .. ngx.worker.id()) then
            math.randomseed(ngx.time() + ngx.worker.id())
            local init = require "shorturl.init"
            init.init()
            su_meta:set("inited_" .. ngx.worker.id(), 1)
        end
    }

    # ★ 短链跳转(最低优先级,匹配 1-4 位小写字母数字)
    # 正则中的花括号必须用双引号包裹,否则 Nginx 会报错
    location ~* "^/([0-9a-z-]{1,4})$" {
        limit_req zone=redirect_burst burst=10 nodelay;
        limit_except GET { deny all; }
        content_by_lua_block {
            package.path = "/www/sites/{你的域名}/index/backend/lua/?.lua;;;" .. package.path
            local redirect = require "shorturl.redirect"
            redirect.go()
        }
    }
    
  # ══════════════════════[END]═════════════════════════════

插入示例:

server {
    listen 443 ssl;
    listen 443 quic;
    listen [::]:443 ssl;
    listen [::]:443 quic;
    server_name {你的域名};
    ...前面保持不变...

    ...在中间任意位置插入...
    
    ...后面保持不变...
}

五、生成 API Key

docker exec {PHP容器名} php /www/sites/{你的域名}/index/nurl -new

预期输出如下:

正在生成密钥...

━━━ 常驻密钥 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  su_#  //Key 内容已隐去,下同
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
有效期:7 天。过期时间:2026-05-27T12:36:48+08:00

━━━ 一次性密钥(新增 20 个)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  1. su_#1
  2. su_#2
  3. su_#3
  4. su_#4
  5. su_#5
  ……
  20. su_#20
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
请立即保存所有密钥,之后将不再显示。
每个密钥仅可使用一次。使用后会自动生成新密钥。
密钥永不过期,直到被使用。

密钥已存储至:/www/sites/{你的域名}/index/backend/data/keys.json

━━━ 内部通信令牌(LPA-Key)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  5b7b8a45d3e0db1685...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
已写入:/www/sites/{你的域名}/index/backend/data/internal_token
此令牌用于 PHP↔Lua 内部通信认证,无需手动复制。

内部通信令牌(LPA-Key)由 nurl -new 首次执行时自动生成,用于 PHP↔Lua 内部 API 认证。无需手动操作。

Key 明文仅在此时显示一次,立即保存。


六、设置文件权限

cd {1panel安装路径}/www/sites/{你的域名}/index

chmod 777 backend/data

chmod 666 backend/data/*

必须设置,否则 Lua 无法加载配置!


七、验证

curl -I https://{你的域名}
HTTP/2 200
curl https://{你的域名}/api/stat \ -H "X-Token: {su_你的Key}"
{"perm_count":0,"temp_count":0,"perm_limit":9999,"temp_limit":9999}
curl -X POST https://{你的域名}/api/create \
-H "Content-Type: application/json" \
-d '{"url":"https://{测试长链接}","key":"{su_你的Key}"}'
{"short_url":"https:\/\/{你的域名}\/{4位短码}"}
curl -I https://{你的域名}/{4位短码}
HTTP/2 302

本页目录