5 Laravel Production Mistakes That Pass Every Local Test
本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。
5 Laravel Production Mistakes That Pass Every Local Test
最昂贵的 Laravel 生产环境错误往往是那些在你的笔记本电脑上运行完美的错误。在本地,你只有一个服务器、没有缓存文件、没有负载均衡器、有一个你想看到的调试页面,以及一个手动重启的队列工作进程。生产环境则完全相反。以下是五个表现完全如此的错误,解释了为什么它们只在部署后出现,以及针对每个错误的修复方法。下文所有内容均已根据 Laravel 文档和框架源码(v13.35.0 版本)进行了核对。
1. Calling env() outside config files
This works locally and returns null in production: // app/Services/PaymentGateway.php $key = env(‘STRIPE_SECRET’); // fine locally, null once config is cached The Laravel documentation is explicit: once configuration has been cached with config:cache, the .env file is not loaded, and calls to env() for .env variables return null. The framework source shows why: the bootstrapper that loads .env returns immediately when the configuration is cached. Since php artisan optimize runs config:cache (along with the event, route and view caches), most production deploys do this without anyone noticing.
1. 在配置文件之外调用 env()
这在本地运行正常,但在生产环境中会返回 null: // app/Services/PaymentGateway.php $key = env(‘STRIPE_SECRET’); // 本地正常,一旦配置被缓存则为 null Laravel 文档明确指出:一旦使用 config:cache 缓存了配置,.env 文件就不会被加载,此时对 .env 变量的 env() 调用将返回 null。框架源码揭示了原因:当配置被缓存时,加载 .env 的引导程序会立即返回。由于 php artisan optimize 会运行 config:cache(以及事件、路由和视图缓存),大多数生产环境部署都会在无人察觉的情况下执行此操作。
The fix: read env() only inside config/*.php, then use config() everywhere else. // config/services.php ‘stripe’ => [ ‘secret’ => env(‘STRIPE_SECRET’), ], // app/Services/PaymentGateway.php $key = config(‘services.stripe.secret’); To catch it before production, run php artisan config:cache locally and click through the feature, or run php artisan config:show services.stripe to see the value the app will really use. Remember to run php artisan config:clear afterwards.
修复方法:仅在 config/*.php 内部读取 env(),然后在其他任何地方使用 config()。 // config/services.php ‘stripe’ => [ ‘secret’ => env(‘STRIPE_SECRET’), ], // app/Services/PaymentGateway.php $key = config(‘services.stripe.secret’); 为了在上线前发现此问题,请在本地运行 php artisan config:cache 并点击测试该功能,或者运行 php artisan config:show services.stripe 来查看应用程序实际使用的值。记得事后运行 php artisan config:clear。
2. Leaving APP_DEBUG on
It is the setting you need every day in development, so it is easy to copy the same .env to a server. The documentation states it plainly: in production this value should always be false, and if it is true you risk exposing sensitive configuration values to your end users. An error page with a stack trace and environment details is a gift to anyone probing your site.
.env on the production server
APP_ENV=production APP_DEBUG=false Add a line to your deploy checklist, or a post-deploy smoke test that requests a URL which throws an error and checks the response contains no stack trace.
2. 保持 APP_DEBUG 开启
这是你在开发过程中每天都需要用到的设置,因此很容易将相同的 .env 文件复制到服务器上。文档明确指出:在生产环境中,该值应始终为 false;如果为 true,你就有可能向最终用户暴露敏感的配置值。带有堆栈跟踪和环境详细信息的错误页面对于任何探测你网站的人来说都是一份“大礼”。
生产服务器上的 .env
APP_ENV=production APP_DEBUG=false 在你的部署清单中增加一行,或者进行部署后的冒烟测试,请求一个会抛出错误的 URL,并检查响应中是否不包含堆栈跟踪。
3. Not trusting your load balancer
Behind a load balancer that terminates TLS, your app receives plain HTTP on port 80. Locally you never see this. In production, Laravel can generate http:// links on an HTTPS site, because it does not know it should trust the forwarded headers. The same problem affects anything that depends on the client IP, such as per-IP rate limiting, since every request can appear to come from the proxy.
3. 不信任你的负载均衡器
在终止 TLS 的负载均衡器后面,你的应用程序在 80 端口接收的是普通的 HTTP 请求。在本地你永远不会遇到这种情况。在生产环境中,Laravel 可能会在 HTTPS 站点上生成 http:// 链接,因为它不知道应该信任转发的标头。同样的问题也会影响任何依赖客户端 IP 的功能,例如基于 IP 的速率限制,因为每个请求看起来都像是来自代理服务器。
Per the documentation, configure trusted proxies in bootstrap/app.php: use Illuminate\Foundation\Configuration\Middleware; ->withMiddleware(function (Middleware $middleware): void { $middleware->trustProxies(at: [ ‘10.0.0.0/8’, // your load balancer range ]); }) If you are on a cloud load balancer and cannot know its addresses, the documentation allows ’*’ to trust all proxies. That is convenient but only safe when the app is reachable exclusively through the load balancer; otherwise a client can send forged forwarded headers directly. If your provider uses a different header (for example AWS ELB), set the headers argument as the documentation describes.
根据文档,在 bootstrap/app.php 中配置受信任的代理: use Illuminate\Foundation\Configuration\Middleware; ->withMiddleware(function (Middleware $middleware): void { $middleware->trustProxies(at: [ ‘10.0.0.0/8’, // 你的负载均衡器网段 ]); }) 如果你使用的是云负载均衡器且无法获知其地址,文档允许使用 ’*’ 来信任所有代理。这很方便,但只有在应用程序只能通过负载均衡器访问时才是安全的;否则,客户端可以直接发送伪造的转发标头。如果你的服务提供商使用不同的标头(例如 AWS ELB),请按照文档说明设置 headers 参数。
4. Queue workers still running the old code
Queue workers are long-lived processes that keep the booted application in memory, so they do not notice a new release. After a deploy your web requests run the new code while your workers keep running the old code, and jobs fail in ways that look impossible: a job class that no longer exists, a changed constructor, a renamed column.
4. 队列工作进程仍在运行旧代码
队列工作进程是长驻内存的进程,它们将已启动的应用程序保留在内存中,因此它们不会察觉到新版本的发布。部署后,你的 Web 请求运行的是新代码,而工作进程仍在运行旧代码,这会导致作业以看起来不可能的方式失败:例如作业类不再存在、构造函数已更改或列名已重命名。
The documentation says to restart the workers during your deployment: php artisan queue:restart This tells workers to exit gracefully after the current job, so you need a process manager such as Supervisor to start them again. The restart signal is stored in the cache, so a working cache driver is required. The current deployment documentation also describes php artisan reload, which restarts all long-running services in one command. In the 13.35 source it runs queue:restart and schedule:interrupt, plus any commands registered by packages, and accepts —except to skip some. On Laravel 12 or older, use queue:restart directly.
文档建议在部署期间重启工作进程: php artisan queue:restart 这会通知工作进程在完成当前作业后优雅地退出,因此你需要像 Supervisor 这样的进程管理器来重新启动它们。重启信号存储在缓存中,因此需要一个可用的缓存驱动程序。当前的部署文档还介绍了 php artisan reload,它可以通过一个命令重启所有长驻服务。在 13.35 版本的源码中,它会运行 queue:restart 和 schedule:interrupt,以及包注册的任何命令,并接受 —except 参数来跳过某些命令。在 Laravel 12 或更早版本中,请直接使用 queue:restart。
5. Running things on every server
With one local machine, a scheduled task runs once and a migration runs once. With three web servers, a deploy script and a scheduler can run on all three. Two defenses cover most of it. Migrations: add —isolated so only one server runs them, with the others skipping safely: php artisan migrate —force —isolated Scheduled tasks: mark them to run on a single server: use Illuminate\Support\Facades\Schedule; Schedule::command(‘report:generate’) ->fridays() ->at(‘17:00’) ->onOneServer();
5. 在每台服务器上运行任务
在本地单机环境下,计划任务运行一次,迁移也运行一次。但在拥有三台 Web 服务器的情况下,部署脚本和调度程序可能会在所有三台服务器上运行。有两种防御措施可以解决大部分问题。 迁移:添加 —isolated,这样只有一台服务器会运行它们,而其他服务器会安全地跳过: php artisan migrate —force —isolated 计划任务:标记它们在单台服务器上运行: use Illuminate\Support\Facades\Schedule; Schedule::command(‘report:generate’) ->fridays() ->at(‘17:00’) ->onOneServer();
Both rely on an atomic lock in the cache, so every server must use the same central cache. For onOneServer(), the documentation requires the default cache driver to be database, memcached, dynamodb or redis. A per-server file cache will not coordinate anything. Closures scheduled with Schedule::call() need a ->name() to be run on one server. If you are on Laravel 13.35 or later, Schedule::alwaysOnOneServer() applies this to every event, with the same cache requirement.
两者都依赖于缓存中的原子锁,因此每台服务器必须使用相同的中央缓存。对于 onOneServer(),文档要求默认缓存驱动程序必须是 database、memcached、dynamodb 或 redis。基于每台服务器的文件缓存无法进行任何协调。使用 Schedule::call() 调度的闭包需要一个 ->name() 才能在单台服务器上运行。如果你使用的是 Laravel 13.35 或更高版本,Schedule::alwaysOnOneServer() 会将此规则应用于每个事件,且具有相同的缓存要求。
A deploy script that avoids all five
composer install —no-dev —optimize-autoloader
php artisan migrate —force —isolated
php artisan optimize # caches config, events, routes and views
php artisan queue:restart # or php artisan reload on Laravel 13
避免上述五个错误的部署脚本
composer install —no-dev —optimize-autoloader
php artisan migrate —force —isolated
php artisan optimize # 缓存配置、事件、路由和视图
php artisan queue:restart # 或在 Laravel 13 上使用 php artisan reload
| Mistake | Symptom in production | Fix |
|---|---|---|
| env() in app code | Values are null after config:cache | Use config(); env() only in config/ |
| APP_DEBUG=true | Stack traces and config shown to users | APP_DEBUG=false |
| Proxies not trusted | http:// links, wrong client IP | trustProxies(at: …) |
| Workers not restarted | Jobs run old code | queue:restart in every deploy |
| Multi-server duplicates | Tasks and migrations run several times | —isolated, onOneServer() |
| 错误 | 生产环境症状 | 修复方法 |
|---|---|---|
| 在应用代码中使用 env() | config:cache 后值为 null | 使用 config();仅在 config/ 中使用 env() |
| APP_DEBUG=true | 向用户显示堆栈跟踪和配置 | APP_DEBUG=false |
| 未信任代理 | http:// 链接,错误的客户端 IP | trustProxies(at: …) |
| 未重启工作进程 | 作业运行旧代码 | 每次部署时运行 queue:restart |
| 多服务器重复执行 | 任务和迁移运行多次 | —isolated, onOneServer() |