宝塔面板部署
本文介绍如何在宝塔 Linux 面板上部署 Lychee PHP 项目。
环境要求
- PHP >= 8.2
- Nginx
- MySQL / PostgreSQL / SQLite(按需)
- Composer
一、安装宝塔面板
参考宝塔官网教程安装 Linux 面板,安装完成后登录面板。
二、安装运行环境
在宝塔面板的 软件商店 中安装:
- Nginx(任意稳定版本)
- PHP 8.2+(推荐 8.3),安装时勾选以下扩展:
fileinfoopensslpdombstringtokenizerctypejsonbcmath
- MySQL(如使用数据库)
- Redis(如使用缓存 / 队列)
三、上传项目代码
- 在宝塔 文件 中进入
/www/wwwroot/ - 创建站点目录,例如
/www/wwwroot/example.com - 将项目代码上传到该目录(可使用 Git 克隆或直接上传压缩包解压)
- 确保目录结构如下:
example.com/
├── app/
├── config/
├── public/
│ └── index.php
├── runtime/
├── vendor/
└── lee- 在项目根目录执行 Composer 安装:
cd /www/wwwroot/example.com
composer install --no-dev --optimize-autoloader四、创建站点
- 宝塔面板 → 网站 → 添加站点
- 填写域名,根目录选择项目的
public目录,例如/www/wwwroot/example.com/public - PHP 版本选择 8.2+
- 点击提交
五、配置站点
5.1 设置运行目录
宝塔面板 → 网站 → 设置 → 网站目录,将运行目录设置为 /public。
5.2 伪静态规则
宝塔面板 → 网站 → 设置 → 伪静态,填入以下规则:
location / {
try_files $uri $uri/ /index.php$is_args$args;
}为什么用 try_files?
try_files会先检查请求的静态文件是否存在,存在则直接返回,不存在则交给index.php处理。这是 Laravel / ThinkPHP 等框架的标准做法。
5.3 Nginx 配置(关键)
宝塔面板 → 网站 → 设置 → 配置文件,重点检查以下配置:
① 注释掉 error_page
宝塔默认会生成 error_page 404 /404.html;,这会导致框架返回的 404 被 Nginx 拦截。需要注释掉:
#ERROR-PAGE-START
#error_page 404 /404.html;
#error_page 502 /502.html;
#ERROR-PAGE-END② 禁止 Nginx 拦截 PHP 错误响应
在 PHP 的 location 块中添加 fastcgi_intercept_errors off;:
location ~ \.php$ {
include enable-php-83.conf;
fastcgi_intercept_errors off;
}为什么要关 fastcgi_intercept_errors?
宝塔的 PHP 配置文件(
enable-php-*.conf)默认开启了fastcgi_intercept_errors on;。这会导致 Nginx 拦截 PHP 返回的 4xx / 5xx 状态码,再根据error_page配置去找错误页面文件。框架已经自己处理了异常响应(返回 JSON 或 HTML),不需要 Nginx 再插手。
③ 完整配置示例
server
{
listen 80;
listen 443 ssl http2;
server_name example.com;
root /www/wwwroot/example.com/public;
index index.php index.html index.htm;
# SSL 配置(宝塔自动生成,保持不变)
# ...
# 错误页:注释掉,由框架自行处理
#error_page 404 /404.html;
# 伪静态
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
# PHP 处理
location ~ \.php$ {
include enable-php-83.conf;
fastcgi_intercept_errors off;
}
# 静态资源缓存
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
expires 30d;
}
location ~ .*\.(js|css)?$ {
expires 12h;
}
access_log /www/wwwlogs/example.com.log;
error_log /www/wwwlogs/example.com.error.log;
}六、配置 .env
复制 .env.example 为 .env,修改数据库等配置:
cp .env.example .env确保 .env 中的关键配置正确:
APP_DEBUG=false
APP_URL=https://example.com七、验证部署
- 访问首页,确认能正常打开
- 请求一个不存在的路由,应返回框架的 404 JSON:
{"code":404,"msg":"Not Found"}- 查看
runtime/log/目录是否有日志生成 - 查看 Nginx 错误日志
/www/wwwlogs/example.com.error.log是否有异常
八、SSL / HTTPS
宝塔面板 → 网站 → 设置 → SSL,可申请 Let's Encrypt 免费证书或上传已有证书。开启后建议勾选 强制 HTTPS。
九、前后端分离部署
前后端分离项目(Vue / React 等 SPA + Lychee PHP API)与普通部署的差异仅在于:API 侧的配置和前端静态文件的部署。后端代码的上传、Composer 安装、站点创建等步骤与上文一致。
9.1 部署方式
| 方式 | 说明 | 是否需要 CORS |
|---|---|---|
| SPA 放在 public/admin(推荐) | 前端构建产物放入后端 public/admin,单站点单项目 | 否(同源) |
| 同域分目录 php-fpm 直连 | 前后端分目录,/api 路径直接交给 php-fpm | 否(同源) |
| Nuxt SSR 同域共存 | Nuxt 由 PM2 守护,Nginx 反代;与 admin、API 同域 | 否(同源) |
| 跨域独立部署 | 前端与 API 使用不同域名 / 子域名 | 是 |
9.2 API 配置差异
① 异常渲染为 JSON
API 项目建议在 config/app.php 中将异常响应固定为 JSON,避免浏览器收到 HTML 错误页:
// config/app.php
return [
'exception_render' => 'json',
];② 认证改为 Header 模式
前后端分离推荐 Token 鉴权,前端在请求头中携带 Authorization: Bearer <token>。在 config/satoken.php 中将 token 读取驱动设为 header:
// config/satoken.php
return [
'token_name' => '', // 留空则从 Authorization: Bearer 读取
'token_reader' => 'header', // 仅从 HTTP Header 读取
'timeout' => 86400 * 7,
'auto_renew' => true,
];详细认证配置见 认证 Auth。
9.3 方式一:SPA 放在 public/admin(推荐)
将前端构建产物放入后端项目的 public/admin 目录,Nginx 同时提供 API 与后台页面,是最简单的部署方式。
目录结构:
example.com/
├── app/
├── config/
├── public/
│ ├── index.php # 后端 API 入口
│ └── admin/ # SPA 构建产物(dist 内容)
│ ├── index.html
│ └── assets/
├── vendor/
└── lee① 前端构建时指定子路径
Vite 配置 base,路由配置 basename,三者路径必须一致:
// vite.config.js
export default defineConfig({
base: '/admin/',
})// Vue Router
createWebHistory('/admin/')
// React Router
<BrowserRouter basename="/admin">② 上传产物:将 npm run build 产出的 dist/ 内容上传到 public/admin/,然后正常构建发布后端代码即可。
③ Nginx 配置(站点根目录仍指向 public):
server
{
listen 80;
listen 443 ssl http2;
server_name example.com;
root /www/wwwroot/example.com/public;
index index.php index.html;
# 错误页:注释掉,由框架自行处理
#error_page 404 /404.html;
# 管理后台 SPA:文件存在直接返回,否则回退到 /admin/index.html
location ^~ /admin/ {
try_files $uri $uri/ /admin/index.html;
}
# 访问 /admin 时自动补斜杠
location = /admin {
return 301 /admin/;
}
# 其余路径交给框架处理(API)
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
include enable-php-83.conf;
fastcgi_intercept_errors off;
}
access_log /www/wwwlogs/example.com.log;
error_log /www/wwwlogs/example.com.error.log;
}关键说明:
^~ /admin/前缀匹配优先级高于.php正则,确保后台的静态文件与前端路由不会被交给 PHP 处理try_files ... /admin/index.html让/admin/users等 History 路由刷新时回退到 SPA 入口- API 与页面同源,前端请求直接用相对路径(如
/login、/users),无需 CORS、无需/api前缀 - 注意后端不要定义
/admin开头的 API 路由,避免与后台路径冲突;若 API 统一使用/api前缀则无此问题
9.4 方式二:同域分目录 php-fpm 直连
当前后端代码需要分开维护时,可在同一个 Nginx 站点内处理:静态文件由 Nginx 直接返回,/api/ 请求直接交给 php-fpm 执行后端的 index.php。
server
{
listen 80;
listen 443 ssl http2;
server_name app.example.com;
root /www/wwwroot/app.example.com; # 前端静态文件目录
index index.html;
# 前端 SPA:路由回退到 index.html
location / {
try_files $uri $uri/ /index.html;
}
# 后端 API:php-fpm 直连
location ^~ /api/ {
root /www/wwwroot/api.example.com/public;
rewrite ^/api/(.*)$ /$1 break;
include enable-php-83.conf;
fastcgi_param SCRIPT_FILENAME /www/wwwroot/api.example.com/public/index.php;
fastcgi_param REQUEST_URI $uri$is_args$args;
fastcgi_intercept_errors off;
}
# 静态资源缓存
location ~ .*\.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
expires 30d;
}
}关键说明:
root指向后端public,rewrite去掉/api前缀,后端路由无需加/apiinclude enable-php-83.conf;引入宝塔 php-fpm 配置,版本号按实际 PHP 修改(82 / 83 / 84)fastcgi_param SCRIPT_FILENAME .../index.php;所有 API 请求统一走后端入口fastcgi_param REQUEST_URI $uri$is_args$args;将去前缀后的路径传给框架fastcgi_intercept_errors off;禁止 Nginx 拦截 PHP 错误响应
9.5 方式三:跨域独立部署 + CORS 配置
前端独立创建纯静态站点:将 npm run build 产出的 dist/ 内容上传到站点目录(如 /www/wwwroot/app.example.com/),宝塔添加站点时 PHP 版本选择纯静态,伪静态填入 SPA 回退规则:
location / {
try_files $uri $uri/ /index.html;
}由于前端与 API 不在同一域名(如 app.example.com 调用 api.example.com),还需在后端配置 CORS。
// config/cors.php
return [
'allowed_origins' => ['https://app.example.com'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],
'allowed_credentials' => false,
'max_age' => 86400,
];注册为全局中间件:
// config/middleware.php
return [
\Lychee\cors\CorsMiddleware::class,
];详细 CORS 配置见 跨域 CORS。
9.6 前端请求示例(axios)
方式一 / 方式二(同源):
// 方式一(public/admin):API 与页面同源,直接用相对路径
// 方式二(分目录直连):baseURL 用 /api
const http = axios.create({
baseURL: '/api', // 方式一可省略或按实际接口前缀填写
timeout: 10000,
})跨域独立部署:
import axios from 'axios'
const http = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
})
// 请求拦截器:携带 Token
http.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器:处理 401
http.interceptors.response.use(
res => res,
err => {
if (err.response?.status === 401) {
localStorage.removeItem('token')
location.href = '/login'
}
return Promise.reject(err)
},
)9.7 方式四:Nuxt SSR 同域部署
Nuxt 默认是 SSR(服务端渲染),需要一个常驻 Node.js 进程,由 PM2 守护,Nginx 反向代理过去,不能像静态 SPA 一样直接由 Nginx 提供文件。典型架构是 Nuxt 前台、admin 后台、PHP API 三端同域共存:
https://example.com/
├── / → Nuxt SSR(Node:3000,PM2 守护,Nginx 反代)
├── /admin/ → 管理后台 SPA(public/admin 静态文件)
└── /api/ → Lychee PHP(php-fpm 直连)三者同源,浏览器侧无 CORS 问题。
① 宝塔安装 Node 环境
软件商店安装 PM2管理器,Node 选择 18 LTS 或 20 LTS。
② 构建并以 PM2 启动
在 Nuxt 项目目录(如 /www/wwwroot/nuxt-app)执行 npm run build,然后创建 ecosystem.config.js:
module.exports = {
apps: [{
name: 'nuxt-app',
script: './.output/server/index.mjs',
exec_mode: 'cluster',
instances: 'max',
env: {
PORT: 3000,
NODE_ENV: 'production',
// SSR 服务端内部回环访问 API
NUXT_API_BASE: 'http://127.0.0.1/api',
},
}],
}pm2 start ecosystem.config.js
pm2 save
pm2 startup # 开机自启③ Nginx 配置
站点根目录仍指向 PHP 项目的 public。WebSocket 的 map 指令需放在 http 块中(宝塔可写入 /www/server/nginx/conf/config/enable.conf 或在站点配置外层,勿放进 server):
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}站点 server 配置:
server
{
listen 80;
listen 443 ssl http2;
server_name example.com;
root /www/wwwroot/example.com/public;
index index.php;
#error_page 404 /404.html;
# 管理后台:静态 SPA
location ^~ /admin/ {
try_files $uri $uri/ /admin/index.html;
}
location = /admin {
return 301 /admin/;
}
# PHP API:php-fpm 直连
location ^~ /api/ {
rewrite ^/api/(.*)$ /$1 break;
include enable-php-83.conf;
fastcgi_param SCRIPT_FILENAME /www/wwwroot/example.com/public/index.php;
fastcgi_param REQUEST_URI $uri$is_args$args;
fastcgi_intercept_errors off;
}
# Nuxt 静态构建资源:Nginx 直接返回,减轻 Node 压力
location /_nuxt/ {
alias /www/wwwroot/nuxt-app/.output/public/_nuxt/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# 其余路径:反向代理给 Nuxt SSR
location / {
proxy_pass http://127.0.0.1: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 $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}匹配优先级:^~ /admin/、^~ /api/ 前缀命中后不再检查正则,/_nuxt/ 次之,剩余路径全部落到 location / 交给 Nuxt。
④ Nuxt 侧 API 地址(SSR 的关键点)
SSR 下数据请求发生在两处,地址不同:服务端渲染时走内部回环 http://127.0.0.1/api,浏览器 hydration 后走同源相对路径 /api。
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
apiBase: process.env.NUXT_API_BASE || 'http://127.0.0.1/api', // 服务端
public: {
apiBase: '/api', // 浏览器端
},
},
})// composables/useApi.ts
export function useApi() {
const config = useRuntimeConfig()
const base = import.meta.server ? config.apiBase : config.public.apiBase
return (url: string) => $fetch(base + url, { credentials: 'include' })
}首屏 SSR 需要鉴权时,可用 useRequestHeaders(['cookie', 'authorization']) 将用户请求头透传给服务端发起的 API 请求。
⑤ 备选:Nuxt 纯静态生成
若前台不需要 SSR,可执行 nuxi generate 产出纯静态文件(.output/public/),此时退化为静态站:配置 app: { baseURL: '/m/' } 后上传到 public/m/,Nginx 仿照 admin 添加 try_files $uri $uri/ /m/index.html; 即可,无需 Node / PM2,但失去 SSR 的 SEO 与首屏优势。
常见问题
1. 请求异常接口时报 open() ".../public/error" failed (2: No such file or directory)
原因:宝塔的伪静态规则中有指向 /error 的 rewrite,或者 error_page + fastcgi_intercept_errors 拦截了框架的错误响应。
解决:
- 检查伪静态规则,使用本文 5.2 节的
try_files规则 - 注释掉
error_page 404 /404.html; - 添加
fastcgi_intercept_errors off;
2. 静态资源 404
原因:Nginx 的 root 没有指向 public 目录。
解决:确保站点根目录和运行目录都指向 public。
3. 上传大小限制
修改 PHP 配置(宝塔 → 软件商店 → PHP 设置 → 配置修改):
upload_max_filesize = 50M
post_max_size = 50M
max_execution_time = 300同时在 Nginx 的 server 块中添加:
client_max_body_size 50m;4. 前端 SPA 刷新页面 404
原因:Vue / React 的 History 模式下,Nginx 没有将路由回退到 index.html。
解决:在前端站点的伪静态中添加 try_files $uri $uri/ /index.html;。
5. 跨域请求被浏览器拦截
原因:后端未配置 CORS 或 allowed_origins 不包含前端域名。
解决:
- 创建
config/cors.php并注册CorsMiddleware为全局中间件(见 9.5 节) - 确保
allowed_origins包含前端实际域名(含协议) - 检查
allowed_headers是否包含Authorization
6. 401 未生效,始终能访问
原因:SatokenMiddleware 未挂载到对应控制器或方法。
解决:在需要鉴权的控制器或方法上添加 #[Middleware(SatokenMiddleware::class)],登录接口用 #[WithoutMiddleware] 排除。
7. php-fpm 直连方案下获取不到真实客户端 IP
php-fpm 直连方案没有代理层,客户端 IP 直接由 $remote_addr 传递给 PHP,框架的 IP 方法即可正确获取。若服务器前面还有 CDN / 负载均衡,需在 location ^~ /api/ 中补充:
fastcgi_param REMOTE_ADDR $http_x_forwarded_for;注意:直接信任
X-Forwarded-For有伪造风险,生产环境应仅在确认前置代理可信时使用。
