Laravel 项目在本地跑通后,搬到 VPS 最容易卡在四处:Nginx 指向了项目根目录、.env 没配对、storage 不能写、队列任务没有常驻进程。首页出现 500 时,反复重装 PHP 往往解决不了这些问题。本文按 Ubuntu 24.04 + Laravel 13 + PHP 8.3-FPM + Nginx + MySQL 给一套单机上线流程,覆盖 HTTPS、数据库迁移、队列、定时任务、备份和回滚。Laravel 13 的 PHP 要求与命令已按 2026 年 10 月 3 日的官方文档核对;已有项目仍应以自己的 composer.lock 和扩展依赖为准。
| 场景 | 起步配置 | 需要盯住的资源 |
|---|---|---|
| 小流量站点、数据库同机 | 2 vCPU / 4 GB RAM / 40 GB SSD | PHP-FPM Worker、MySQL 缓冲池、日志和备份空间 |
| 有导入、图片处理或多个队列任务 | 4 vCPU / 8 GB RAM / 80 GB SSD | 并发 Job 的内存峰值和执行时长 |
这些是部署起点,不是性能保证。正式容量要按真实请求、数据库大小和 Job 峰值压测。公网只放行 SSH 管理端口、80 和 443;MySQL 3306 只在本机监听。若数据库还没准备好,可先按MySQL 8.4 的 VPS 部署与备份教程完成安装,本文不重复开放数据库公网端口。
下面命令在一台尚未运行其他 Web 服务的 Ubuntu 24.04 VPS 上执行。已有宝塔或 Nginx 站点时,先检查 80/443 占用和现有配置,避免覆盖入口。
sudo apt update
sudo apt install -y nginx git unzip composer supervisor certbot python3-certbot-nginx mysql-client \
php8.3-fpm php8.3-cli php8.3-mysql php8.3-mbstring php8.3-xml \
php8.3-curl php8.3-zip php8.3-bcmath php8.3-intl php8.3-gd
php -v
composer --version
php -m | grep -E 'mbstring|pdo_mysql|xml|curl'
systemctl status php8.3-fpm --no-pager
Laravel 13 官方最低为 PHP 8.3;项目使用图片、PDF、Redis 或其他扩展时,按 composer.json 和实际功能补齐模块。不要用 composer install --ignore-platform-reqs 掩盖缺失扩展。
建立单独的部署用户,源码放在 /srv/laravel-app。以下以已有 Git 仓库为例,把 YOUR_REPO_URL 换成自己的仓库地址;私有仓库优先用只读部署密钥,不要把个人访问令牌写进命令历史。
sudo adduser --disabled-password --gecos '' deploy
sudo install -d -o deploy -g www-data -m 2750 /srv/laravel-app
sudo -u deploy git clone YOUR_REPO_URL /srv/laravel-app
cd /srv/laravel-app
sudo -u deploy composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
composer install 应依据提交的 composer.lock 安装。若项目使用 Vite,必须用符合项目 package.json 要求的 Node.js 版本执行 npm ci && npm run build,并确认 public/build 已出现在当前发布版本;也可在 CI 先构建,再把产物和源码一起部署。不要让服务器临时运行 npm install 后产生不可复现的依赖版本。
在数据库主机上创建专用数据库与账户,密码使用随机生成的长密码。示例账户仅能访问 laravel_app 库;成熟环境可进一步拆分运行账户和执行迁移的账户。
CREATE DATABASE laravel_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'laravel_app'@'127.0.0.1' IDENTIFIED BY 'REPLACE_WITH_LONG_RANDOM_PASSWORD';
GRANT ALL PRIVILEGES ON laravel_app.* TO 'laravel_app'@'127.0.0.1';
复制项目的 .env.example,再编辑 /srv/laravel-app/.env。以下只列关键字段,邮件、对象存储和第三方 API 仍按项目实际需求补齐:
APP_NAME=MyLaravelApp
APP_ENV=production
APP_DEBUG=false
APP_URL=https://app.example.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_app
DB_USERNAME=laravel_app
DB_PASSWORD=REPLACE_WITH_LONG_RANDOM_PASSWORD
QUEUE_CONNECTION=database
CACHE_STORE=database
SESSION_DRIVER=database
cd /srv/laravel-app
sudo -u deploy cp .env.example .env
# 用编辑器填入上面的生产配置后,再执行以下命令
sudo chown -R deploy:www-data /srv/laravel-app
sudo find storage bootstrap/cache -type d -exec chmod 2775 {} \;
sudo find storage bootstrap/cache -type f -exec chmod 664 {} \;
sudo chmod 640 .env
sudo -u deploy php artisan key:generate
APP_KEY 只在首次上线时生成,后续更新不能重新生成,否则已加密的 Cookie、会话或应用数据可能无法解密。把 .env 安全备份到站外;不要提交到 Git。Laravel 需要写入的是 storage 和 bootstrap/cache,不要对整个项目执行 chmod -R 777。如果应用把上传文件存在本地,再运行 sudo -u deploy php artisan storage:link,并把 storage/app/public 纳入备份。
检查项目是否已有队列、缓存和会话所需迁移,再迁移数据库。Laravel 新项目通常自带相应表迁移;老项目若缺少,应按所用 Laravel 版本的文档创建迁移,确认文件后再执行:
cd /srv/laravel-app
ls database/migrations
sudo -u deploy php artisan migrate --force
sudo -u deploy php artisan optimize
optimize 会缓存配置、路由和视图。更新 .env 后要重新执行;业务代码也应只在配置文件内调用 env(),避免配置缓存后读到空值。迁移前务必先做数据库备份,生产迁移可能无法简单反向执行。
把域名 app.example.com 的 A 记录指向 VPS。在 /etc/nginx/sites-available/laravel-app 写入下列站点配置:
server {
listen 80;
server_name app.example.com;
root /srv/laravel-app/public;
index index.php;
charset utf-8;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ \.php$ { return 404; }
location ~ /\.(?!well-known).* { deny all; }
}
sudo ln -s /etc/nginx/sites-available/laravel-app /etc/nginx/sites-enabled/laravel-app
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d app.example.com
curl -I https://app.example.com/up
sudo certbot renew --dry-run
root 必须是项目的 public,不能指向 /srv/laravel-app。否则 .env、源码和依赖可能暴露。证书签发前确认 DNS、云防火墙与主机防火墙允许 80/443。Laravel 默认 /up 健康路由只表明框架启动成功;需要验证 MySQL 或缓存时应增加独立检查,不能把 /up 的 200 当成整套业务正常。
只配置 QUEUE_CONNECTION=database 不会自动消费任务。若应用会发邮件、处理上传或执行异步 Job,在 /etc/supervisor/conf.d/laravel-worker.conf 配一个常驻 Worker:
[program:laravel-worker]
command=/usr/bin/php /srv/laravel-app/artisan queue:work database --sleep=3 --tries=3 --timeout=60 --max-time=3600
directory=/srv/laravel-app
user=deploy
numprocs=1
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
stopwaitsecs=120
redirect_stderr=true
stdout_logfile=/srv/laravel-app/storage/logs/worker.log
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status laravel-worker
cd /srv/laravel-app
sudo -u deploy php artisan queue:failed
--timeout=60 必须小于所用队列连接的 retry_after,否则同一 Job 可能被重复处理。长任务要同时调整两者及 Supervisor 的 stopwaitsecs。Worker 读到的代码会长期驻留内存,每次发布后需要 php artisan reload 或 queue:restart,并由 Supervisor 自动拉起。
Laravel Scheduler 只需要每分钟触发一次。以部署用户运行 crontab -e,加入一行:
* * * * * cd /srv/laravel-app && /usr/bin/php artisan schedule:run >> /srv/laravel-app/storage/logs/scheduler.log 2>&1
检查 sudo -u deploy crontab -l 与 sudo -u deploy php artisan schedule:list。如果应用没有定时任务,可先不配置。日志会增长,需配合轮转;VPS 的 logrotate 配置教程有具体做法。
每次上线记下旧提交号和数据库备份位置。下面是需要维护窗口的单机发布顺序,更新时不要再执行 key:generate:
cd /srv/laravel-app
git rev-parse HEAD # 记下旧版本
sudo -u deploy php artisan down
sudo -u deploy git fetch --tags
sudo -u deploy git switch --detach YOUR_TESTED_COMMIT_SHA
sudo -u deploy composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
# 若前端资源在服务器构建:以项目要求的 Node.js 版本执行 npm ci && npm run build
sudo -u deploy php artisan migrate --force
sudo -u deploy php artisan optimize
sudo -u deploy php artisan reload
sudo systemctl reload php8.3-fpm
sudo -u deploy php artisan up
curl -fsS https://app.example.com/up
备份至少有三份:数据库一致性备份、.env/APP_KEY 的加密副本、用户上传文件。先用 mysqldump --version 核对客户端;如果数据库采用单独的 MySQL 8.4 容器,应优先使用与其匹配的容器内客户端。给 /home/deploy/.my.cnf 写入仅备份所需的 MySQL [client] 连接参数,并设为 chmod 600;不要把密码直接写在命令行。以下命令假设该文件可连接 laravel_app 数据库:
sudo install -d -o deploy -g deploy -m 700 /var/backups/laravel
sudo -u deploy bash -c 'set -o pipefail; mysqldump --defaults-extra-file=/home/deploy/.my.cnf --single-transaction --routines --triggers --events laravel_app | gzip > /var/backups/laravel/db-$(date +%F-%H%M).sql.gz'
sudo -u deploy tar -C /srv/laravel-app -czf /var/backups/laravel/uploads-$(date +%F-%H%M).tgz storage/app/public
没有本地上传文件时可以省去最后一条。备份完成后将压缩包和加密保存的 .env 同步到另一台机器或对象存储;同一块 VPS 磁盘不算异地备份。定期在隔离的测试库执行 gunzip -c 数据库备份.sql.gz | mysql --defaults-extra-file=测试库凭据.cnf 测试库名,再恢复上传文件并验证关键页面。不要把正式库当成恢复演练目标。更完整的保留周期见VPS 自动备份方案。
回滚时切回上一个已测试提交,重新安装其锁定依赖与前端资源,重新优化配置并重载 Worker。代码回滚不等于数据库回滚:若新迁移删除列或改写数据,必须事先设计兼容迁移,或从备份恢复数据库和文件。不要在发现问题后直接执行 migrate:rollback,那可能回退本次以外的迁移或造成数据丢失。
| 现象 | 先看什么 | 常见原因 |
|---|---|---|
| 500 错误 | storage/logs/laravel.log、journalctl -u php8.3-fpm | .env、APP_KEY、数据库连接、目录权限或扩展缺失 |
| 502 Bad Gateway | nginx -t、PHP-FPM 状态和 Nginx error log | FPM 未启动、Socket 路径不对、Worker 资源耗尽 |
| 页面没样式 | public/build 与浏览器网络请求 | Vite 产物未构建或未部署 |
| 邮件和 Job 一直不执行 | supervisorctl status、queue:failed | Worker 未启动、队列连接不一致、失败任务积压 |
| 定时任务没跑 | 用户 crontab、schedule:list、scheduler.log | Cron 属于错误用户、服务器时区不符或任务自身失败 |
修改 .env 后没变化 | php artisan about、配置缓存 | 未重新执行 optimize,或业务代码在配置文件外调用 env() |
参考资料:Laravel 13 部署指南、Laravel 队列与 Supervisor、Laravel 定时任务。正式切 DNS 前,至少在测试域名上完成一次“迁移、HTTPS、队列消费、备份恢复、代码回滚”的闭环。
