<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: devTalk</title>
    <description>The latest articles on DEV Community by devTalk (@devtalk94).</description>
    <link>https://dev.to/devtalk94</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3228626%2F5495a7f0-c356-4253-865c-b83286ec1fe4.jpg</url>
      <title>DEV Community: devTalk</title>
      <link>https://dev.to/devtalk94</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devtalk94"/>
    <language>en</language>
    <item>
      <title>Secure a Laravel App Before Production: Tested Checklist</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Fri, 13 Nov 2026 03:38:46 +0000</pubDate>
      <link>https://dev.to/devtalk94/secure-a-laravel-app-before-production-tested-checklist-16a0</link>
      <guid>https://dev.to/devtalk94/secure-a-laravel-app-before-production-tested-checklist-16a0</guid>
      <description>&lt;p&gt;&lt;em&gt;Last updated: 8 November 2026. See the&lt;/em&gt;&lt;em&gt;update log&lt;/em&gt;&lt;em&gt;at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Laravel gives you safe defaults for a lot of things: Blade escapes output, the query builder uses bound parameters, forms need a CSRF token, and session IDs change on login. But a handful of protections only exist if you set them up, and the dangerous ones fail silently. Nothing breaks when debug mode is on, when the web server points at the wrong folder, or when a user can open someone else's record by changing a number in the URL. The app just works, until someone finds it.&lt;/p&gt;

&lt;p&gt;This article is a pre-launch checklist where every item was reproduced against a real Laravel 13.40.0 app over HTTP, so you see the actual requests, responses and numbers, including the defaults that are not safe. For each item you get the problem, the evidence, the fix, and how to check it afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Laravel handles for you, and what it does not
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;Out of the box&lt;/th&gt;
&lt;th&gt;Your job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;XSS in Blade&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{{ }}&lt;/code&gt; escapes&lt;/td&gt;
&lt;td&gt;Never use &lt;code&gt;{!! !!}&lt;/code&gt; on user input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL injection&lt;/td&gt;
&lt;td&gt;Bound parameters in the query builder and Eloquent&lt;/td&gt;
&lt;td&gt;Never interpolate variables into raw SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSRF&lt;/td&gt;
&lt;td&gt;Web routes reject a missing token with 419&lt;/td&gt;
&lt;td&gt;Be careful with exclusions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session fixation&lt;/td&gt;
&lt;td&gt;Session ID regenerated on login&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debug output&lt;/td&gt;
&lt;td&gt;Enabled in the default &lt;code&gt;.env&lt;/code&gt; file&lt;/td&gt;
&lt;td&gt;Turn it off in production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mass assignment&lt;/td&gt;
&lt;td&gt;The skeleton's &lt;code&gt;User&lt;/code&gt; model whitelists fields&lt;/td&gt;
&lt;td&gt;Your own models and request handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorization&lt;/td&gt;
&lt;td&gt;Nothing automatic&lt;/td&gt;
&lt;td&gt;Every route that loads a record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brute-force protection&lt;/td&gt;
&lt;td&gt;Throttle middleware is available but not applied to your routes&lt;/td&gt;
&lt;td&gt;Add it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security headers&lt;/td&gt;
&lt;td&gt;None sent&lt;/td&gt;
&lt;td&gt;Add a middleware or set them at the web server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxy and host trust&lt;/td&gt;
&lt;td&gt;Forwarded headers ignored by default&lt;/td&gt;
&lt;td&gt;Configure it correctly for your setup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. Turn off debug mode and set the environment
&lt;/h2&gt;

&lt;p&gt;A fresh Laravel app's &lt;code&gt;.env&lt;/code&gt; has &lt;code&gt;APP_ENV=local&lt;/code&gt; and &lt;code&gt;APP_DEBUG=true&lt;/code&gt;. In production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;APP_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production
&lt;span class="nv"&gt;APP_DEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I made a route that throws an exception and requested it both ways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_DEBUG=true:   status 500, 842,814 bytes, 110 references to absolute server paths,
                  stack trace with source code
APP_DEBUG=false:  status 500, 6,592 bytes, a generic "Server Error" page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The debug page told an attacker my directory layout, the framework internals and application code around the failure. I did not find &lt;code&gt;APP_KEY&lt;/code&gt; or database passwords in that page's text in this test, but file paths and code are already more than anyone should get from an error. Debug mode on staging servers that are reachable from the internet counts too. More on this in &lt;a href="https://dev-talk.com/laravel-production-mistakes" rel="noopener noreferrer"&gt;5 Laravel production mistakes that pass every local test&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Point the web server at public/, never the project root
&lt;/h2&gt;

&lt;p&gt;Only the &lt;code&gt;public&lt;/code&gt; folder should be reachable from the web. I served the project root instead of &lt;code&gt;public&lt;/code&gt; and requested &lt;code&gt;/.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;document root = project root:  GET /.env  -&amp;gt;  status 200, file contents returned (APP_KEY included)
document root = public/:       GET /.env  -&amp;gt;  status 404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A web server pointed at the wrong folder serves your secrets to the world. In Nginx, set the root to &lt;code&gt;public&lt;/code&gt; and refuse dotfiles as a second line of defense. This follows the structure of the standard Laravel Nginx config; I did not run Nginx for this article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;try_files&lt;/span&gt; $&lt;span class="n"&gt;uri&lt;/span&gt; $&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;$&lt;span class="n"&gt;query_string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;\&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?!&lt;/span&gt;&lt;span class="n"&gt;well&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;known&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;deny&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check it from outside after every infrastructure change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -s -o /dev/null -w &lt;span class="s2"&gt;"%{http_code}\n"&lt;/span&gt; https://your-site.example/.env
&lt;span class="c"&gt;# expect 404 or 403, never 200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Protect APP_KEY and plan for rotating it
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;APP_KEY&lt;/code&gt; encrypts cookies, sessions (if you encrypt them) and anything you pass through &lt;code&gt;Crypt&lt;/code&gt;. The skeleton's &lt;code&gt;.gitignore&lt;/code&gt; already ignores &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.env.backup&lt;/code&gt; and &lt;code&gt;.env.production&lt;/code&gt;; keep it that way, and keep keys out of build logs and chat messages. The skeleton's session config sets &lt;code&gt;'serialization' =&amp;gt; 'json'&lt;/code&gt;, and its comment explains that this guards against "gadget chain" attacks if the key leaks.&lt;/p&gt;

&lt;p&gt;If a key does leak, you need to replace it without logging everyone out or breaking stored encrypted data. Laravel reads old keys from &lt;code&gt;APP_PREVIOUS_KEYS&lt;/code&gt; (comma separated) in &lt;code&gt;config/app.php&lt;/code&gt;. I tested the mechanism with the framework's encrypter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;new key only:                  DecryptException: The MAC is invalid.
new key + previousKeys([old]): session-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan key:generate --show      &lt;span class="c"&gt;# prints a new key without writing it&lt;/span&gt;
&lt;span class="c"&gt;# then in .env:&lt;/span&gt;
&lt;span class="nv"&gt;APP_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;base64:NEW_KEY...
&lt;span class="nv"&gt;APP_PREVIOUS_KEYS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;base64:OLD_KEY...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--show&lt;/code&gt; option is listed in the command's help ("Display a new key instead of modifying files"). Remove old keys once the data encrypted with them has expired or been re-encrypted. For storing the file itself, &lt;code&gt;php artisan env:encrypt&lt;/code&gt; and &lt;code&gt;env:decrypt&lt;/code&gt; exist in 13.40 (I listed them but did not run them), which lets you commit an encrypted environment file and keep the decryption key elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Cache config in production, and stop calling env() outside config files
&lt;/h2&gt;

&lt;p&gt;This is partly a performance item and partly a security one, because it changes where your settings really come from. With the config cached, &lt;code&gt;env()&lt;/code&gt; outside &lt;code&gt;config/*.php&lt;/code&gt; no longer reads &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before config:cache:  {"env":"Laravel","config":"Laravel"}
after config:cache:   {"env":null,"config":"Laravel"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code like &lt;code&gt;if (env('APP_DEBUG'))&lt;/code&gt; in a controller then silently gets &lt;code&gt;null&lt;/code&gt;. Read settings with &lt;code&gt;config()&lt;/code&gt;, and put every &lt;code&gt;env()&lt;/code&gt; call in a config file. On deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan optimize
php artisan about &lt;span class="o"&gt;|&lt;/span&gt; grep -E &lt;span class="s2"&gt;"Environment|Debug Mode|Config|Routes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Environment ..................................................... production
Debug Mode ............................................................. OFF
Config .............................................................. CACHED
Routes .............................................................. CACHED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That output is from my test app with production settings. &lt;code&gt;about&lt;/code&gt; is a fast way to confirm what a server is actually running.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Force HTTPS and secure cookies
&lt;/h2&gt;

&lt;p&gt;The default session cookie is &lt;code&gt;HttpOnly&lt;/code&gt; with &lt;code&gt;SameSite=Lax&lt;/code&gt;, but it has no &lt;code&gt;Secure&lt;/code&gt; flag, so a browser will send it over plain HTTP too. This is what the default response sends (values removed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Set-Cookie: XSRF-TOKEN=...; path=/; samesite=lax
Set-Cookie: laravel-session=...; path=/; httponly; samesite=lax
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;SESSION_SECURE_COOKIE=true&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Set-Cookie: XSRF-TOKEN=...; path=/; secure; samesite=lax
Set-Cookie: laravel-session=...; path=/; secure; httponly; samesite=lax
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;XSRF-TOKEN&lt;/code&gt; cookie is not &lt;code&gt;HttpOnly&lt;/code&gt; by design, so JavaScript can read it for AJAX requests. Redirect HTTP to HTTPS at the load balancer or web server, and send an HSTS header (see item 8). Setting the secure flag before your site is fully on HTTPS locks out your own sessions on HTTP, so do the two together.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Trust proxies precisely, never blindly
&lt;/h2&gt;

&lt;p&gt;Behind a load balancer, the app must read the real client IP and scheme from &lt;code&gt;X-Forwarded-*&lt;/code&gt; headers, but only from your proxy. By default Laravel ignores them, which is safe but means &lt;code&gt;isSecure()&lt;/code&gt; is false and every request appears to come from the proxy's IP. The tempting fix is &lt;code&gt;trustProxies(at: '*')&lt;/code&gt;. I tested all three configurations, sending forged &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; and &lt;code&gt;X-Forwarded-For&lt;/code&gt; headers, then eight wrong logins (each with a different forged IP) against a route limited to five attempts per minute:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Forged headers accepted?&lt;/th&gt;
&lt;th&gt;8 login attempts with different forged IPs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default (none)&lt;/td&gt;
&lt;td&gt;No: &lt;code&gt;isSecure=false, ip=127.0.0.1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;422 422 422 422 422 &lt;strong&gt;429 429 429&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;at: '*'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes: &lt;code&gt;isSecure=true, ip=1.2.3.4&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;422 422 422 422 422 422 422 422 (&lt;strong&gt;throttle bypassed&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;at: '10.0.0.0/8'&lt;/code&gt; (not the sender)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;422 422 422 422 422 &lt;strong&gt;429 429 429&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// bootstrap/app.php&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Middleware&lt;/span&gt; &lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;trustProxies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'10.0.0.0/8'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// your load balancer's address range&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;at: '*'&lt;/code&gt; any client could pretend to be any IP, which defeats IP-based throttling and corrupts your logs. Using &lt;code&gt;'*'&lt;/code&gt; is only reasonable when the app can be reached exclusively through the proxy, for example inside a private network where the proxy is the only way in. Otherwise list your proxy's IP or CIDR range. The method also takes an array, which I did not test.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Trust your own hostnames
&lt;/h2&gt;

&lt;p&gt;Laravel builds URLs from the request's &lt;code&gt;Host&lt;/code&gt; header unless told otherwise. A password-reset email that uses &lt;code&gt;url()&lt;/code&gt; or &lt;code&gt;route()&lt;/code&gt; can then link to an attacker's domain if they send a forged &lt;code&gt;Host&lt;/code&gt;. I requested a route that returns &lt;code&gt;url('/reset-password/abc123')&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -s -H &lt;span class="s2"&gt;"Host: evil.example"&lt;/span&gt; http://127.0.0.1:8300/host
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"host":"evil.example","url":"http://evil.example/reset-password/abc123", ...}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is &lt;code&gt;trustHosts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;trustHosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'^app\.example\.com$'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_ENV=production, Host: evil.example     -&amp;gt; status 400
APP_ENV=production, Host: 127.0.0.1:8300   -&amp;gt; status 200 (an allowed host)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One trap I hit: the middleware does nothing when &lt;code&gt;APP_ENV=local&lt;/code&gt;. The framework's &lt;code&gt;shouldSpecifyTrustedHosts()&lt;/code&gt; returns false in the local environment and in unit tests, so in my first attempt the forged host still got a 200. Test it with production settings, or you will believe it works when it does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Send security headers
&lt;/h2&gt;

&lt;p&gt;Laravel sends none. A small middleware covers the common ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;?&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;\&lt;span class="nc"&gt;Http&lt;/span&gt;\&lt;span class="nc"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Closure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;\&lt;span class="nc"&gt;Http&lt;/span&gt;\&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony&lt;/span&gt;\&lt;span class="nc"&gt;Component&lt;/span&gt;\&lt;span class="nc"&gt;HttpFoundation&lt;/span&gt;\&lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecurityHeaders&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Closure&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Content-Type-Options'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'nosniff'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Frame-Options'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DENY'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Referrer-Policy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'strict-origin-when-cross-origin'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Permissions-Policy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'camera=(), microphone=(), geolocation=()'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isSecure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Strict-Transport-Security'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max-age=31536000; includeSubDomains'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// bootstrap/app.php&lt;/span&gt;
&lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;\&lt;span class="nc"&gt;App&lt;/span&gt;\&lt;span class="nc"&gt;Http&lt;/span&gt;\&lt;span class="nc"&gt;Middleware&lt;/span&gt;\&lt;span class="nc"&gt;SecurityHeaders&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HSTS only appears when the request is seen as secure, so it depends on item 6: behind an HTTPS-terminating proxy that you have not trusted, &lt;code&gt;isSecure()&lt;/code&gt; is false and the header is never sent (I confirmed both outcomes in the table above, with HSTS present only under &lt;code&gt;at: '*'&lt;/code&gt;). Start with a short &lt;code&gt;max-age&lt;/code&gt; while you test, because browsers remember HSTS. A &lt;code&gt;Content-Security-Policy&lt;/code&gt; is the strongest header against XSS, but it needs tuning to your scripts and styles, so I left it out of this generic example.&lt;/p&gt;

&lt;p&gt;The test server also sent &lt;code&gt;X-Powered-By: PHP/8.3.6&lt;/code&gt;. Set &lt;code&gt;expose_php = Off&lt;/code&gt; in &lt;code&gt;php.ini&lt;/code&gt; to stop advertising the version (standard PHP setting, not tested here).&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Keep CSRF protection on
&lt;/h2&gt;

&lt;p&gt;In Laravel 13.40 the web middleware group includes &lt;code&gt;PreventRequestForgery&lt;/code&gt;, and the older &lt;code&gt;VerifyCsrfToken&lt;/code&gt; class is a deprecated subclass of it. A POST without a token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -s -o /dev/null -w &lt;span class="s2"&gt;"%{http_code}\n"&lt;/span&gt; -X POST http://127.0.0.1:8300/ping
&lt;span class="c"&gt;# 419&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you must exclude a route, such as a payment provider's webhook, do it narrowly, and verify the sender some other way (a signature check), because an excluded route has no CSRF protection at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;preventRequestForgery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'webhooks/stripe'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The method also has &lt;code&gt;originOnly&lt;/code&gt; and &lt;code&gt;allowSameSite&lt;/code&gt; options in the 13.40 source; read their docs before changing them. If real users hit 419 errors, see &lt;a href="https://dev-talk.com/laravel-419-page-expired" rel="noopener noreferrer"&gt;Laravel 419 Page Expired: causes and fixes&lt;/a&gt; rather than turning the protection off.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Harden login: hashing, throttling, sessions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hashing.&lt;/strong&gt; The skeleton sets &lt;code&gt;BCRYPT_ROUNDS=12&lt;/code&gt;. Cost doubles with each extra round, so measure on your hardware. On this machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bcrypt rounds=10:   51 ms
bcrypt rounds=12:  198 ms
bcrypt rounds=14:  796 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are single runs; the pattern (about 4 times slower per two rounds) is what matters. Pick the highest cost your login endpoint can afford under load, and use &lt;code&gt;Hash::needsRehash()&lt;/code&gt; at login to upgrade old hashes as users sign in. PHP here also offered &lt;code&gt;argon2i&lt;/code&gt; and &lt;code&gt;argon2id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Throttling.&lt;/strong&gt; Login routes need a rate limit. The built-in middleware worked as expected on a route with &lt;code&gt;throttle:5,1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;attempt 1-5 -&amp;gt; 422  X-RateLimit-Limit: 5  X-RateLimit-Remaining: 4 ... 0
attempt 6   -&amp;gt; 429  Retry-After: 59
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For guests that limit is keyed by IP address, which is why item 6 matters. A more targeted limiter keys on the account as well. This is the standard form from the Laravel documentation, not something I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;\&lt;span class="nc"&gt;Cache&lt;/span&gt;\&lt;span class="nc"&gt;RateLimiting&lt;/span&gt;\&lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;\&lt;span class="nc"&gt;Support&lt;/span&gt;\&lt;span class="nc"&gt;Facades&lt;/span&gt;\&lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Route::post('/login', ...)-&amp;gt;middleware('throttle:login');&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For API limits and algorithms in depth, see &lt;a href="https://dev-talk.com/api-rate-limiting-strategies" rel="noopener noreferrer"&gt;API rate limiting strategies&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sessions.&lt;/strong&gt; &lt;code&gt;Auth::attempt()&lt;/code&gt; regenerates the session ID, which blocks session fixation. I checked it in the &lt;code&gt;sessions&lt;/code&gt; table: before login the newest row had a null &lt;code&gt;user_id&lt;/code&gt;; after login the ID was different and &lt;code&gt;user_id&lt;/code&gt; was set. In the source this is &lt;code&gt;$this-&amp;gt;session-&amp;gt;regenerate(true)&lt;/code&gt; in &lt;code&gt;SessionGuard::updateSession()&lt;/code&gt;. If you write your own login code, call &lt;code&gt;$request-&amp;gt;session()-&amp;gt;regenerate()&lt;/code&gt; yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Authorize every record access (the IDOR check)
&lt;/h2&gt;

&lt;p&gt;Authentication tells you who the user is. Authorization decides what they may touch. Route model binding fetches any record, for anyone, unless you check. I logged in as &lt;code&gt;alice&lt;/code&gt; and requested &lt;code&gt;bob&lt;/code&gt;'s record on two routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// No check: any logged-in user can read any user's email&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/users/{user}/email-unsafe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// With an ownership check&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/users/{user}/email-safe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Gate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;allowIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$auth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$auth&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alice reads bob, no check:      bob@example.test
alice reads bob, ownership check: status 403
alice reads herself:             alice@example.test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For real apps, use policies (&lt;code&gt;$this-&amp;gt;authorize('view', $invoice)&lt;/code&gt; or &lt;code&gt;Gate::authorize()&lt;/code&gt;) and write a test per resource that logs in as a second user and expects a 403. This is the bug most likely to survive review, because no error ever appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Block mass assignment
&lt;/h2&gt;

&lt;p&gt;Passing &lt;code&gt;$request-&amp;gt;all()&lt;/code&gt; into &lt;code&gt;create()&lt;/code&gt; lets a client set any column, including ones your form never showed. Laravel 13's skeleton &lt;code&gt;User&lt;/code&gt; model whitelists fields with an attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;#[Fillable(['name', 'email', 'password'])]&lt;/span&gt;
&lt;span class="c1"&gt;#[Hidden(['password', 'remember_token'])]&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Authenticatable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I compared that with an unguarded model on the same table (&lt;code&gt;protected $guarded = [];&lt;/code&gt;), posting &lt;code&gt;is_admin=1&lt;/code&gt; to both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Vulnerable&lt;/span&gt;
&lt;span class="nv"&gt;$u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LooseUser&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Safer: validate, and use only validated data on a model that whitelists fields&lt;/span&gt;
&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|string|max:100'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|email|unique:users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|min:12'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unguarded model + $request-&amp;gt;all():       {"is_admin":1}
validated data + whitelisted model:      {"is_admin":0}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat &lt;code&gt;$guarded = []&lt;/code&gt; as a bug unless the data is never from a request. In development, &lt;code&gt;Model::shouldBeStrict()&lt;/code&gt; turns silently discarded attributes into exceptions (see the &lt;a href="https://dev-talk.com/laravel-13-performance-optimization" rel="noopener noreferrer"&gt;performance article&lt;/a&gt; for how it behaves).&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Never interpolate into SQL, never print raw HTML
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SQL.&lt;/strong&gt; Bound parameters are the defense. Here are two raw queries, one with the input pasted into the string and one with a placeholder, given the classic payload &lt;code&gt;' OR '1'='1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Vulnerable&lt;/span&gt;
&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"select id, name from users where name = '{$request-&amp;gt;query('name')}'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Safe&lt;/span&gt;
&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'select id, name from users where name = ?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interpolated: [{"id":1,"name":"alice"},{"id":2,"name":"bob"}]   (every row returned)
bound:        []
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same rule applies to &lt;code&gt;whereRaw()&lt;/code&gt;, &lt;code&gt;selectRaw()&lt;/code&gt; and &lt;code&gt;orderByRaw()&lt;/code&gt;. Bindings protect values, not column names: if a user chooses the sort column, check it against an allow-list. See &lt;a href="https://dev-talk.com/eloquent-vs-query-builder" rel="noopener noreferrer"&gt;Eloquent vs Query Builder&lt;/a&gt; for when each fits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XSS.&lt;/strong&gt; I rendered &lt;code&gt;&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&lt;/code&gt; both ways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ $q }}     -&amp;gt; &amp;lt;p&amp;gt;escaped: &amp;amp;lt;script&amp;amp;gt;alert(1)&amp;amp;lt;/script&amp;amp;gt;&amp;lt;/p&amp;gt;
{!! $q !!}   -&amp;gt; &amp;lt;p&amp;gt;raw: &amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;{!! !!}&lt;/code&gt; only for HTML you generated yourself. If users may submit HTML, run it through a sanitizer first (I did not test one) and add a Content-Security-Policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. Treat uploads as hostile
&lt;/h2&gt;

&lt;p&gt;Never trust the file extension or the client's content type. I uploaded a real PNG, a PHP script renamed &lt;code&gt;evil.jpg&lt;/code&gt; (sent with &lt;code&gt;Content-Type: image/jpeg&lt;/code&gt;) and a file named &lt;code&gt;evil.php&lt;/code&gt;, against this validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'avatar'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'image'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'mimes:jpg,jpeg,png'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:2048'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'avatar'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'avatars'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;real.png  -&amp;gt; {"stored":"avatars/JNQgygKEpFyrUOQkyojulvOi4gEUyGBJvg1fq290.png"}
evil.jpg  -&amp;gt; "The avatar field must be an image." / "must be a file of type: jpg, jpeg, png."
evil.php  -&amp;gt; the same two errors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Laravel checks the file's real content, so renaming does not pass. Two more details from the result: the stored name is random (&lt;code&gt;store()&lt;/code&gt; generates it, so never use the client's filename), and the file landed in &lt;code&gt;storage/app/private/avatars/&lt;/code&gt;, outside the web root. Keep uploads on a private disk and serve them through a controller or signed URLs unless they must be public. Also set a request size limit at the web server and in PHP (&lt;code&gt;upload_max_filesize&lt;/code&gt;, &lt;code&gt;post_max_size&lt;/code&gt;), which I did not test.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. Audit dependencies and keep patching
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer install --no-dev --optimize-autoloader   &lt;span class="c"&gt;# no dev tools on the server&lt;/span&gt;
composer audit                                      &lt;span class="c"&gt;# check installed packages against known advisories&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;composer audit&lt;/code&gt; command is present in Composer 2.8.12 here. I could not run it, because it fetches advisories from Packagist, which this environment cannot reach. Run it in CI on every build and on a schedule, since new advisories appear for code you have not changed. Keep the framework current within your major version (this article's tests used 13.40.0), and read the release notes of security releases. If you are on Laravel 12, see the &lt;a href="https://dev-talk.com/upgrade-laravel-12-to-13" rel="noopener noreferrer"&gt;12 to 13 upgrade guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure items I did not test
&lt;/h2&gt;

&lt;p&gt;These matter just as much and live outside the Laravel app, so verify them in your own environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The database, Redis and any queue backend should not be reachable from the internet. Bind to private addresses and use a firewall or security group.&lt;/li&gt;
&lt;li&gt;Use separate database users for the app and for migrations, with the least privileges that work.&lt;/li&gt;
&lt;li&gt;Only &lt;code&gt;storage&lt;/code&gt; and &lt;code&gt;bootstrap/cache&lt;/code&gt; need to be writable by the web server user.&lt;/li&gt;
&lt;li&gt;Keep backups, and test restoring one.&lt;/li&gt;
&lt;li&gt;Do not log passwords, tokens or full request bodies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pre-launch checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;How to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;APP_ENV=production&lt;/code&gt;, &lt;code&gt;APP_DEBUG=false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;php artisan about&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Document root is &lt;code&gt;public/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl -I https://site/.env&lt;/code&gt; returns 404 or 403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Key not in git, rotation plan exists&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;.gitignore&lt;/code&gt;, note &lt;code&gt;APP_PREVIOUS_KEYS&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Config cached, no &lt;code&gt;env()&lt;/code&gt; outside config&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;php artisan optimize&lt;/code&gt;, search for &lt;code&gt;env(&lt;/code&gt; in &lt;code&gt;app/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;HTTPS redirect and secure cookies&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SESSION_SECURE_COOKIE=true&lt;/code&gt;, inspect &lt;code&gt;Set-Cookie&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Trusted proxies set to your proxy only&lt;/td&gt;
&lt;td&gt;Forged &lt;code&gt;X-Forwarded-For&lt;/code&gt; does not change &lt;code&gt;$request-&amp;gt;ip()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Trusted hosts set&lt;/td&gt;
&lt;td&gt;Forged &lt;code&gt;Host&lt;/code&gt; gets a 400 in production mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Security headers present&lt;/td&gt;
&lt;td&gt;`curl -sI &lt;a href="https://site" rel="noopener noreferrer"&gt;https://site&lt;/a&gt; \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;CSRF on, exclusions justified&lt;/td&gt;
&lt;td&gt;POST without a token returns 419&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Login throttled&lt;/td&gt;
&lt;td&gt;Sixth bad attempt returns 429&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Policies on every record route&lt;/td&gt;
&lt;td&gt;A second-user test expects 403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;No {% raw %}&lt;code&gt;$guarded = []&lt;/code&gt; with request data&lt;/td&gt;
&lt;td&gt;Search for &lt;code&gt;guarded&lt;/code&gt; and &lt;code&gt;$request-&amp;gt;all()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;No interpolated raw SQL, no raw Blade output&lt;/td&gt;
&lt;td&gt;Search for &lt;code&gt;Raw(&lt;/code&gt;, &lt;code&gt;DB::select("&lt;/code&gt; and &lt;code&gt;{!!&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Uploads validated and stored privately&lt;/td&gt;
&lt;td&gt;Upload a renamed script and expect a validation error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;composer audit&lt;/code&gt; clean in CI&lt;/td&gt;
&lt;td&gt;CI job output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Checking security only in the local environment.&lt;/strong&gt; Trusted hosts does nothing in &lt;code&gt;local&lt;/code&gt;, and debug is on by default there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using&lt;/strong&gt;&lt;code&gt;trustProxies(at: '*')&lt;/code&gt;&lt;strong&gt;because it makes HTTPS detection work.&lt;/strong&gt; It also makes IP addresses forgeable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authenticating but not authorizing.&lt;/strong&gt; &lt;code&gt;middleware('auth')&lt;/code&gt; only proves someone is logged in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disabling CSRF globally to fix a 419.&lt;/strong&gt; Find the cause instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validating by file extension.&lt;/strong&gt; Use content-based rules and a private disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relying on a WAF or CDN instead of fixing the app.&lt;/strong&gt; They reduce noise; they do not repair an IDOR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Committing&lt;/strong&gt;&lt;code&gt;.env.production&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; The skeleton ignores it for a reason.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Dev Talk articles and tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/post/api-rate-limiting-strategies" rel="noopener noreferrer"&gt;API rate limiting strategies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/post/laravel-cors-error-react-vue-frontend" rel="noopener noreferrer"&gt;Laravel CORS errors with a React or Vue frontend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/post/mysql-too-many-connections" rel="noopener noreferrer"&gt;MySQL "Too many connections"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools: &lt;code&gt;composer audit&lt;/code&gt;, &lt;code&gt;php artisan about&lt;/code&gt;, and &lt;code&gt;curl -I&lt;/code&gt; for checking headers and exposed files from outside. A static analyzer such as PHPStan or Larastan and a scanner like OWASP ZAP help too; I did not run them for this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Laravel secure by default?
&lt;/h3&gt;

&lt;p&gt;For the common injection and CSRF cases, yes, as the tests above show. It is not secure by default in the areas that depend on your configuration and your code: debug mode, web server layout, proxy and host trust, throttling, headers and authorization.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between APP_ENV and APP_DEBUG?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;APP_DEBUG&lt;/code&gt; controls whether detailed errors are shown. &lt;code&gt;APP_ENV&lt;/code&gt; names the environment, and some framework behavior depends on it, such as trusted hosts, which is skipped in &lt;code&gt;local&lt;/code&gt;. Set both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need all of these if I'm behind Cloudflare or a load balancer?
&lt;/h3&gt;

&lt;p&gt;Yes. A proxy does not know your business rules, so it cannot fix a missing authorization check, mass assignment or unsafe SQL. It also makes the proxy-trust settings more important, not less.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should staging have debug mode off too?
&lt;/h3&gt;

&lt;p&gt;If it is reachable from the internet, yes, and it should not contain real customer data.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should I run composer audit?
&lt;/h3&gt;

&lt;p&gt;On every CI build, plus a scheduled run (daily or weekly), because advisories are published for code you did not change.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/secure-laravel-applications-before-production" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel13</category>
      <category>security</category>
      <category>production</category>
      <category>csrf</category>
    </item>
    <item>
      <title>PostgreSQL vs MySQL Performance: Real Benchmarks Compared</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Thu, 12 Nov 2026 13:42:07 +0000</pubDate>
      <link>https://dev.to/devtalk94/postgresql-vs-mysql-performance-real-benchmarks-compared-51df</link>
      <guid>https://dev.to/devtalk94/postgresql-vs-mysql-performance-real-benchmarks-compared-51df</guid>
      <description>&lt;p&gt;&lt;em&gt;Last updated: 7 November 2026. See the&lt;/em&gt;&lt;em&gt;update log&lt;/em&gt;&lt;em&gt;at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Which is faster, PostgreSQL or MySQL?" has no single answer, because speed depends on the query, the data, the settings and the version. Most comparisons you will find change several of those at once. So I ran both on the same machine, loaded identical data, used matching memory settings, and measured typical web-application workloads: simple reads, mixed reads and writes, reporting queries, indexes, and what happens to a table under heavy updates. The results are below, along with the places where the numbers do not mean what they seem to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;What I measured&lt;/th&gt;
&lt;th&gt;Result in this test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple point reads&lt;/td&gt;
&lt;td&gt;sysbench, 8 connections&lt;/td&gt;
&lt;td&gt;PostgreSQL about 1.7 to 2.2 times the throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed read/write transactions&lt;/td&gt;
&lt;td&gt;sysbench, 8 connections&lt;/td&gt;
&lt;td&gt;PostgreSQL about 2.1 to 2.5 times the throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregates and joins over 2M rows&lt;/td&gt;
&lt;td&gt;GROUP BY, JOIN&lt;/td&gt;
&lt;td&gt;PostgreSQL 5 to 6 times faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table size after heavy updates&lt;/td&gt;
&lt;td&gt;5 full-table updates&lt;/td&gt;
&lt;td&gt;PostgreSQL grew 30 to 113 MB; MySQL stayed at 24.5 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single primary-key lookup from PHP&lt;/td&gt;
&lt;td&gt;one connection, sequential&lt;/td&gt;
&lt;td&gt;MySQL about 2 times faster (0.06 vs 0.12 ms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost of opening a connection&lt;/td&gt;
&lt;td&gt;PDO connect, localhost&lt;/td&gt;
&lt;td&gt;MySQL 0.16 ms, PostgreSQL 1.62 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is one small machine, one workload generator and two specific versions. Read the caveats before you quote any of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;2 vCPU, 8 GB RAM Linux container, client and servers on the same machine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;16.15 (Ubuntu package), &lt;code&gt;shared_buffers=1GB&lt;/code&gt;, &lt;code&gt;fsync=on&lt;/code&gt;, &lt;code&gt;synchronous_commit=on&lt;/code&gt;, autovacuum on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;8.0.46 (Ubuntu package), &lt;code&gt;innodb_buffer_pool_size=1G&lt;/code&gt;, &lt;code&gt;innodb_flush_log_at_trx_commit=1&lt;/code&gt;, binary log off (PostgreSQL has no equivalent second log, so I switched it off for parity)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load generator&lt;/td&gt;
&lt;td&gt;sysbench 1.0.20, 4 tables of 100,000 rows, 20 second runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other tests&lt;/td&gt;
&lt;td&gt;PHP 8.3.6 PDO, a 2,000,000-row table loaded identically into both (the &lt;code&gt;sum(amount)&lt;/code&gt; checksums matched)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both are close to default settings apart from memory. I did not tune either server for its workload. Versions matter here: MySQL 8.0 reached end of life on 30 April 2026, and the supported LTS lines are 8.4 and 9.7. PostgreSQL 16 is still supported (until November 2028), and 18 is the newest release I could confirm. Results on those newer versions may differ, so treat these as a snapshot of what Ubuntu 24.04 ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 1: OLTP throughput with sysbench
&lt;/h2&gt;

&lt;p&gt;sysbench's standard transactional workloads. TPS is transactions per second, so higher is better. The 8-thread figures are the first run; I repeated some workloads and give the range.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Threads&lt;/th&gt;
&lt;th&gt;PostgreSQL TPS&lt;/th&gt;
&lt;th&gt;MySQL TPS&lt;/th&gt;
&lt;th&gt;95th percentile latency (PG / MySQL)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;point_select&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;23,557&lt;/td&gt;
&lt;td&gt;17,646&lt;/td&gt;
&lt;td&gt;0.06 / 0.08 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;point_select&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;142,737 (range 120,940 to 142,737)&lt;/td&gt;
&lt;td&gt;85,080 (range 55,926 to 85,080)&lt;/td&gt;
&lt;td&gt;0.09 / 0.19 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;read_only&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1,146&lt;/td&gt;
&lt;td&gt;831&lt;/td&gt;
&lt;td&gt;1.03 / 1.39 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;read_only&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;5,250&lt;/td&gt;
&lt;td&gt;2,761&lt;/td&gt;
&lt;td&gt;1.86 / 4.65 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;read_write&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;710&lt;/td&gt;
&lt;td&gt;464&lt;/td&gt;
&lt;td&gt;1.70 / 2.91 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;read_write&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;1,963 (range 1,694 to 1,963)&lt;/td&gt;
&lt;td&gt;780 (range 762 to 789)&lt;/td&gt;
&lt;td&gt;6.43 / 18.61 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;write_only&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1,959&lt;/td&gt;
&lt;td&gt;1,280&lt;/td&gt;
&lt;td&gt;0.67 / 1.25 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;write_only&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6,246&lt;/td&gt;
&lt;td&gt;2,278&lt;/td&gt;
&lt;td&gt;2.03 / 7.98 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;PostgreSQL was ahead in every row. Three things to know about the numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run-to-run noise is real.&lt;/strong&gt; The point-select results varied by 20 to 30 percent between runs, because the load generator shares two CPUs with the database. The ratio stayed between 1.7 and 2.2, but do not quote the absolute figures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL's&lt;/strong&gt;&lt;code&gt;performance_schema&lt;/code&gt;&lt;strong&gt;was not the reason.&lt;/strong&gt; It is on by default and costs some throughput, so I restarted MySQL with it off and reran two workloads: 78,803 TPS for point_select and 789 for read_write, in line with the earlier runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL reported a few ignored errors under write load&lt;/strong&gt;: 13 in the 8-thread read_write run and 99 in write_only, out of roughly 39,000 and 125,000 transactions. The server log showed duplicate-key errors from concurrent delete-and-reinsert of the same row, which sysbench ignores by default. MySQL reported none. They are a tiny fraction of transactions and do not change the picture, but a real application would have to retry them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Result 2: reporting queries over 2 million rows
&lt;/h2&gt;

&lt;p&gt;Same table and same data in both (&lt;code&gt;events&lt;/code&gt;: id, user_id, kind, amount, created_at; plus a 50,000-row &lt;code&gt;users&lt;/code&gt; table). I timed each query nine times inside one persistent PHP PDO connection and took the median. No secondary indexes on &lt;code&gt;events&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;count(*)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;37 ms&lt;/td&gt;
&lt;td&gt;40 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GROUP BY kind&lt;/code&gt; with count, sum, avg&lt;/td&gt;
&lt;td&gt;120 ms&lt;/td&gt;
&lt;td&gt;616 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;JOIN users GROUP BY region&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;213 ms&lt;/td&gt;
&lt;td&gt;1,321 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ORDER BY id LIMIT 20 OFFSET 1900000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;84 ms&lt;/td&gt;
&lt;td&gt;200 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyset: &lt;code&gt;WHERE id &amp;gt; 1900000 ... LIMIT 20&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0.14 ms&lt;/td&gt;
&lt;td&gt;0.72 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary key lookup&lt;/td&gt;
&lt;td&gt;0.12 ms&lt;/td&gt;
&lt;td&gt;0.06 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Part of PostgreSQL's lead on the heavy queries is parallel query. With &lt;code&gt;set max_parallel_workers_per_gather=0&lt;/code&gt; its &lt;code&gt;GROUP BY&lt;/code&gt; went from 120 to 211 ms and the join from 213 to 378 ms, which is still well ahead of MySQL's single-threaded run. Two lessons from the pagination rows: a deep &lt;code&gt;OFFSET&lt;/code&gt; is slow on both (so use keyset pagination, as in &lt;a href="https://dev-talk.com/laravel-pagination-duplicate-rows-orderby" rel="noopener noreferrer"&gt;the Laravel pagination article&lt;/a&gt;), and MySQL is faster at the single primary-key lookup, though both are far below a millisecond.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 3: adding an index made MySQL's join slower
&lt;/h2&gt;

&lt;p&gt;This one surprised me, and it is why you should check plans instead of assuming. I created an index on &lt;code&gt;events(user_id)&lt;/code&gt; in both databases, ran &lt;code&gt;ANALYZE&lt;/code&gt;, and repeated the join:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;JOIN + GROUP BY region&lt;/th&gt;
&lt;th&gt;Before index&lt;/th&gt;
&lt;th&gt;After index on user_id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;213 ms&lt;/td&gt;
&lt;td&gt;203 ms (same plan: hash join)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL 8.0.46&lt;/td&gt;
&lt;td&gt;1,321 ms&lt;/td&gt;
&lt;td&gt;2,094 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MySQL's plan flipped. Before the index it scanned &lt;code&gt;events&lt;/code&gt; and looked up each row's user by primary key. After it, the optimizer chose to scan &lt;code&gt;users&lt;/code&gt; and do one index lookup into &lt;code&gt;events&lt;/code&gt; per user (50,000 lookups returning about 40 rows each), which its cost estimate rated cheaper and which was slower in practice. This is MySQL 8.0.46 only; I did not test 8.4 or 9.7. The index helped exactly the query it was made for: &lt;code&gt;WHERE user_id = 123&lt;/code&gt; took 0.08 ms in MySQL and 0.13 ms in PostgreSQL. Also in my measurements, MySQL's &lt;code&gt;count(*)&lt;/code&gt; went from 40 to 124 ms after the index was added, and I did not investigate why.&lt;/p&gt;

&lt;p&gt;Building the index took 421 ms in PostgreSQL and 1,922 ms in MySQL (single runs, wall time from the command-line clients).&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 4: what repeated updates do to the table
&lt;/h2&gt;

&lt;p&gt;PostgreSQL's design keeps old row versions in the table until VACUUM removes them. I took a 100,000-row table, disabled autovacuum for it to make the test deterministic, and ran &lt;code&gt;update sbtest1 set k = k + 1&lt;/code&gt; five times in both databases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                          PostgreSQL                         MySQL (InnoDB)
before:                   30 MB table, 4.7 MB indexes        24.5 MB table, 1.5 MB indexes
after 5 full updates:     113 MB table, 11 MB indexes        24.5 MB table, 5.0 MB indexes
                          dead_tuples = 500000
after plain VACUUM:       113 MB table, dead_tuples = 0
5 more updates:           114 MB table (space reused)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table in PostgreSQL grew to nearly four times its size, and a plain &lt;code&gt;VACUUM&lt;/code&gt; made the space reusable but did not shrink the file (&lt;code&gt;VACUUM FULL&lt;/code&gt; does, with a lock). In production autovacuum is on by default and normally keeps up, since I disabled it here to show the mechanism, but tables with heavy updates still need monitoring. In MySQL the table stayed the same size; the secondary index did grow, from 1.5 to 5.0 MB. The "before" sizes include some leftovers from the earlier benchmark runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 5: connections
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;new PDO connection (localhost, TCP):   PostgreSQL 1.62 ms    MySQL 0.16 ms
"select 1" on an open connection:      PostgreSQL 0.10 ms    MySQL 0.04 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opening a PostgreSQL connection was about ten times more expensive than a MySQL one, which matters if your application opens a connection per request without pooling. Under heavy traffic, put a pooler such as PgBouncer in front of PostgreSQL. I did not test PgBouncer. The "select 1" gap is mostly a driver and protocol effect: sysbench uses prepared statements, where PostgreSQL was faster per query, while PHP's plain &lt;code&gt;query()&lt;/code&gt; was faster on MySQL. Differences this small say more about the client than the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run this comparison on your own data
&lt;/h2&gt;

&lt;p&gt;Benchmark your own queries on your own schema. The commands I used, with the connection details changed to yours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# load the test tables (PostgreSQL shown; use --db-driver=mysql and --mysql-* options for MySQL)&lt;/span&gt;
sysbench oltp_read_write --db-driver=pgsql --pgsql-host=127.0.0.1 --pgsql-port=5432 \
  --pgsql-user=postgres --pgsql-db=sbtest --tables=4 --table-size=100000 prepare

&lt;span class="c"&gt;# run it&lt;/span&gt;
sysbench oltp_read_write --db-driver=pgsql --pgsql-host=127.0.0.1 --pgsql-port=5432 \
  --pgsql-user=postgres --pgsql-db=sbtest --tables=4 --table-size=100000 \
  --threads=8 --time=20 run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run each workload more than once, at more than one thread count, and read the "ignored errors" line. For your real queries, time them inside one open connection and read the plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- PostgreSQL&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- MySQL 8.0.18 and later&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- watch for dead rows in PostgreSQL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_live_tup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_dead_tup&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_user_tables&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n_dead_tup&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common mistakes when comparing databases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Comparing different settings.&lt;/strong&gt; Match buffer memory and durability, or you are comparing configurations, not databases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixing connection methods.&lt;/strong&gt; My first timings used a unix socket for one and TCP for the other, and the results were wrong. Use the same transport and a persistent connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing the client startup.&lt;/strong&gt; A new &lt;code&gt;psql&lt;/code&gt; or &lt;code&gt;mysql&lt;/code&gt; process per query measures process start, not the query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting&lt;/strong&gt;&lt;code&gt;ANALYZE&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; Planners need fresh statistics after a bulk load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting one run.&lt;/strong&gt; Repeat, and look at the spread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adding an index and not re-checking the plan.&lt;/strong&gt; As Result 3 shows, an index can change a plan for the worse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Benchmarking an end-of-life version.&lt;/strong&gt; MySQL 8.0 is past end of life; test the version you would actually deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Choosing: what the numbers do and do not say
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For reporting, aggregates and joins over millions of rows, PostgreSQL was clearly faster here, partly through parallel query.&lt;/li&gt;
&lt;li&gt;For many small, simple queries, both were fast, and MySQL was quicker on a single primary-key lookup and at opening connections.&lt;/li&gt;
&lt;li&gt;For tables with heavy updates, PostgreSQL needs vacuum monitoring and MySQL does not have that particular concern.&lt;/li&gt;
&lt;li&gt;Application fit usually matters more than these gaps: the team's experience, the tooling, the hosting options, and how close you already are to one of them. Laravel works with both. If you are already on MySQL 8.0, moving to 8.4 is the more urgent job (see &lt;a href="https://dev-talk.com/upgrade-mysql-8-0-to-8-4" rel="noopener noreferrer"&gt;the MySQL 8.0 to 8.4 upgrade guide&lt;/a&gt;), and most slow pages come from queries and indexes before they come from the database engine (see &lt;a href="https://dev-talk.com/laravel-13-performance-optimization" rel="noopener noreferrer"&gt;Laravel 13 performance optimization&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Dev Talk articles and tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/post/upgrade-mysql-8-0-to-8-4" rel="noopener noreferrer"&gt;Upgrade MySQL 8.0 to 8.4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/post/mysql-too-many-connections" rel="noopener noreferrer"&gt;MySQL "Too many connections"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools: sysbench for load tests, &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; for plans, and PgBouncer for PostgreSQL connection pooling (not tested here).&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is PostgreSQL faster than MySQL?
&lt;/h3&gt;

&lt;p&gt;In this test, with near-default settings on a small machine, PostgreSQL 16 was faster on every sysbench workload and on aggregate and join queries. That is not a law. It depends on version, settings, hardware and workload, so test with your own queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I migrate from MySQL to PostgreSQL for speed?
&lt;/h3&gt;

&lt;p&gt;Not on these numbers alone. Fix slow queries and missing indexes first, and weigh migration cost, tooling and team experience. If you are on MySQL 8.0, upgrading to a supported version is the priority.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Laravel work with both?
&lt;/h3&gt;

&lt;p&gt;Yes, the framework supports both databases. Check the Laravel documentation for any feature that is specific to one of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about MariaDB?
&lt;/h3&gt;

&lt;p&gt;I did not test it, so I have no numbers for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Would you get the same results on bigger hardware?
&lt;/h3&gt;

&lt;p&gt;I do not know. I ran on 2 vCPUs with the load generator on the same machine; multi-core servers, faster disks and separate client machines could change the ratios.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/postgresql-vs-mysql-performance-comparison" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>mysql</category>
      <category>performance</category>
      <category>benchmarks</category>
    </item>
    <item>
      <title>How to Detect and Fix PHP Memory Leaks</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Thu, 12 Nov 2026 03:24:24 +0000</pubDate>
      <link>https://dev.to/devtalk94/how-to-detect-and-fix-php-memory-leaks-4e14</link>
      <guid>https://dev.to/devtalk94/how-to-detect-and-fix-php-memory-leaks-4e14</guid>
      <description>&lt;p&gt;&lt;em&gt;Last updated: 5 November 2026. See the&lt;/em&gt;&lt;em&gt;update log&lt;/em&gt;&lt;em&gt;at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In a normal PHP-FPM request, a memory leak mostly does not exist: PHP frees everything when the request ends. The leaks people actually hit happen in processes that stay alive: queue workers, daemons, long import scripts, and application servers like Laravel Octane. The symptom is always the same. Memory climbs a little with every job or request, then something dies with &lt;code&gt;Allowed memory size exhausted&lt;/code&gt;, or the operating system kills the process, hours after the code that caused it ran. This article shows how to confirm that you have a leak, the four causes I would check first (each reproduced and measured), how to find the guilty objects, and how to contain a leak you cannot fix immediately.&lt;/p&gt;

&lt;p&gt;If your problem is one request using too much memory in a single go, that is not a leak. See &lt;a href="https://dev-talk.com/php-allowed-memory-size-exhausted" rel="noopener noreferrer"&gt;PHP "Allowed memory size exhausted"&lt;/a&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested
&lt;/h2&gt;

&lt;p&gt;PHP 8.3.6 CLI, and the Laravel 13.35 database and events code (the real classes, run outside a full application) on SQLite. All memory figures come from &lt;code&gt;memory_get_usage()&lt;/code&gt; unless stated. Each result is one run; the growth patterns are what matter, not the exact megabytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Confirm it is a leak, not just a big job
&lt;/h2&gt;

&lt;p&gt;A leak is memory that keeps growing as you repeat the same work. Process the same unit of work many times and print memory every so often. If it plateaus, you do not have a leak. If it rises in a straight line, you do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;?&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$label&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%s: %.1f MB (peak %.1f MB)\n"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;memory_get_usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1048576&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;memory_get_peak_usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1048576&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$round&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$round&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$round&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;handleOneJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// your code here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;gc_collect_cycles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// so cycles don't hide the real result&lt;/span&gt;
    &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"after round $round"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;gc_collect_cycles()&lt;/code&gt; call matters. It frees unreachable circular references, so whatever growth remains afterwards is memory that your code is still holding on to. Three PHP functions worth knowing while you measure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;memory_get_usage()&lt;/code&gt; is what your script has allocated. &lt;code&gt;memory_get_usage(true)&lt;/code&gt; is what PHP has reserved from the system, in larger chunks. In my test with one 5 MB string they reported 5.4 MB and 7.0 MB. Laravel's queue worker checks the &lt;code&gt;true&lt;/code&gt; value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory_get_peak_usage()&lt;/code&gt; is the high-water mark. Since PHP 8.2, &lt;code&gt;memory_reset_peak_usage()&lt;/code&gt; resets it so you can measure one phase at a time. My test: a freed 50 MB string left the peak at 50.4 MB, and after the reset the peak read 0.4 MB.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cause 1: a static array, singleton or global that only grows
&lt;/h2&gt;

&lt;p&gt;The classic leak. Something stores data "for later" and nothing ever removes it. I simulated 50,000 units of work, each storing a 1 KB payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Cache&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nv"&gt;$items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// grows forever&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unbounded static cache:      after 10000: 12.9 MB ... after 50000: 62.5 MB
bounded cache (max 1000):    after 10000:  1.7 MB ... after 50000:  1.7 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fixes, in order of preference: do not cache at all if the data is cheap to reload; bound the cache (a maximum size that evicts the oldest entry); or tie the lifetime of cached data to the object it belongs to with &lt;code&gt;WeakMap&lt;/code&gt;, which drops an entry when its key object is freed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WeakMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;stdClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'expensive'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'data'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// 1&lt;/span&gt;
&lt;span class="nb"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Laravel the same trap hides in singletons registered in the container, static properties on services, and traits with static state. They are harmless under PHP-FPM and dangerous under Octane or &lt;code&gt;queue:work&lt;/code&gt;. The Octane documentation (the 9.x docs page; check the current one) gives exactly this example: adding data to a static array on each request is a memory leak, because Octane keeps the application in memory between requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 2: circular references, and why PHP's garbage collector does not always save you
&lt;/h2&gt;

&lt;p&gt;PHP frees most values the moment nothing references them. Objects that reference each other never reach zero, so PHP has a separate cycle collector. It works, and that is why cycles are less often the culprit than people expect. I created 100,000 pairs of objects pointing at each other and threw them away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GC enabled:  after 100000: 2.5 MB   gc runs=19  collected=285000
GC disabled: after 100000: 42.1 MB  gc runs=0   roots=200000
             manual gc_collect_cycles() freed 300000; memory back to 2.4 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So with the collector on, those cycles cost nothing lasting. The catch is when it runs. The collector triggers when the number of possible cycle roots reaches a threshold (&lt;code&gt;gc_status()['threshold']&lt;/code&gt; was 10001 in my test), not when memory gets high. Large objects in cycles can therefore exhaust memory before the collector ever wakes up. I created objects that referenced themselves and each held 200 KB, with a 128 MB limit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GC enabled, large self-referencing objects:
iteration 500: 96.2 MB
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted

same loop with gc_collect_cycles() every 100 iterations:
iteration 3000: 0.4 MB   finished
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a loop that creates big cyclic structures (parsed documents, large object graphs), call &lt;code&gt;gc_collect_cycles()&lt;/code&gt; every few hundred iterations, or break the cycle. Do not call it on every iteration and do not turn the collector off: it has a cost, and disabling it just made the first test above 17 times worse.&lt;/p&gt;

&lt;h3&gt;
  
  
  The sneaky cycle: a closure that captures $this
&lt;/h3&gt;

&lt;p&gt;An arrow function or closure created inside a method captures &lt;code&gt;$this&lt;/code&gt; automatically. If you store that closure on the same object, you have built a cycle. I tested with &lt;code&gt;WeakReference&lt;/code&gt; to see when the object was really freed, with the collector disabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Report&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$cb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$static&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str_repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$static&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;capturing closure: freed right after unset? NO (waits for GC)
static closure:    freed right after unset? yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a callback does not use &lt;code&gt;$this&lt;/code&gt;, declare it &lt;code&gt;static&lt;/code&gt;. It cannot create the cycle, and the object is released at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 3: listeners and callbacks registered over and over
&lt;/h2&gt;

&lt;p&gt;Registering an event listener inside code that runs per job or per request adds a new listener every time. Each one holds a closure and everything that closure captured. I registered one listener per "job" on Laravel's event dispatcher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;registered once:    after 20000 jobs: 0.9 MB   listeners=1
registered per job: after 20000 jobs: 16.2 MB  listeners=20000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Memory grew in a straight line, and every event dispatch also gets slower because it now calls 20,000 listeners. In Laravel, register listeners once, in a service provider's &lt;code&gt;boot()&lt;/code&gt; method or the event-to-listener mapping, never inside a job's &lt;code&gt;handle()&lt;/code&gt;, a controller, or a loop. The same applies to &lt;code&gt;Model::observe()&lt;/code&gt;, macros, and any &lt;code&gt;on()&lt;/code&gt;/&lt;code&gt;listen()&lt;/code&gt; call on a long-lived object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 4: the query log in a long-running process
&lt;/h2&gt;

&lt;p&gt;Laravel can record every query. It is off by default, which is why this shows up only when someone turns it on for debugging and forgets, or a package does. In a worker or a long command it grows without limit. I ran 20,000 trivial queries through the database connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query log off: after 20000 queries: 4.8 MB  log entries=0
query log on:  after 20000 queries: 18.2 MB  log entries=20000
after DB::connection()-&amp;gt;flushQueryLog(): 4.8 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// if you need the log inside a long loop, clear it as you go&lt;/span&gt;
&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;enableQueryLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ids&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... work ...&lt;/span&gt;
    &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;flushQueryLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;disableQueryLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search your code and your debugging packages for &lt;code&gt;enableQueryLog&lt;/code&gt;. A related case is not a leak but looks like one: loading whole tables into memory. Use &lt;code&gt;chunkById()&lt;/code&gt;, &lt;code&gt;lazyById()&lt;/code&gt; or &lt;code&gt;cursor()&lt;/code&gt;. I measured 54 MB for &lt;code&gt;get()&lt;/code&gt; on 20,000 rows against 8 to 12 MB for the streaming options in the &lt;a href="https://dev-talk.com/laravel-13-performance-optimization" rel="noopener noreferrer"&gt;Laravel 13 performance article&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find out which objects are being kept: php-meminfo
&lt;/h2&gt;

&lt;p&gt;When the usual suspects do not explain it, you need to see what is in memory. The php-meminfo extension dumps every live item in a process to JSON. I built it from source against PHP 8.3.6 (&lt;code&gt;phpize&lt;/code&gt;, &lt;code&gt;./configure --enable-meminfo&lt;/code&gt;, &lt;code&gt;make&lt;/code&gt;) and ran a dump after creating 1,500 objects that were kept and 300 that were not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// php -d extension=/path/to/meminfo.so script.php&lt;/span&gt;
&lt;span class="nv"&gt;$fh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/tmp/dump.json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'w'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;meminfo_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;fclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Counting the objects by class in that dump:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[('Order', 1500), ('Tmp', 1)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The class that has far more instances than it should is your leak. The project ships an analyzer for this; its README documents the commands &lt;code&gt;bin/analyzer summary&lt;/code&gt;, &lt;code&gt;top-children&lt;/code&gt;, &lt;code&gt;query -f "class=Order"&lt;/code&gt; and &lt;code&gt;ref-path &amp;lt;item-id&amp;gt;&lt;/code&gt;, where &lt;code&gt;ref-path&lt;/code&gt; shows what is still referencing a given object. I could not run the analyzer (it needs Composer packages that I could not download), so I counted classes with a short script instead.&lt;/p&gt;

&lt;p&gt;Two warnings. The README lists PHP 7.x and 8.0 and the last commit I saw was from March 2022, so treat it as an unmaintained tool: it worked for a dump on 8.3.6 in my test, but try it on a staging copy first. And dumps of a real worker are large, so never run it in production traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contain a leak you cannot fix today
&lt;/h2&gt;

&lt;p&gt;Recycling the process is a legitimate stopgap, as long as you treat it as a stopgap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Laravel queue workers.&lt;/strong&gt; From the Laravel 13 source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan queue:work --memory=256 --max-jobs=1000 --max-time=3600
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--memory&lt;/code&gt; defaults to 128 (MB). In the 13.x source it also accepts a percentage of PHP's &lt;code&gt;memory_limit&lt;/code&gt;, such as &lt;code&gt;50%&lt;/code&gt;. The worker compares &lt;code&gt;memory_get_usage(true)&lt;/code&gt; in MB with the limit, and when it is reached the worker exits with status code 12.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--max-jobs&lt;/code&gt; and &lt;code&gt;--max-time&lt;/code&gt; stop the worker after that many jobs or seconds.&lt;/li&gt;
&lt;li&gt;A stopped worker needs something to start it again, such as Supervisor or systemd. See &lt;a href="https://dev-talk.com/laravel-queue-jobs-not-processing" rel="noopener noreferrer"&gt;Laravel queue jobs not processing or stuck&lt;/a&gt;. Set &lt;code&gt;--memory&lt;/code&gt; below the PHP &lt;code&gt;memory_limit&lt;/code&gt;, so the worker exits cleanly between jobs instead of dying in the middle of one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;PHP-FPM.&lt;/strong&gt; In a pool configuration, &lt;code&gt;pm.max_requests&lt;/code&gt; is the number of requests a child handles before it is respawned. The PHP manual says this can be useful to work around memory leaks in third-party libraries, and the default is 0, meaning never respawn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="na"&gt;pm.max_requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Octane.&lt;/strong&gt; The 9.x documentation says Octane restarts a worker after 500 requests by default, adjustable with &lt;code&gt;php artisan octane:start --max-requests=250&lt;/code&gt;. I did not run Octane for this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trusting a single measurement.&lt;/strong&gt; One reading proves nothing. Repeat the work and watch the trend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measuring without collecting cycles first.&lt;/strong&gt; You may be chasing garbage that PHP was about to free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calling&lt;/strong&gt;&lt;code&gt;unset()&lt;/code&gt;&lt;strong&gt;and expecting the process size to shrink.&lt;/strong&gt; PHP reuses freed memory internally. Judge by &lt;code&gt;memory_get_usage()&lt;/code&gt; across rounds, not by the process size in &lt;code&gt;top&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disabling the garbage collector for speed.&lt;/strong&gt; My cycle test shows what that costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raising&lt;/strong&gt;&lt;code&gt;memory_limit&lt;/code&gt;&lt;strong&gt;instead of finding the growth.&lt;/strong&gt; It only delays the crash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaving debug tools on.&lt;/strong&gt; Query logging, event recording and similar tools hold everything they record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating restarts as the fix.&lt;/strong&gt; &lt;code&gt;--max-jobs&lt;/code&gt; and &lt;code&gt;pm.max_requests&lt;/code&gt; hide the leak, and a leak that grows fast enough still kills the process between restarts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Dev Talk articles and tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/post/php-allowed-memory-size-exhausted" rel="noopener noreferrer"&gt;PHP "Allowed memory size exhausted"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/post/laravel-queue-jobs-not-processing" rel="noopener noreferrer"&gt;Laravel queue jobs not processing or stuck&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/post/docker-build-taking-too-long" rel="noopener noreferrer"&gt;Docker build taking too long? 10 ways to speed it up&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools: &lt;code&gt;memory_get_usage()&lt;/code&gt;, &lt;code&gt;memory_reset_peak_usage()&lt;/code&gt; and &lt;code&gt;gc_status()&lt;/code&gt; (built in and used above), the php-meminfo extension, and a memory-capable profiler such as Blackfire. I did not use a profiler for this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does PHP have memory leaks?
&lt;/h3&gt;

&lt;p&gt;PHP itself frees all request memory at the end of a request, so ordinary PHP-FPM apps rarely leak in the long-term sense. Long-running processes can accumulate memory through your code (static data, listeners, logs), through reference cycles that have not been collected yet, and occasionally through bugs in extensions or libraries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I call gc_collect_cycles() in my code?
&lt;/h3&gt;

&lt;p&gt;Only in long loops that create large cyclic structures, and every few hundred iterations rather than every iteration. My test showed it can be the difference between finishing and a fatal error. For small objects the automatic collector is enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does the process memory in top not go down after unset()?
&lt;/h3&gt;

&lt;p&gt;PHP keeps freed memory to reuse it. &lt;code&gt;memory_get_usage(true)&lt;/code&gt; reports memory PHP has reserved, which is larger than what your data uses (7.0 against 5.4 MB in my test). Judge growth by the trend across rounds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Xdebug the right tool for finding leaks?
&lt;/h3&gt;

&lt;p&gt;I did not test it for this. I used built-in memory functions to find the trend and php-meminfo to see what is held, so use those first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a memory limit on queue workers a real fix?
&lt;/h3&gt;

&lt;p&gt;No. It protects you from a crash by restarting cleanly. Use it as a safety net while you find the cause.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/detect-fix-memory-leaks-php-applications" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>memoryleaks</category>
      <category>laravel</category>
      <category>queueworkers</category>
    </item>
    <item>
      <title>Docker Build Too Slow? 10 Ways to Speed It Up</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Wed, 11 Nov 2026 14:53:07 +0000</pubDate>
      <link>https://dev.to/devtalk94/docker-build-too-slow-10-ways-to-speed-it-up-4aon</link>
      <guid>https://dev.to/devtalk94/docker-build-too-slow-10-ways-to-speed-it-up-4aon</guid>
      <description>&lt;p&gt;A slow Docker build usually has one of a few causes: you are sending far more files to the builder than you need, a cheap change at the top of the Dockerfile throws away an expensive step below it, or every build re-downloads packages it fetched an hour ago. All of these are fixable without changing your application. This article goes through ten fixes in the order I would try them. Most of them I ran and timed on a real Docker Engine, and I show the output, so you can compare it with what you see on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: how I tested, and what the numbers mean
&lt;/h2&gt;

&lt;p&gt;Docker Engine 29.8.2, Buildx 0.37.1, BuildKit 0.33.1, on Linux with the containerd image store. The environment could not pull images from Docker Hub, so I built a tiny local base image (a shell plus a few core tools) and used &lt;code&gt;sleep 4&lt;/code&gt; as a stand-in for a slow dependency install. That makes the cache behaviour real, because the cache does not care what a step does, but the absolute timings are not what &lt;code&gt;npm install&lt;/code&gt; or &lt;code&gt;composer install&lt;/code&gt; will take on your project. Every timing is a single run. Look at the ratios, then measure your own project.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Find out which step is slow before changing anything
&lt;/h2&gt;

&lt;p&gt;Run the build with plain progress output. It prints each step and how long it took, and marks steps served from cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build --progress=plain -t myapp .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#6 [2/5] WORKDIR /app
#6 CACHED
#8 [4/5] RUN sleep 4 &amp;amp;&amp;amp; mkdir -p node_modules
#8 CACHED
#9 [5/5] COPY src ./src
#9 DONE 0.0s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for three things: &lt;code&gt;transferring context&lt;/code&gt; with a large size (see technique 2), steps near the top that are not &lt;code&gt;CACHED&lt;/code&gt; on a rebuild (techniques 3 and 9), and a single step that dominates the total (techniques 4 and 5). The default fancy progress view hides most of this, so use &lt;code&gt;--progress=plain&lt;/code&gt; while you investigate.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Add a .dockerignore file
&lt;/h2&gt;

&lt;p&gt;When your Dockerfile has &lt;code&gt;COPY . .&lt;/code&gt;, Docker has to send everything in the build directory to the builder: &lt;code&gt;.git&lt;/code&gt;, &lt;code&gt;node_modules&lt;/code&gt;, logs, local build output. I made a project with 450 MB of that kind of material and one small source file, then built with &lt;code&gt;COPY . .&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;without .dockerignore:  transferring context: 472.0MB   total 33.9 s
with .dockerignore:     transferring context: 254B       total  1.0 s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A starting point for a Laravel project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.git
node_modules
vendor
storage/logs
storage/framework/cache
storage/framework/sessions
storage/framework/views
.env
.env.*
*.log
.idea
.vscode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Excluding &lt;code&gt;.env&lt;/code&gt; matters for more than speed: otherwise &lt;code&gt;COPY . .&lt;/code&gt; bakes your local secrets into an image layer. Only exclude &lt;code&gt;vendor&lt;/code&gt; if the image installs its own dependencies, as in the example near the end. One detail from my testing: BuildKit transfers only the files a &lt;code&gt;COPY&lt;/code&gt; actually references. With &lt;code&gt;COPY src ./src&lt;/code&gt; the same 450 MB project transferred 79 bytes, so the huge-context problem shows up with &lt;code&gt;COPY . .&lt;/code&gt;, which is what most Dockerfiles use.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Order layers from least to most frequently changed
&lt;/h2&gt;

&lt;p&gt;Docker reuses a cached layer only if the instruction and everything before it are unchanged. If &lt;code&gt;COPY . .&lt;/code&gt; comes first, any source edit invalidates the dependency install after it. Here is the same 4-second "install" step in both orders, timed after changing one line of source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Slow: source copied first&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; mini:1
&lt;span class="k"&gt;WORKDIR&lt;/span&gt; /app
&lt;span class="k"&gt;COPY&lt;/span&gt; . .
&lt;span class="k"&gt;RUN&lt;/span&gt; sleep 4 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mkdir -p node_modules

&lt;span class="c"&gt;# Fast: manifest first, install, then source&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; mini:1
&lt;span class="k"&gt;WORKDIR&lt;/span&gt; /app
&lt;span class="k"&gt;COPY&lt;/span&gt; package.json .
&lt;span class="k"&gt;RUN&lt;/span&gt; sleep 4 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mkdir -p node_modules
&lt;span class="k"&gt;COPY&lt;/span&gt; src ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;slow order, after a source edit: 4.6 s
fast order, after a source edit: 0.3 s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For real projects this means copying &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt;, or &lt;code&gt;composer.json&lt;/code&gt; and &lt;code&gt;composer.lock&lt;/code&gt;, before the rest of the code, and running the install in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Use cache mounts for package downloads
&lt;/h2&gt;

&lt;p&gt;Layer caching helps until the manifest changes. Add one dependency and the whole install reruns, re-downloading everything. A BuildKit cache mount keeps the package manager's download cache between builds, outside the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt; package.json package-lock.json ./
&lt;span class="k"&gt;RUN&lt;/span&gt; --mount=type=cache,target=/root/.npm npm ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tested the mechanism with a step that "downloads" for 4 seconds only when its cache directory is empty, and changed &lt;code&gt;package.json&lt;/code&gt; between builds so the layer was invalidated each time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;without cache mount: first 4.7 s | after package.json change 4.5 s
with cache mount:    first 4.7 s | after package.json change 0.5 s
after docker builder prune -af:   4.7 s again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Docker documentation lists cache targets for common tools: &lt;code&gt;/root/.npm&lt;/code&gt; for npm, &lt;code&gt;/root/.cache/pip&lt;/code&gt; for pip, &lt;code&gt;/root/.gem&lt;/code&gt; for Bundler, &lt;code&gt;/go/pkg/mod&lt;/code&gt; and &lt;code&gt;/root/.cache/go-build&lt;/code&gt; for Go, &lt;code&gt;/tmp/cache&lt;/code&gt; for Composer, and for apt &lt;code&gt;/var/cache/apt&lt;/code&gt; and &lt;code&gt;/var/lib/apt&lt;/code&gt; with &lt;code&gt;sharing=locked&lt;/code&gt;. For Composer I would set the directory explicitly rather than rely on a default, using Composer's documented &lt;code&gt;COMPOSER_CACHE_DIR&lt;/code&gt; variable (I confirmed that the variable changes &lt;code&gt;composer config cache-dir&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt; &lt;span class="nv"&gt;COMPOSER_CACHE_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/cache
&lt;span class="k"&gt;RUN&lt;/span&gt; --mount=type=cache,target=/tmp/cache \
    composer install --no-dev --no-scripts --no-autoloader --prefer-dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two cautions. The cache lives in the builder's storage, not in the image, so a fresh CI runner starts with it empty (see technique 8). And as the test shows, pruning the builder cache wipes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Split independent work into stages: BuildKit builds them in parallel
&lt;/h2&gt;

&lt;p&gt;A multi-stage build keeps compilers and dev tools out of the final image, and BuildKit also runs stages that do not depend on each other at the same time. A single-stage Dockerfile runs its steps one after another. I used two independent 3-second steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt; mini:1 AS front
&lt;span class="k"&gt;RUN&lt;/span&gt; sleep 3 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; front &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /front.txt

&lt;span class="k"&gt;FROM&lt;/span&gt; mini:1 AS back
&lt;span class="k"&gt;RUN&lt;/span&gt; sleep 3 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; back &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /back.txt

&lt;span class="k"&gt;FROM&lt;/span&gt; mini:1
&lt;span class="k"&gt;COPY&lt;/span&gt; --from=front /front.txt /
&lt;span class="k"&gt;COPY&lt;/span&gt; --from=back /back.txt /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;both steps in one stage (serial):   6.9 s
two independent stages (parallel):  3.7 s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real-world version is a Laravel image where Composer dependencies and frontend assets are separate stages. A fuller example is near the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Create and delete in the same layer
&lt;/h2&gt;

&lt;p&gt;Deleting a file in a later &lt;code&gt;RUN&lt;/code&gt; does not shrink the image; the earlier layer still holds it. Bigger images mean slower pushes, pulls and CI cache uploads. I created a 100 MB temporary file and removed it, once in two steps and once in one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;two RUN steps:
 8.19kB   RUN rm /tmp/build.tmp
 105MB    RUN dd if=/dev/urandom of=/tmp/build.tmp ...

one RUN step (create and rm joined with &amp;amp;&amp;amp;):
 16.4kB   RUN dd if=/dev/urandom of=/tmp/build.tmp ... &amp;amp;&amp;amp; rm /tmp/build.tmp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is output from &lt;code&gt;docker history&lt;/code&gt;. Use it rather than the total size from &lt;code&gt;docker image inspect&lt;/code&gt; for this comparison: on this containerd-store install the inspect size was larger than the sum of the layers, so I did not trust it. Typical real cases are archive downloads you extract and delete, and build tools installed for one compile step.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Choose a smaller, pinned base image
&lt;/h2&gt;

&lt;p&gt;This one I could not measure, because I could not pull images here. The reasoning is straightforward: the base image is pulled on every fresh machine, so a smaller one means faster first builds and pulls. Pin an explicit version tag (for example a specific PHP or Node version) instead of &lt;code&gt;latest&lt;/code&gt;, so a base image update does not silently invalidate your whole cache or change behavior. Slim and Alpine variants are smaller, but they differ from the full images (Alpine uses musl instead of glibc, which can break some native extensions), so test your app before switching. Also keep your language version supported: see the &lt;a href="https://dev-talk.com/php-8-2-end-of-life" rel="noopener noreferrer"&gt;PHP 8.2 end-of-life&lt;/a&gt; and &lt;a href="https://dev-talk.com/nodejs-20-end-of-life-upgrade-node-24" rel="noopener noreferrer"&gt;Node.js 20 end-of-life&lt;/a&gt; articles.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Share the build cache in CI
&lt;/h2&gt;

&lt;p&gt;CI runners are usually fresh machines, so every build starts with an empty cache and rebuilds everything. Buildx can export the cache and import it on the next run. The local form is easy to try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx build --cache-to &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;local,dest=./cache,mode=max -t myapp .

&lt;span class="c"&gt;# later, on another machine or after a prune&lt;/span&gt;
docker buildx build --cache-from &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;local,src=./cache -t myapp .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran this, pruned the whole builder cache in between, and the second build finished in 0.4 s with every step marked &lt;code&gt;CACHED&lt;/code&gt;. In CI you would store that directory with your CI's cache feature, or use a registry or the GitHub Actions backend. The Docker documentation's GitHub Actions example is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v7&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;registry&amp;gt;/&amp;lt;image&amp;gt;:latest"&lt;/span&gt;
    &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha&lt;/span&gt;
    &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha,mode=max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did not run this on GitHub Actions. The same documentation page states that the &lt;code&gt;gha&lt;/code&gt; backend works with the default &lt;code&gt;docker&lt;/code&gt; driver only when the containerd image store is enabled; otherwise use a different driver. &lt;code&gt;mode=max&lt;/code&gt; exports the layers of intermediate stages too, which matters for multi-stage builds, at the cost of a larger cache. If you build several images in one workflow, give each a separate &lt;code&gt;scope&lt;/code&gt;, otherwise each build overwrites the previous cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Stop accidentally invalidating the cache
&lt;/h2&gt;

&lt;p&gt;These are the usual culprits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A changing&lt;/strong&gt;&lt;code&gt;ARG&lt;/code&gt;&lt;strong&gt;near the top.&lt;/strong&gt; A build timestamp or commit SHA passed as an &lt;code&gt;ARG&lt;/code&gt; invalidates every step after it. In my test, the same build with the same argument value was fully cached (0.3 s), and changing only the value rerun the expensive step (3.5 s). Declare such arguments at the end, just before the instruction that needs them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COPY . .&lt;/code&gt;&lt;strong&gt;before the install&lt;/strong&gt;, as in technique 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copying files that change on every build&lt;/strong&gt;, such as logs, caches or a generated file, which also belong in &lt;code&gt;.dockerignore&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Splitting&lt;/strong&gt;&lt;code&gt;apt-get update&lt;/code&gt;&lt;strong&gt;from&lt;/strong&gt;&lt;code&gt;apt-get install&lt;/code&gt; in different &lt;code&gt;RUN&lt;/code&gt; steps, which can leave a stale package list cached. Keep them in one step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docker can also lint the Dockerfile for common mistakes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build --check .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check complete, 2 warnings have been found!
WARNING: JSONArgsRecommended - https://docs.docker.com/go/dockerfile/rule/json-args-recommended/
JSON arguments recommended for CMD to prevent unintended behavior related to OS signals
WARNING: FromAsCasing - https://docs.docker.com/go/dockerfile/rule/from-as-casing/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those two rules are style and signal-handling warnings rather than speed problems, but the check is quick and catches real mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Watch the builder's cache, and prune on purpose
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx du        &lt;span class="c"&gt;# what the build cache holds&lt;/span&gt;
docker builder prune    &lt;span class="c"&gt;# remove unused build cache&lt;/span&gt;
docker builder prune -af   &lt;span class="c"&gt;# remove all of it, no prompt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A full prune explains "why is my build suddenly slow again" when it follows a disk cleanup, since it also empties your cache mounts, as the technique 4 test showed. If disk space is the reason you prune, consider removing only old cache instead of everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together: a Laravel Dockerfile
&lt;/h2&gt;

&lt;p&gt;This combines techniques 3, 4, 5 and 9 for a Laravel app. I did not build this exact file, because the official PHP, Node and Composer images could not be pulled here; each technique in it was tested separately above. Check the tags and the files your frontend build reads (the config files, &lt;code&gt;resources&lt;/code&gt;) against your project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt; composer:2 AS vendor
&lt;span class="k"&gt;WORKDIR&lt;/span&gt; /app
&lt;span class="k"&gt;ENV&lt;/span&gt; &lt;span class="nv"&gt;COMPOSER_CACHE_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/cache
&lt;span class="k"&gt;COPY&lt;/span&gt; composer.json composer.lock ./
&lt;span class="k"&gt;RUN&lt;/span&gt; --mount=type=cache,target=/tmp/cache \
    composer install --no-dev --no-scripts --no-autoloader --prefer-dist
&lt;span class="k"&gt;COPY&lt;/span&gt; . .
&lt;span class="k"&gt;RUN&lt;/span&gt; composer dump-autoload --optimize --no-dev

&lt;span class="k"&gt;FROM&lt;/span&gt; node:24 AS assets
&lt;span class="k"&gt;WORKDIR&lt;/span&gt; /app
&lt;span class="k"&gt;COPY&lt;/span&gt; package.json package-lock.json ./
&lt;span class="k"&gt;RUN&lt;/span&gt; --mount=type=cache,target=/root/.npm npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt; vite.config.js ./
&lt;span class="k"&gt;COPY&lt;/span&gt; resources ./resources
&lt;span class="k"&gt;RUN&lt;/span&gt; npm run build

&lt;span class="k"&gt;FROM&lt;/span&gt; php:8.4-fpm AS app
&lt;span class="k"&gt;WORKDIR&lt;/span&gt; /var/www/html
&lt;span class="k"&gt;RUN&lt;/span&gt; docker-php-ext-install pdo_mysql
&lt;span class="k"&gt;COPY&lt;/span&gt; --from=vendor /app ./
&lt;span class="k"&gt;COPY&lt;/span&gt; --from=assets /app/public/build ./public/build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;vendor&lt;/code&gt; and &lt;code&gt;assets&lt;/code&gt; stages do not depend on each other, so BuildKit can run them at the same time. The slow system-level step (&lt;code&gt;docker-php-ext-install&lt;/code&gt;) comes before any application code, so it stays cached across code changes. After the build, the image contains neither Composer's cache nor &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optimizing without reading the timings.&lt;/strong&gt; Use &lt;code&gt;--progress=plain&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copying the whole project before installing dependencies.&lt;/strong&gt; It is the most common reason the cache never hits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using&lt;/strong&gt;&lt;code&gt;--no-cache&lt;/code&gt;&lt;strong&gt;as a habit.&lt;/strong&gt; It is a debugging tool. It rebuilds everything every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expecting cache mounts to survive on a fresh CI runner.&lt;/strong&gt; They live in the builder's storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaving&lt;/strong&gt;&lt;code&gt;.env&lt;/code&gt;&lt;strong&gt;out of&lt;/strong&gt;&lt;code&gt;.dockerignore&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; Secrets end up in image layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using&lt;/strong&gt;&lt;code&gt;latest&lt;/code&gt;&lt;strong&gt;as the base tag.&lt;/strong&gt; A base image update can change behavior and bust the cache without any change in your repo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Dev Talk articles and tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/post/docker-vs-kubernetes-difference" rel="noopener noreferrer"&gt;Docker vs Kubernetes: what's the difference?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/post/upgrade-laravel-12-to-13" rel="noopener noreferrer"&gt;Upgrade Laravel 12 to 13, step by step&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev-talk.com/post/php-8-2-end-of-life" rel="noopener noreferrer"&gt;PHP 8.2 end of life&lt;/a&gt; and &lt;a href="https://dev-talk.com/post/nodejs-20-end-of-life-upgrade-node-24" rel="noopener noreferrer"&gt;Node.js 20 end of life&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools: &lt;code&gt;docker build --check&lt;/code&gt; and &lt;code&gt;docker buildx du&lt;/code&gt; (both used above), plus the Dockerfile linter Hadolint and the image layer explorer Dive. I did not run the last two for this article.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/docker-build-taking-too-long" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>buildkit</category>
      <category>dockerfile</category>
      <category>cicd</category>
    </item>
    <item>
      <title>AI Coding Prompts That Actually Improve Productivity</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Tue, 10 Nov 2026 14:16:15 +0000</pubDate>
      <link>https://dev.to/devtalk94/ai-coding-prompts-that-actually-improve-productivity-3i20</link>
      <guid>https://dev.to/devtalk94/ai-coding-prompts-that-actually-improve-productivity-3i20</guid>
      <description>&lt;p&gt;Most lists of "best AI coding prompts" are collections of clever wording. Wording is rarely the problem. The real problem is that AI output still has to be checked by you, and checking is the slow part. A prompt that produces more code faster but leaves you reading 400 unfamiliar lines has not saved time. This article is about prompts that make the result cheaper to verify, and every example was run against a small PHP project so you can see the actual output, including where a prompt did not make the difference I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "faster code" is not the same as productivity
&lt;/h2&gt;

&lt;p&gt;The research does not support a simple "AI makes developers faster" story. In METR's 2025 randomized trial with 16 experienced open-source developers working on their own repositories, developers took 19% longer with AI tools than without, while believing afterwards that the tools had made them about 20% faster. METR's February 2026 follow-up used a larger group (57 developers, 800+ tasks) and found changes of -18% and -4% in task time for the two groups, which would mean faster, but neither result was statistically significant, and METR listed design problems: between 30% and 50% of developers said they skipped tasks they did not want to do without AI, and time measurements get unreliable when developers run several agents at once. METR's own conclusion is that developers are likely more sped up now, but it cannot show that with the current data. (I read the 2026 figures in a secondary write-up of METR's post, so check METR's site for the original numbers.)&lt;/p&gt;

&lt;p&gt;The 2025 DORA report on AI-assisted development points the same direction from the team side: AI improves throughput, but often at the cost of stability when the underlying engineering foundation is weak. And the Stack Overflow Developer Survey 2026 shows what developers do in practice after an AI answer arrives: 76.5% run the code locally, 63.9% compare it against their codebase, 53.0% inspect tests and security implications, and only 9.6% use it as-is.&lt;/p&gt;

&lt;p&gt;So the useful question is not "how do I get more code out of the model" but "how do I get output I can trust quickly". The prompts below are built around that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five habits that did the work
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Give it a check it can run.&lt;/strong&gt; A failing test, a command, a linter. This matters more than anything you write in the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Say what it must not touch.&lt;/strong&gt; "Do not edit anything in tests/" costs ten words and prevents the classic cheat of changing the test to pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope the task.&lt;/strong&gt; One bug, one file, one concern. Say "security and performance only; ignore style".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask for evidence in a fixed format.&lt;/strong&gt; Line numbers, severity, the command it ran, the exact line it changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask it to report, not fix, when you want to decide.&lt;/strong&gt; Tests that expose problems are more valuable than silent patches.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The test project
&lt;/h2&gt;

&lt;p&gt;All examples use this tiny project. &lt;code&gt;src/Cart.php&lt;/code&gt; has one real bug: it truncates instead of rounding half up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;?&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;
&lt;span class="c1"&gt;// src/Cart.php&lt;/span&gt;
&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Cart&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/** @param array&amp;lt;int, array{price_cents:int, qty:int}&amp;gt; $items */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$discountPercent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'price_cents'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'qty'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sum&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$discountPercent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tests/run.php&lt;/code&gt; is a plain script with a small &lt;code&gt;check($name, $expected, $actual)&lt;/code&gt; function and three checks. Running it fails on exactly one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php tests/run.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASS  no discount
FAIL  10% off, rounds half up (expected 896, got 895)
PASS  empty cart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran every prompt with Claude Code 2.1.318 in print mode, giving it only the tools it needed, for example &lt;code&gt;--permission-mode acceptEdits --allowedTools "Read Edit Bash(php *)"&lt;/code&gt;. The prompts are plain text and not specific to that tool, but I only ran them in Claude Code. Model output varies from run to run, so treat each result as one sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt 1: fixing a bug
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The vague version:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The cart total is wrong, fix it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The specific version:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Running 'php tests/run.php' fails: the test '10% off, rounds half up' expects 896
but total() returns 895 for one item of 995 cents with a 10% discount. Fix the bug
in src/Cart.php only: do not edit anything in tests/. Money is integer cents, so
do not introduce floats into the return type. Run 'php tests/run.php' to confirm
all tests pass, then answer in two sentences: the root cause, and the exact line
you changed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; both fixed it, and both left the test file untouched (I compared checksums). The vague prompt worked because the failing test was already in the repository, and the agent found and ran it. That is the first lesson: &lt;em&gt;the test did the real work, not the wording&lt;/em&gt;. The differences were smaller than I expected:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Vague prompt&lt;/th&gt;
&lt;th&gt;Specific prompt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;All 3 tests pass&lt;/td&gt;
&lt;td&gt;All 3 tests pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change&lt;/td&gt;
&lt;td&gt;Replaced the return line and added a comment&lt;/td&gt;
&lt;td&gt;Replaced the return line only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer&lt;/td&gt;
&lt;td&gt;A long explanation with assumptions and an offer to look at tax, shipping and negative quantities&lt;/td&gt;
&lt;td&gt;Two sentences: root cause and the exact line&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both used the same fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;intdiv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sum&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$discountPercent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The specific prompt gave me a shorter, easier-to-review answer and a smaller diff. It did not give a more correct fix. Which brings us to the next prompt, because this fix has a problem the original test never covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt 2: edge-case tests that report instead of fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write edge-case tests for Cart::total() in tests/edge.php, reusing the check() style
from tests/run.php. Cover: quantity 0, a 100% discount, a discount over 100, a
negative price, several items in one cart, and a large quantity. Run 'php tests/edge.php'.
Do NOT modify anything in src/. For each failing test, say whether the bug is in
Cart or in my expectation, and what behaviour you think Cart should have.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran this against the code after the fix above. Five tests passed, three failed, &lt;code&gt;src/&lt;/code&gt; was untouched (checksum compared), and I re-ran the file myself to confirm the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASS  qty 0 gives 0
PASS  qty 0 with discount gives 0
PASS  100% discount is free
FAIL  discount 150 is rejected (expected 'InvalidArgumentException', got -499)
FAIL  negative price is rejected (expected 'InvalidArgumentException', got -499)
PASS  several items, 10% off
PASS  large quantity
FAIL  overflowing quantity is rejected (expected 'OverflowException', got 'TypeError')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful part was the triage. For a discount of 150 the agent explained that &lt;code&gt;1000 * (100 - 150) + 50&lt;/code&gt; is &lt;code&gt;-49950&lt;/code&gt; and &lt;code&gt;intdiv&lt;/code&gt; truncates toward zero, so the customer gets a negative total, and it argued for throwing on a discount outside 0-100. For the negative price it pointed out that the &lt;code&gt;+ 50&lt;/code&gt; rounding trick is only correct for non-negative sums: a single item of -500 with no discount returns -499. That is a genuine bug that &lt;em&gt;the fix itself introduced&lt;/em&gt;, and it was invisible to the original test. For the overflow test it correctly said the failure was mostly its own expectation: the code fails loudly with a &lt;code&gt;TypeError&lt;/code&gt; and never returns a wrong number, and the exception class I asked for was an arbitrary choice.&lt;/p&gt;

&lt;p&gt;This is the main reason to ask for tests and a verdict instead of a patch: the patch that passes your test may still be wrong, and you only learn that if something probes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt 3: structured code review
&lt;/h2&gt;

&lt;p&gt;I used a small Laravel controller with real problems: a SQL string built from request input, &lt;code&gt;Order::find($id)-&amp;gt;delete()&lt;/code&gt; with no existence or authorization check, an N+1 loop over relations, and an unbounded &lt;code&gt;get()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vague:&lt;/strong&gt; &lt;code&gt;review src/OrderController.php&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review src/OrderController.php (a Laravel 13 controller) for security and performance
problems only; ignore style. For each finding give: severity (high/medium/low), the
line number, why it is a problem in one sentence, and a minimal fix as a code snippet.
Order by severity. Do not report anything you cannot point to a specific line for.
Do not edit any files.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both reviews found the SQL injection, the missing authorization on delete, the crash on a missing ID, and the N+1 queries. I checked the line numbers in the structured answer (13, 19-20, 31 and 36) against the file and they were right. So this prompt did not find more bugs. What it changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The vague review returned seven findings, including a "minor" group, and a long fix-priority list. The structured one returned five findings in a fixed format with line numbers, which is easier to turn into tickets.&lt;/li&gt;
&lt;li&gt;It stated what it could not judge: "I can't see the routes file or policies. If a middleware or policy already ..." That sentence is worth more than the extra findings, because it tells you what to verify yourself.&lt;/li&gt;
&lt;li&gt;It left out two possible issues it could not tie to a specific line, which is exactly what the "do not report anything you cannot point to" instruction asked for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fixes it proposed for authorization are sketches (they assume an &lt;code&gt;OrderPolicy&lt;/code&gt; exists). Treat review output as a list of places to look, not as a patch to apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  More prompt templates (not run for this article)
&lt;/h2&gt;

&lt;p&gt;These follow the same five habits. I have not tested them here, so adapt them and check the results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Plan before code
Plan how to add [feature]. List the files you would change, the new tests you would
write, and the risks. Do not write any code yet.

# Refactor with a safety net
Refactor [class] to [goal]. Behaviour must not change. Run [test command] before and
after, and show both results. Stop and tell me if any test fails.

# Explain unfamiliar code
Explain what [function] does in five bullets. Flag side effects (database writes,
network calls, global state) and anything that looks like a bug. Do not change code.

# SQL review
Here is a query and its EXPLAIN output: [paste]. Tell me what is slow and why, and
suggest an index. Say which assumptions you are making about the table size.

# Commit message
Write a commit message for the staged changes ('git diff --staged'). Subject under
72 characters, imperative mood, then a short body explaining why, not what.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Treating a passing test as proof.&lt;/strong&gt; The fix in Prompt 1 passed and still broke on negative amounts. Add edge cases and read the assertions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No way for the agent to check itself.&lt;/strong&gt; Without a test command, the answer is a guess presented confidently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts that invite scope creep.&lt;/strong&gt; "Improve this class" produces a rewrite. Name one concern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not saying what to leave alone.&lt;/strong&gt; Forbid edits to tests, migrations or config explicitly when that matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accepting fix sketches as final.&lt;/strong&gt; Generated authorization and validation code assumes things about your app that it cannot see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never measuring.&lt;/strong&gt; If you do not track review time and defects, you cannot tell whether a habit helps. The METR results show that feelings about speed can be wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Alternatives to writing better prompts every time
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Put standing rules in a project file&lt;/strong&gt; (&lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt;): the test command, the formatter, directories not to touch. Then you do not repeat them in every prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a tool that gives the agent project context.&lt;/strong&gt; For Laravel, Laravel Boost does this; see &lt;a href="https://dev-talk.com/ai-agents-build-laravel-applications" rel="noopener noreferrer"&gt;How to use AI agents to build Laravel applications&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate the checks.&lt;/strong&gt; A formatter, static analysis and a test suite that run on every change catch more than any prompt wording.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Dev Talk articles and tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/claude-code-commands-every-developer-should-know" rel="noopener noreferrer"&gt;10 Claude Code commands every developer should know&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/claude-code-vs-cursor-vs-github-copilot" rel="noopener noreferrer"&gt;Claude Code vs Cursor vs GitHub Copilot: which is best in 2026?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/ai-agents-build-laravel-applications" rel="noopener noreferrer"&gt;How to use AI agents to build Laravel applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/open-source-tools-developer-productivity" rel="noopener noreferrer"&gt;Open source tools that make developers more productive&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools that make AI output cheaper to verify: your test runner (Pest or PHPUnit), Laravel Pint for formatting, and a static analyzer such as PHPStan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do these prompts work with Cursor and Copilot?
&lt;/h3&gt;

&lt;p&gt;The wording is tool-agnostic, and the five habits apply to any assistant. I only ran them in Claude Code, so I cannot show results for the others.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long should a prompt be?
&lt;/h3&gt;

&lt;p&gt;Long enough to state the failing check, the constraints and the output format. The specific bug-fix prompt above is five sentences. Length beyond that rarely helped in my runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a vague prompt ever fine?
&lt;/h3&gt;

&lt;p&gt;Yes. When the repository already contains a failing test, a short prompt worked. Vague prompts get risky when there is nothing for the agent to check against.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I ask the AI to explain its reasoning?
&lt;/h3&gt;

&lt;p&gt;Ask for evidence you can verify (the line it changed, the command it ran, the output), not for a story. A confident explanation can be wrong; a test result can be rerun.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will good prompts make me faster?
&lt;/h3&gt;

&lt;p&gt;I can't promise that. The best-known controlled study found a slowdown in 2025 and an inconclusive result in 2026. Measure your own review time and defect rate over a few weeks.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/ai-coding-prompts-improve-developer-productivity" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aicoding</category>
      <category>prompts</category>
      <category>productivity</category>
      <category>codereview</category>
    </item>
    <item>
      <title>Claude Code vs Cursor vs Copilot: Which Is Best in 2026?</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Mon, 09 Nov 2026 02:46:19 +0000</pubDate>
      <link>https://dev.to/devtalk94/claude-code-vs-cursor-vs-copilot-which-is-best-in-2026-b15</link>
      <guid>https://dev.to/devtalk94/claude-code-vs-cursor-vs-copilot-which-is-best-in-2026-b15</guid>
      <description>&lt;p&gt;&lt;em&gt;Last updated: 5 November 2026. Pricing and plans for all three tools changed several times this year, so see the&lt;/em&gt;&lt;em&gt;update log&lt;/em&gt;&lt;em&gt;at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Asking which of these three is "best" runs into a problem before you compare a single feature: they are not the same kind of product. Claude Code is an agent that works in your terminal. Cursor is a code editor built around AI. GitHub Copilot is an assistant that plugs into the editors you already use and into GitHub itself. Picking the wrong one usually means picking the wrong &lt;em&gt;shape&lt;/em&gt; of tool for how you work. This article compares them on that basis, shows the commands for the two that have a CLI, and gives you a way to test all three on your own code in an afternoon, because nobody else's benchmark runs on your repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Cursor&lt;/th&gt;
&lt;th&gt;GitHub Copilot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;Terminal-first coding agent&lt;/td&gt;
&lt;td&gt;AI-native code editor&lt;/td&gt;
&lt;td&gt;Assistant for many editors, plus GitHub integration and a CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where you use it&lt;/td&gt;
&lt;td&gt;Terminal (IDE integrations exist)&lt;/td&gt;
&lt;td&gt;The Cursor editor&lt;/td&gt;
&lt;td&gt;VS Code, other IDEs, GitHub, terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing model&lt;/td&gt;
&lt;td&gt;Included in Claude Pro and Max plans, or pay-as-you-go API&lt;/td&gt;
&lt;td&gt;Subscription with a monthly dollar usage pool&lt;/td&gt;
&lt;td&gt;Subscription with monthly AI credits since June 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Multi-file tasks, scripts, CI, people who live in the terminal&lt;/td&gt;
&lt;td&gt;People who want the AI built into the editor itself&lt;/td&gt;
&lt;td&gt;Teams already on GitHub and mixed editors; cheapest entry point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watch out for&lt;/td&gt;
&lt;td&gt;Usage limits on subscription plans&lt;/td&gt;
&lt;td&gt;Usage pool running out on heavy agent use&lt;/td&gt;
&lt;td&gt;Credits drawn down by agent and premium-model use&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you want one sentence: pick the one that matches where you already work, then test it on real tasks. Many developers use more than one, as the next section shows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What developers actually use
&lt;/h2&gt;

&lt;p&gt;In the Stack Overflow Developer Survey 2026, among respondents who use AI coding assistants or agents (about 12,000 answers), the most used agents were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Share of those respondents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;65.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;58.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Codex&lt;/td&gt;
&lt;td&gt;29.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;25.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two cautions. Survey respondents choose to take the survey, so this is not a random sample of developers. And the shares add up to far more than 100%, which means many people use two or more tools. It is evidence of popularity, not of quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing as of November 2026
&lt;/h2&gt;

&lt;p&gt;I could not open the vendors' own pricing pages from my environment, so the figures below are cross-checked from several independent roundups that agreed with each other, dated between July and November 2026. Treat them as a guide and confirm on the official pages before you pay.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Monthly price&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;$20 ($17 billed annually)&lt;/td&gt;
&lt;td&gt;Claude Code is included; the Free plan does not include it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max 5x&lt;/td&gt;
&lt;td&gt;$100&lt;/td&gt;
&lt;td&gt;About 5 times Pro usage&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max 20x&lt;/td&gt;
&lt;td&gt;$200&lt;/td&gt;
&lt;td&gt;About 20 times Pro usage&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;Pay per token&lt;/td&gt;
&lt;td&gt;No monthly minimum; usage-based&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;Hobby&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Limited agent requests and tab completions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;Monthly usage pool in dollars&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro+&lt;/td&gt;
&lt;td&gt;$60&lt;/td&gt;
&lt;td&gt;Larger pool, same features as Pro&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultra&lt;/td&gt;
&lt;td&gt;$200&lt;/td&gt;
&lt;td&gt;Largest individual pool&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teams&lt;/td&gt;
&lt;td&gt;$40 per user&lt;/td&gt;
&lt;td&gt;Adds SSO, analytics, central billing&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Limited monthly allowance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;Includes monthly AI credits&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro+&lt;/td&gt;
&lt;td&gt;$39&lt;/td&gt;
&lt;td&gt;More credits, access to all models&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max&lt;/td&gt;
&lt;td&gt;$100&lt;/td&gt;
&lt;td&gt;Highest individual credit allowance&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business&lt;/td&gt;
&lt;td&gt;$19 per seat&lt;/td&gt;
&lt;td&gt;Org policy, audit logs, SSO&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;$39 per seat&lt;/td&gt;
&lt;td&gt;Adds knowledge bases on private code&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The prices matter less than how each tool counts usage, because that is what surprises people later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude plans&lt;/strong&gt; limit usage in rolling five-hour windows plus a weekly limit, shared between Claude's apps and Claude Code. Anthropic publishes multipliers, not token counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor&lt;/strong&gt; uses dollar-denominated monthly pools. When the pool runs out, on-demand usage is billed after the fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copilot&lt;/strong&gt; moved to usage-based billing on 1 June 2026: each plan includes AI credits, and agent and premium-model use draws them down. Roundups report that plain code completions do not use credits. One roundup also noted that sign-ups for some individual plans were temporarily paused in mid-2026, so check that your plan is available.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Claude Code: working examples
&lt;/h2&gt;

&lt;p&gt;Claude Code is the one to look at if you want scripting. The &lt;code&gt;-p&lt;/code&gt; flag runs a single prompt non-interactively, so it works in pipes and CI. I ran these with Claude Code 2.1.318:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pipe a failing test into it&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'FAILED Tests\\Feature\\InvoiceTest::test_export - Expected 200 but received 419\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"In one short sentence, what does this test output suggest?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A 419 in Laravel means the CSRF token was missing or expired, so the test's export
request (likely a POST) is being rejected before it reaches the controller. ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Machine-readable output&lt;/span&gt;
claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"say ok"&lt;/span&gt; &lt;span class="nt"&gt;--output-format&lt;/span&gt; json     &lt;span class="c"&gt;# the answer is in the "result" field&lt;/span&gt;

&lt;span class="c"&gt;# Continue the most recent conversation in this directory&lt;/span&gt;
claude &lt;span class="nt"&gt;-c&lt;/span&gt;

&lt;span class="c"&gt;# Cap spending on a scripted run&lt;/span&gt;
claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Review the staged changes"&lt;/span&gt; &lt;span class="nt"&gt;--max-budget-usd&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output formats are &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt; and &lt;code&gt;stream-json&lt;/code&gt;. I did not run the &lt;code&gt;--max-budget-usd&lt;/code&gt; example; it comes from the CLI's help text. For the interactive commands, see &lt;a href="https://dev-talk.com/claude-code-commands-every-developer-should-know" rel="noopener noreferrer"&gt;10 Claude Code commands every developer should know&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Copilot CLI: working examples
&lt;/h2&gt;

&lt;p&gt;Copilot also has a terminal agent, installed from npm. I installed version 1.0.105 and read its options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @github/copilot
copilot &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub Copilot CLI 1.0.105.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;copilot &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Explain what this repository does"&lt;/span&gt;     &lt;span class="c"&gt;# one prompt, then exit&lt;/span&gt;
copilot &lt;span class="nt"&gt;--continue&lt;/span&gt;                                  &lt;span class="c"&gt;# continue the last session&lt;/span&gt;
copilot &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;--deny-tool&lt;/span&gt; ...                    &lt;span class="c"&gt;# restrict which tools it may use&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The help lists &lt;code&gt;-p&lt;/code&gt;/&lt;code&gt;--prompt&lt;/code&gt;, &lt;code&gt;--model&lt;/code&gt;, &lt;code&gt;--continue&lt;/code&gt;, &lt;code&gt;--allow-tool&lt;/code&gt;, &lt;code&gt;--deny-tool&lt;/code&gt; and &lt;code&gt;--output-format&lt;/code&gt;, plus &lt;code&gt;--allow-all&lt;/code&gt; (also &lt;code&gt;--yolo&lt;/code&gt;), which switches off permission prompts. I could not log in from my environment, so I did not run an actual prompt. Prefer the narrower &lt;code&gt;--allow-tool&lt;/code&gt; and &lt;code&gt;--deny-tool&lt;/code&gt; options over &lt;code&gt;--allow-all&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cursor
&lt;/h2&gt;

&lt;p&gt;Cursor is an editor, so most of what it does happens in its interface (tab completion, an agent panel, inline edits) and does not translate into terminal commands. I did not install or run it, so I cannot show output for it, and I am not going to describe its features from marketing copy. The facts I can support are its plans and its usage pools, in the pricing section above. If you try it, the free Hobby plan includes a one-week Pro trial according to the roundups, which is enough for the test below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test all three on your own code
&lt;/h2&gt;

&lt;p&gt;I did not run a head-to-head benchmark, and I would distrust any article that claims one without publishing the tasks. Run your own in an afternoon:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick three real tasks from your backlog: a small bug fix, a feature that touches several files, and a refactor with tests.&lt;/li&gt;
&lt;li&gt;Use the same prompt for each tool, starting from the same git branch.&lt;/li&gt;
&lt;li&gt;Record: did the tests pass, how large was the diff, how long did you spend reviewing and fixing, and what did it cost in usage.&lt;/li&gt;
&lt;li&gt;Compare on &lt;strong&gt;review time&lt;/strong&gt;, not on how impressive the demo looked. The slow part of AI-assisted work is reading the result.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch &lt;span class="nt"&gt;-c&lt;/span&gt; try-claude main
&lt;span class="c"&gt;# ... run the task, then record the numbers&lt;/span&gt;
git diff &lt;span class="nt"&gt;--stat&lt;/span&gt; main
php artisan &lt;span class="nb"&gt;test&lt;/span&gt;        &lt;span class="c"&gt;# or your own test command&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Which one should you choose?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You work mostly in the terminal, or want to automate tasks in scripts and CI:&lt;/strong&gt; Claude Code or Copilot CLI. Claude Code is the one I tested end to end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want AI inside the editor on every keystroke:&lt;/strong&gt; Cursor is built for that, and Copilot is the alternative if you want to keep VS Code or a JetBrains IDE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your company is on GitHub and needs policy controls, audit logs and one invoice:&lt;/strong&gt; Copilot Business or Enterprise fits that setup, because it lives where your repositories already are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget is tight:&lt;/strong&gt; Copilot Pro at $10 is the lowest-priced paid individual plan in the table, and all three have free tiers to test with. Compare usage limits, not only prices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You use Laravel:&lt;/strong&gt; any of them can work. Laravel Boost, the official package that gives coding agents project context, supports a dozen agents, including all three. See &lt;a href="https://dev-talk.com/ai-agents-build-laravel-applications" rel="noopener noreferrer"&gt;How to use AI agents to build Laravel applications&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Choosing by demo.&lt;/strong&gt; Short demos hide the review cost. Judge by the three-task test above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring how usage is counted.&lt;/strong&gt; A cheap plan with a small pool feels fine for a week and then runs out mid-task. Check the usage model, not only the price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switching off permission prompts.&lt;/strong&gt; Options like &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; in Claude Code or &lt;code&gt;--allow-all&lt;/code&gt; in Copilot CLI remove the checks that stop a bad command. Use them only in disposable sandboxes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pasting secrets.&lt;/strong&gt; Keep &lt;code&gt;.env&lt;/code&gt; contents out of prompts with every tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not reading the generated tests.&lt;/strong&gt; A tool that writes the code and the test can make the same mistake in both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming you must pick one.&lt;/strong&gt; Mixing is normal, for example Copilot completions in the editor and a terminal agent for larger tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Alternatives
&lt;/h2&gt;

&lt;p&gt;The survey data above also lists OpenAI Codex (29.5%), Google Antigravity (16.0%) and Gemini Code Assist (14.6%). JetBrains users can look at Junie. I have not tested any of these for this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Dev Talk articles and tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/claude-code-commands-every-developer-should-know" rel="noopener noreferrer"&gt;10 Claude Code commands every developer should know&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/ai-agents-build-laravel-applications" rel="noopener noreferrer"&gt;How to use AI agents to build Laravel applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/laravel-ai-sdk-agent-skills" rel="noopener noreferrer"&gt;Laravel AI SDK: building agents inside your app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/what-developers-search-and-use-2026" rel="noopener noreferrer"&gt;What developers search for and use in 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev-talk.com/open-source-tools-developer-productivity" rel="noopener noreferrer"&gt;Open source tools that make developers more productive&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools worth a look alongside these: &lt;a href="https://github.com/laravel/boost" rel="noopener noreferrer"&gt;Laravel Boost&lt;/a&gt; for PHP projects, and your existing test runner and formatter (Pest or PHPUnit, Pint), since an agent with a fast feedback loop produces better results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which is the cheapest?
&lt;/h3&gt;

&lt;p&gt;By list price, Copilot Pro at $10 per month. But all three meter agent usage, so the real cost depends on how much you use the agent features. Test with the free tiers first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Claude Code free?
&lt;/h3&gt;

&lt;p&gt;No. It needs a paid Claude plan (Pro or higher) or API credits. The Free Claude plan does not include it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use more than one?
&lt;/h3&gt;

&lt;p&gt;Yes, and many developers do. They can coexist in the same project, since they work on the same files and git history. Review changes from each before committing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which one is best for beginners?
&lt;/h3&gt;

&lt;p&gt;An in-editor tool (Copilot or Cursor) is usually the gentler start, because suggestions appear where you are already typing. A terminal agent asks you to be comfortable with the command line and git.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will my code be used for training?
&lt;/h3&gt;

&lt;p&gt;Policies differ by vendor and plan, and they change. I did not verify them for this article. Read each vendor's current data-use terms, especially on free and individual plans, before using private code.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/claude-code-vs-cursor-vs-github-copilot" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>cursor</category>
      <category>githubcopilot</category>
      <category>aicoding</category>
    </item>
    <item>
      <title>AI Agents for Laravel: Boost Setup and a Safe Workflow</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Sun, 08 Nov 2026 03:56:46 +0000</pubDate>
      <link>https://dev.to/devtalk94/ai-agents-for-laravel-boost-setup-and-a-safe-workflow-13b6</link>
      <guid>https://dev.to/devtalk94/ai-agents-for-laravel-boost-setup-and-a-safe-workflow-13b6</guid>
      <description>&lt;h2&gt;
  
  
  An AI coding agent can write a controller, a migration and a test in a minute. It can also invent a method that does not exist in your Laravel version, ignore how your project is structured, and tell you everything works without running anything. Getting good results is less about the agent and more about what you give it: accurate context, a way to check its own work, and limits on what it may touch. This guide covers that setup for Laravel, using Laravel Boost, the official package for this job, and a workflow that keeps you in control.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What goes wrong without setup
&lt;/h2&gt;

&lt;p&gt;A general-purpose agent knows Laravel from its training data, which mixes versions. Typical results are code written for an older release, queries that ignore your actual schema, and a new style next to your existing one. It also cannot see runtime problems: the exception in your log, the table columns in your database, or what your last request returned. An agent that can read those things fixes its own mistakes. One that cannot guesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install Laravel Boost (development only)
&lt;/h2&gt;

&lt;p&gt;Laravel Boost gives agents Laravel-specific guidelines, skills and an MCP server connected to your running app. Install it as a development dependency, then run its installer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require laravel/boost &lt;span class="nt"&gt;--dev&lt;/span&gt;
php artisan boost:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer asks which agents you use and which parts to set up. You can also run parts separately: &lt;code&gt;--guidelines&lt;/code&gt;, &lt;code&gt;--skills&lt;/code&gt; and &lt;code&gt;--mcp&lt;/code&gt;. In the current source (v2.10.4, released 29 October 2026), Boost knows these agents: Claude Code, Codex, Copilot, Cursor, Junie, Kiro, OpenCode, Zed, Amp, Antigravity, Grok Build and Pi. For several of them the guidelines go into &lt;code&gt;AGENTS.md&lt;/code&gt; in your project root. Claude Code uses &lt;code&gt;CLAUDE.md&lt;/code&gt; if that file already exists and &lt;code&gt;AGENTS.md&lt;/code&gt; otherwise, its skills go into &lt;code&gt;.claude/skills&lt;/code&gt;, and its MCP configuration into &lt;code&gt;.mcp.json&lt;/code&gt;. Boost requires PHP 8.2 or newer and supports Laravel 11, 12 and 13.&lt;/p&gt;

&lt;p&gt;Commit the generated guidelines so the whole team shares them. Keep them current when you upgrade Laravel or add packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan boost:update
php artisan boost:list-skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the agent can do through Boost
&lt;/h2&gt;

&lt;p&gt;The MCP server exposes tools the agent can call while it works. From the tool definitions in the source:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it gives the agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search docs&lt;/td&gt;
&lt;td&gt;Version-specific Laravel documentation for the packages installed in your project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database schema and connections&lt;/td&gt;
&lt;td&gt;Your real tables and columns instead of guessed ones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database query&lt;/td&gt;
&lt;td&gt;Read-only SQL (SELECT, SHOW, EXPLAIN, DESCRIBE); write statements are rejected, and the query runs inside a read-only transaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tinker&lt;/td&gt;
&lt;td&gt;Runs PHP in the application context, like &lt;code&gt;artisan tinker&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last error and log entries&lt;/td&gt;
&lt;td&gt;The latest exception and application log lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser logs&lt;/td&gt;
&lt;td&gt;Frontend console errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Absolute URL&lt;/td&gt;
&lt;td&gt;Correct URLs for routes, so the agent can check pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Record rule&lt;/td&gt;
&lt;td&gt;Saves a project rule into &lt;code&gt;.ai/rules&lt;/code&gt;, only when you explicitly ask for one to be remembered&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This list reflects the source at the time of writing. For example, an older &lt;code&gt;application-info&lt;/code&gt; tool was removed in v2.10.4, so tool names in older tutorials may not match what you get.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Use a workflow, not one big prompt
&lt;/h2&gt;

&lt;p&gt;The pattern that gives consistent results is small steps with a check after each one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ask for a plan first.&lt;/strong&gt; "Plan how to add invoice PDF export: which files change, which routes, which tests. Do not write code yet." Fix the plan before any code exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the test first.&lt;/strong&gt; "Write a feature test for the export endpoint that fails right now." A failing test is a precise definition of "done".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement in small pieces.&lt;/strong&gt; One migration, one model, one controller action at a time, not the whole feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the agent run the checks.&lt;/strong&gt; Tell it to run the tests and the formatter, and to read the output:&lt;code&gt;php artisan test --filter=InvoiceExportTest
./vendor/bin/pint --dirty&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review the diff yourself.&lt;/strong&gt; Read every line before you commit. You own the code that ships.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A prompt in this style, for a change in an existing project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add soft deletes to the Project model.
- Check the real projects table schema first.
- Create the migration and update the model.
- Update the existing ProjectTest so it covers restoring a project.
- Run php artisan test and fix failures. Do not change unrelated files.
- Show me the migration SQL with php artisan migrate --pretend before running it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;migrate --pretend&lt;/code&gt; prints the SQL a migration would run without running it, which is a cheap safety check on anything the agent generates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Set guardrails
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep Boost out of production.&lt;/strong&gt; It is a dev dependency, and in the source its server only starts when the app environment is &lt;code&gt;local&lt;/code&gt; or when &lt;code&gt;APP_DEBUG&lt;/code&gt; is true. Production should have neither. A server with &lt;code&gt;APP_DEBUG=true&lt;/code&gt; would qualify, which is one more reason never to run production that way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point agents at a local database with fake data.&lt;/strong&gt; The query tool is read-only, but Tinker can run any PHP, including writes. Never give an agent production credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review security-sensitive code by hand.&lt;/strong&gt; Authorization (policies and gates), validation rules, mass-assignment settings and file uploads are where plausible-looking code is often wrong. A missing authorization check does not fail any test unless someone wrote one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review migrations before you run them.&lt;/strong&gt; Dropping a column or changing a type on a large table is not something to approve by skimming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look for performance mistakes.&lt;/strong&gt; Agents readily write queries inside loops. Check for N+1 patterns in anything that lists records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep secrets out of the conversation.&lt;/strong&gt; Do not paste &lt;code&gt;.env&lt;/code&gt; contents into prompts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use version control as your undo button.&lt;/strong&gt; Work on a branch and commit after each working step.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accepting "done" without evidence.&lt;/strong&gt; If the agent did not run the tests, ask it to. Only a passing run counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompting for a whole feature at once.&lt;/strong&gt; Large changes bury mistakes in a big diff that nobody reviews well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale guidelines.&lt;/strong&gt; After upgrading Laravel or adding packages, rerun &lt;code&gt;boost:update&lt;/code&gt;, or the agent keeps following old conventions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Letting the agent restyle your code.&lt;/strong&gt; Tell it to follow existing patterns, and keep formatter changes to files it touched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating generated tests as proof.&lt;/strong&gt; An agent that writes both the code and the test can encode the same misunderstanding in both. Read the assertions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Agents inside your app are a different thing
&lt;/h2&gt;

&lt;p&gt;Everything above is about agents that help you write the application. If you want an agent as a feature of your product (a support assistant, a tool-calling workflow), that is the Laravel AI SDK, covered in &lt;a href="https://dev-talk.com/laravel-ai-sdk-agent-skills" rel="noopener noreferrer"&gt;the Laravel AI SDK article&lt;/a&gt;. The two combine well, since a coding agent can help you build SDK-based features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which coding agent works best with Laravel?
&lt;/h3&gt;

&lt;p&gt;Boost supports a dozen of them, and I have no evidence that one is clearly better. Choose by workflow (terminal, editor or IDE) and test two on a small real task from your own project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need Boost to use an AI agent with Laravel?
&lt;/h3&gt;

&lt;p&gt;No. Any agent can edit Laravel files. Boost adds the project context and the runtime tools that reduce guessing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it safe to let an agent run commands?
&lt;/h3&gt;

&lt;p&gt;Run it in a development environment, with a throwaway database, on a branch, and review what it ran. Keep production access out of reach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will it write my whole application?
&lt;/h3&gt;

&lt;p&gt;It can produce a lot, but you still need to know what good looks like. People who understand Laravel get far better results because they can plan the work and review the output.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/ai-agents-build-laravel-applications" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>ai</category>
      <category>laravelboost</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Docker vs Kubernetes: What’s the Difference? Simple Guide</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Sat, 07 Nov 2026 15:01:34 +0000</pubDate>
      <link>https://dev.to/devtalk94/docker-vs-kubernetes-whats-the-difference-simple-guide-3jhp</link>
      <guid>https://dev.to/devtalk94/docker-vs-kubernetes-whats-the-difference-simple-guide-3jhp</guid>
      <description>&lt;p&gt;People ask this as if they were rivals, and they are not. Docker packages your application so it runs the same everywhere. Kubernetes runs and looks after many of those packages across many servers. One builds the box, the other manages the warehouse full of boxes. Most teams that use Kubernetes also use Docker-built images, so the real question is not "which one?" but "do I need the bigger tool yet?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Docker&lt;/th&gt;
&lt;th&gt;Kubernetes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it does&lt;/td&gt;
&lt;td&gt;Builds images and runs containers&lt;/td&gt;
&lt;td&gt;Runs containers across a group of machines and keeps them healthy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works on&lt;/td&gt;
&lt;td&gt;One machine&lt;/td&gt;
&lt;td&gt;A cluster of machines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You use it to&lt;/td&gt;
&lt;td&gt;Package an app and run it&lt;/td&gt;
&lt;td&gt;Deploy, scale, update and restart many apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup effort&lt;/td&gt;
&lt;td&gt;Install it and write a Dockerfile&lt;/td&gt;
&lt;td&gt;Much higher: cluster, networking, storage, config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical user&lt;/td&gt;
&lt;td&gt;Every developer&lt;/td&gt;
&lt;td&gt;Teams running many services in production&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What Docker does
&lt;/h2&gt;

&lt;p&gt;A container is your application plus everything it needs to run (runtime, libraries, config), isolated from the rest of the machine. You describe it in a &lt;code&gt;Dockerfile&lt;/code&gt;, build an &lt;strong&gt;image&lt;/strong&gt; from it, and run containers from that image. I built and ran the smallest possible example with Docker 29.8.2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;
FROM scratch
COPY hello /hello
CMD &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/hello"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;

docker build &lt;span class="nt"&gt;-t&lt;/span&gt; hello:1 &lt;span class="nb"&gt;.&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; hello:1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hello from a container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;hello&lt;/code&gt; was a tiny statically linked program, which is why the base image is empty (&lt;code&gt;scratch&lt;/code&gt;). In real projects you start from an image such as a PHP, Node or Python base image instead. The image came to 354 kB of content, and I could start two containers from the same image side by side. That is the main idea: one image, as many identical containers as you like, on the same machine.&lt;/p&gt;

&lt;p&gt;Docker answers: "How do I package this app so it behaves the same on my laptop, in CI and on the server?"&lt;/p&gt;

&lt;h2&gt;
  
  
  What Kubernetes does
&lt;/h2&gt;

&lt;p&gt;Kubernetes (often written K8s) is an open source system for running containers at scale. Once you have more than a handful of containers on more than one server, new questions appear: what restarts a container when it crashes? What spreads traffic between five copies? How do you release a new version without downtime? Kubernetes handles these. You tell it the state you want, and it works to keep reality matching. A minimal description looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/my-app:1.0&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That says: "I want three copies of this container running." If one dies or a server fails, Kubernetes starts a replacement. To roll out version 1.1, you change the image tag, and Kubernetes replaces the copies gradually. Kubernetes answers: "How do I keep many containers running, scaled and updated across many machines?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Do Kubernetes and Docker work together?
&lt;/h2&gt;

&lt;p&gt;Yes. Images you build with Docker run on Kubernetes. The Kubernetes project says so directly: "Docker-produced images will continue to work in your cluster with all runtimes, as they always have."&lt;/p&gt;

&lt;p&gt;The confusion comes from a change in Kubernetes 1.24, which removed "dockershim", the small piece of code that let Kubernetes talk to the Docker Engine directly. Kubernetes now talks to container runtimes through its own standard interface (CRI), using runtimes such as containerd or CRI-O. For you as a developer nothing changes: you still build with Docker, push the image to a registry, and Kubernetes pulls and runs it. What changed is only which program on the server starts the container.&lt;/p&gt;

&lt;h2&gt;
  
  
  The middle step: Docker Compose
&lt;/h2&gt;

&lt;p&gt;Between "one container" and "a Kubernetes cluster" sits Docker Compose. It describes several containers on one machine in a single file, for example an app and its database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8.4&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;change-me&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For many small and medium projects, a single server with Docker Compose is enough, and far simpler to run than a cluster. (I confirmed that Docker Compose v5.5.1 is available with the Docker install I tested. I did not start this particular stack, because the sandbox could not download images.)&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use which
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Docker alone (or with Compose)&lt;/strong&gt; for local development, CI builds, and production on one or a few servers with modest traffic. You get reproducible environments with little to learn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move to Kubernetes&lt;/strong&gt; when you run many services, need several copies for availability, need zero-downtime updates, or must scale up and down automatically. The cost is real: you need to learn its concepts, and someone has to operate the cluster or you pay for a managed service from a cloud provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not adopt Kubernetes to look modern.&lt;/strong&gt; If a single server handles your load, a cluster adds work without adding value. Teams often start on Compose and move when a specific problem, such as needing automatic failover, justifies it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Kubernetes a replacement for Docker?
&lt;/h3&gt;

&lt;p&gt;No. They do different jobs. Docker builds and runs containers on one machine. Kubernetes coordinates containers across many machines. Kubernetes needs container images, and Docker is a common way to create them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to learn Docker before Kubernetes?
&lt;/h3&gt;

&lt;p&gt;Yes, in practice. Kubernetes manages containers, so you should understand images, containers, ports and volumes first. Learning them with Docker takes days. Kubernetes takes much longer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Docker free?
&lt;/h3&gt;

&lt;p&gt;The Docker Engine used in my test is free to install on Linux. Docker Desktop and some Docker services have their own license terms for larger companies, so check Docker's current terms for your situation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What version of Kubernetes is current?
&lt;/h3&gt;

&lt;p&gt;Kubernetes 1.36 was the current supported release when I checked on 5 November 2026. Its latest patch was 1.36.5 (released 15 September 2026), with end of life on 28 June 2027. Versions change every few months, so check the &lt;a href="https://kubernetes.io/releases/" rel="noopener noreferrer"&gt;release page&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run Kubernetes on my laptop?
&lt;/h3&gt;

&lt;p&gt;Yes, with tools designed for learning and testing, such as kind or minikube. I did not test them here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/" rel="noopener noreferrer"&gt;Kubernetes Blog: Don't Panic: Kubernetes and Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/blog/2022/02/17/dockershim-faq" rel="noopener noreferrer"&gt;Kubernetes Blog: Dockershim Removal FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/releases/1.36/" rel="noopener noreferrer"&gt;Kubernetes 1.36 release page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/get-started/" rel="noopener noreferrer"&gt;Docker Documentation: Get started&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/docker-vs-kubernetes-difference" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>containers</category>
    </item>
    <item>
      <title>What Developers Use and Search in 2026: Survey Data</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Sat, 07 Nov 2026 11:11:11 +0000</pubDate>
      <link>https://dev.to/devtalk94/what-developers-use-and-search-in-2026-survey-data-3ki6</link>
      <guid>https://dev.to/devtalk94/what-developers-use-and-search-in-2026-survey-data-3ki6</guid>
      <description>&lt;p&gt;If you ask "what are developers searching for these days", the honest answer is not a list of keywords. Search volume data from Google is not public in a form anyone can quote reliably, and most "top search" lists online are guesses. What can be checked is what developers say they use, how they look for answers, and what they actually push to GitHub. Two large reports landed in the last five weeks: the Stack Overflow Developer Survey 2026 (published 6 October) and GitHub's Octoverse 2026 (published 28 October). This article pulls out the numbers that matter for working developers and says what each one does and does not tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where developers look for answers has changed
&lt;/h2&gt;

&lt;p&gt;This is the headline for anyone who writes or reads technical content. From the Stack Overflow 2026 survey, which had more than 30,000 respondents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;83% search online for answers, and 70% ask an AI agent. Many do both.&lt;/li&gt;
&lt;li&gt;64% visit Stack Overflow less often for simple questions.&lt;/li&gt;
&lt;li&gt;16% visit Stack Overflow more often, to check where an AI answer came from.&lt;/li&gt;
&lt;li&gt;93% say source attribution is needed for them to trust AI output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What do developers do after an AI answer arrives? The survey asked (about 13,000 answers, multiple choices allowed):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Next step after an AI response&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run it locally&lt;/td&gt;
&lt;td&gt;76.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compare against the codebase&lt;/td&gt;
&lt;td&gt;63.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inspect tests and security implications&lt;/td&gt;
&lt;td&gt;53.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read official documentation&lt;/td&gt;
&lt;td&gt;38.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search Stack Overflow&lt;/td&gt;
&lt;td&gt;12.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ask a coworker&lt;/td&gt;
&lt;td&gt;6.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use as-is&lt;/td&gt;
&lt;td&gt;9.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only about one in ten uses the answer without checking it. Verification, not generation, is where the search effort has moved. The trust question points the same way: 48.0% said they trust AI output when they can easily verify it, 12.3% do not trust it for most work tasks, and 6.6% trust it for many tasks including important decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The languages and databases in daily use
&lt;/h2&gt;

&lt;p&gt;The survey's technology page gives these shares of respondents:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;62.0%&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;57.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;58.4%&lt;/td&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;42.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML/CSS&lt;/td&gt;
&lt;td&gt;58.2%&lt;/td&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;35.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;58.0%&lt;/td&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;td&gt;28.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bash/Shell&lt;/td&gt;
&lt;td&gt;51.3%&lt;/td&gt;
&lt;td&gt;Microsoft SQL Server&lt;/td&gt;
&lt;td&gt;27.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;43.8%&lt;/td&gt;
&lt;td&gt;MariaDB&lt;/td&gt;
&lt;td&gt;20.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;27.6%&lt;/td&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;td&gt;19.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C#&lt;/td&gt;
&lt;td&gt;27.1%&lt;/td&gt;
&lt;td&gt;Elasticsearch&lt;/td&gt;
&lt;td&gt;14.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things stand out. SQL moved ahead of HTML/CSS, but by 0.2 percentage points, which is within the noise of a survey. PostgreSQL is used by well over half of respondents, far above MySQL. And Redis and MariaDB each sit in the top seven databases, so the "classic" stack of PHP or Python with MySQL, MariaDB and Redis is a long way from disappearing. Rust moved up to 12th place among languages, though the survey summary gives no percentage for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GitHub's own data adds
&lt;/h2&gt;

&lt;p&gt;Surveys record what people say. GitHub records what they push. Octoverse 2026 covers the year from 1 October 2025 to 30 September 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Volume:&lt;/strong&gt; 17.9 billion pushes, 34.3 billion commits (public and private) and 1.3 billion merged pull requests. GitHub reports more than 225 million developers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Languages:&lt;/strong&gt; TypeScript is first, Python second and JavaScript third, the same order as 2025, when TypeScript passed Python for the first time. Net monthly contributor gains over the year were 3.62 million for TypeScript, 3.23 million for Python and 1.82 million for JavaScript. Rust "doubled" over the past year, per GitHub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents:&lt;/strong&gt; more than 40% of pull requests started with an agent's first draft in September 2026, up from 17% in March 2026. That is 110 million agent-started pull requests in a single month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fastest-growing projects:&lt;/strong&gt; the lists are dominated by AI tooling, with &lt;code&gt;anthropics/claude-code&lt;/code&gt;, &lt;code&gt;ggml-org/llama.cpp&lt;/code&gt;, &lt;code&gt;vllm-project/vllm&lt;/code&gt; and &lt;code&gt;openai/codex&lt;/code&gt; among those named.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that the Octoverse language ranking counts contributors, which is a different measure from the survey's "used in the past year". That is why JavaScript leads the survey while TypeScript leads GitHub. GitHub also reports that, among repositories created in 2026, those with an agent configuration file logged eight times as many commits as those without. That is a correlation: projects that adopt agent tooling may simply be the more active ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI tools in practice
&lt;/h2&gt;

&lt;p&gt;Among the survey respondents who use AI coding assistants or agents (12,255 answered), the most used agents were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;65.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;58.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Codex&lt;/td&gt;
&lt;td&gt;29.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;25.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Antigravity&lt;/td&gt;
&lt;td&gt;16.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini Code Assist&lt;/td&gt;
&lt;td&gt;14.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Of those who use AI tools, 73% use them daily, and 31% of the daily users spend four or more hours a day with them. The main uses are generating code in familiar areas (67%) and debugging (61%). Only 20% use AI for deploying, operating or troubleshooting production systems. Treat the tool ranking with care: the respondents chose to take a Stack Overflow survey and are not a random sample of developers, and the figures are shares of people who answered that question.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means if you build or write for developers
&lt;/h2&gt;

&lt;p&gt;The following points are my reading of the numbers above, not extra data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Short factual questions are going to AI chat, and the checking is going to documentation.&lt;/strong&gt; Pages that survive are those with something a chat answer cannot easily give: tested commands, real output, version-specific behaviour, and sources that can be cited. With 93% asking for attribution, citing the official docs is no longer optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification skills are in demand.&lt;/strong&gt; Three out of four developers run AI-generated code locally before trusting it. Tutorials on testing, reading stack traces, reviewing diffs and security checks fit that behaviour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL and PostgreSQL knowledge keeps its value.&lt;/strong&gt; Both are at or near the top of the survey, and AI tools produce queries that still need a person who can read an execution plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript is the default for new work&lt;/strong&gt; on GitHub, with Python close behind. GitHub's own 2025 report argued that type checking helps catch errors in AI-generated code. That is their explanation, not a settled fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old stacks still matter.&lt;/strong&gt; MySQL, MariaDB, Redis and Bash are all in the survey's top ten for their category. (The survey figures I read do not break out PHP, so I make no claim about it.) Maintenance, upgrade and troubleshooting topics for them have a steady audience, but I have no search volume data to put numbers on that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What these numbers do not tell you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;They are not search volumes. A technology that few people "use" can still draw many searches because it is hard to configure.&lt;/li&gt;
&lt;li&gt;Survey samples are self-selected, and the survey pages do not always say which population a chart covers. Where they do not, I say so above.&lt;/li&gt;
&lt;li&gt;Octoverse counts activity on GitHub, including private repositories, so it leans toward teams that host code there.&lt;/li&gt;
&lt;li&gt;For actual search demand on a topic, use Google Search Console for your own site and a keyword tool you trust. I did not use one here.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the most popular programming language in 2026?
&lt;/h3&gt;

&lt;p&gt;It depends on the measure. JavaScript leads the Stack Overflow survey at 62.0% of respondents, and TypeScript leads GitHub by monthly contributors, ahead of Python and JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Stack Overflow still relevant?
&lt;/h3&gt;

&lt;p&gt;The survey says 64% visit it less often for simple questions, while 16% visit more to check AI-provided sources. It is used less for quick answers and more as a reference to verify them.&lt;/p&gt;

&lt;h3&gt;
  
  
  What database do developers use most?
&lt;/h3&gt;

&lt;p&gt;PostgreSQL, at 57.9% of respondents in the survey, followed by SQLite (42.1%) and MySQL (35.1%).&lt;/p&gt;

&lt;h3&gt;
  
  
  Are AI agents replacing developers?
&lt;/h3&gt;

&lt;p&gt;The data shows something narrower: agents now open a large share of pull requests (over 40% in September 2026), and developers are still the ones reviewing, merging and verifying. Nothing in these reports measures replacement.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/what-developers-search-and-use-2026" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>developersurvey</category>
      <category>octoverse</category>
      <category>typescript</category>
      <category>python</category>
    </item>
    <item>
      <title>10 Claude Code Commands Every Developer Should Know</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Thu, 05 Nov 2026 17:08:32 +0000</pubDate>
      <link>https://dev.to/devtalk94/10-claude-code-commands-every-developer-should-know-50kh</link>
      <guid>https://dev.to/devtalk94/10-claude-code-commands-every-developer-should-know-50kh</guid>
      <description>&lt;p&gt;Most people start Claude Code, type a request and hope for the best. That works until the conversation gets long, the answers get worse, and a change you did not want lands in your working tree. A small set of slash commands fixes most of that: they control what Claude remembers, how much context it carries, whether it may act, and how you get back when something goes wrong. These ten are the ones worth learning first, followed by a few command-line options for scripts and CI.&lt;/p&gt;

&lt;p&gt;Everything here was checked against Claude Code 2.1.318. Commands change between versions, so type &lt;code&gt;/help&lt;/code&gt; in your own install to see what yours has.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ten at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Use it to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a &lt;code&gt;CLAUDE.md&lt;/code&gt; for your project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Edit &lt;code&gt;CLAUDE.md&lt;/code&gt; files and memory settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/clear&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start fresh when the topic changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/compact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Summarize a long session and keep going&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;See what is using the context window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/plan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Plan before any code changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/rewind&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Go back to an earlier point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/resume&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Continue an earlier conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/permissions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Control what Claude may run without asking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/usage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check cost and usage (aliases: &lt;code&gt;/cost&lt;/code&gt;, &lt;code&gt;/stats&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. /init: give Claude project knowledge
&lt;/h2&gt;

&lt;p&gt;Claude starts every session without knowing your project. &lt;code&gt;/init&lt;/code&gt; analyzes the codebase and creates a &lt;code&gt;CLAUDE.md&lt;/code&gt; file with documentation about it, which Claude reads at the start of future sessions. Run it once in the project root, then edit the result. A generated file is a draft: delete anything wrong or vague, and add the facts only you know. For a Laravel project, the useful lines are the ones that save a wrong guess:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Project notes&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Run tests: php artisan test
&lt;span class="p"&gt;-&lt;/span&gt; Format code: ./vendor/bin/pint --dirty
&lt;span class="p"&gt;-&lt;/span&gt; Do not edit migrations that are already merged; add a new one.
&lt;span class="p"&gt;-&lt;/span&gt; Authorization lives in app/Policies; every controller action must use it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. /memory: edit what Claude remembers
&lt;/h2&gt;

&lt;p&gt;The command opens your &lt;code&gt;CLAUDE.md&lt;/code&gt; files and memory settings for editing. Use it when you catch yourself correcting the same thing twice. Put the rule in the file once instead of repeating it in every session. Keep the file short, because everything in it is loaded into context each time.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. /clear: start clean
&lt;/h2&gt;

&lt;p&gt;It starts a new session with empty context. The previous session stays on disk and can be reopened with &lt;code&gt;/resume&lt;/code&gt;, so nothing is lost. Use it between unrelated tasks. A long conversation about a billing bug is noise when you move on to a CSS problem, and stale context makes answers worse and costs more.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. /compact: shrink a long session instead of abandoning it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/compact&lt;/code&gt; summarizes the conversation so far to free up context. It accepts optional instructions for what the summary should keep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/compact keep the failing test names, the files we changed, and the decision to use a queue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it when you are mid-task and want to continue. Use &lt;code&gt;/clear&lt;/code&gt; when you are done with the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. /context: see where the context goes
&lt;/h2&gt;

&lt;p&gt;It shows current context usage by category. I ran it in a fresh session and got a table like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**Tokens:** 31.7k / 500k (6%)

| Category      | Tokens | Percentage |
| System prompt | 3.7k   | 0.7%       |
| System tools  | 21.2k  | 4.2%       |
| Skills        | 6.8k   | 1.4%       |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your numbers will differ with your model, tools and settings. The point is that even an empty session uses some of the window, and MCP servers, skills and big &lt;code&gt;CLAUDE.md&lt;/code&gt; files add to it. When responses start degrading in a long session, check this before blaming the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. /plan: think before changing files
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/plan&lt;/code&gt; enables plan mode, or shows the current session plan if one exists. Use it for any change that touches more than a file or two: ask for the approach, read it, correct it, then let Claude implement. Catching a wrong approach in a plan costs a minute. Catching it in a 20-file diff costs an hour. Plan mode is also one of the permission modes you can start a session with, using &lt;code&gt;claude --permission-mode plan&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. /rewind: undo to an earlier point
&lt;/h2&gt;

&lt;p&gt;The command restores the code and/or the conversation to a previous point. It also answers to &lt;code&gt;/checkpoint&lt;/code&gt; and &lt;code&gt;/undo&lt;/code&gt;. It is the fast way out when an approach goes wrong. Do not treat it as a replacement for git: commit before larger tasks, and check the Claude Code documentation for exactly what is tracked, since changes made outside Claude's own edits may not be.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. /resume: pick up where you left off
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/resume&lt;/code&gt; (alias &lt;code&gt;/continue&lt;/code&gt;) reopens a previous conversation, and accepts a session ID or a search term. From the terminal, two flags do the same job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;-c&lt;/span&gt;          &lt;span class="c"&gt;# continue the most recent conversation in this directory&lt;/span&gt;
claude &lt;span class="nt"&gt;-r&lt;/span&gt;          &lt;span class="c"&gt;# open the picker; or: claude -r "invoice export"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tested &lt;code&gt;claude -c&lt;/code&gt; in a scratch directory: it resumed the previous session and could repeat my earlier message back. Name sessions with &lt;code&gt;-n&lt;/code&gt; or &lt;code&gt;--name&lt;/code&gt; when you start them, so they are easy to find later.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. /permissions: decide what runs without asking
&lt;/h2&gt;

&lt;p&gt;It manages allow and deny rules for tools. Allow the safe, repetitive commands (running tests, &lt;code&gt;git status&lt;/code&gt;) so you are not clicking "approve" all day, and leave everything else on ask. Be conservative: an allow rule is a standing permission. The same rules can be given at startup. The CLI help shows the syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Bash(git *) Edit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; on a normal machine. The help text itself recommends it only for sandboxes with no internet access.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. /usage: know what you are spending
&lt;/h2&gt;

&lt;p&gt;It shows session cost, plan usage and activity statistics, under the aliases &lt;code&gt;/cost&lt;/code&gt; and &lt;code&gt;/stats&lt;/code&gt; too. Check it after long agentic runs, and before you decide to automate something at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: use Claude Code in scripts
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;-p&lt;/code&gt; (&lt;code&gt;--print&lt;/code&gt;) flag runs one prompt, prints the answer and exits, which makes Claude usable in shell pipelines and CI. Pipe data in, and ask for structured output when another program reads the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan &lt;span class="nb"&gt;test &lt;/span&gt;2&amp;gt;&amp;amp;1 | claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Summarize the failures in one sentence each"&lt;/span&gt;

claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"List the routes without auth middleware"&lt;/span&gt; &lt;span class="nt"&gt;--output-format&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I piped a failing test line (&lt;code&gt;Expected 200 but received 419&lt;/code&gt;) into &lt;code&gt;claude -p&lt;/code&gt; and got back an explanation that a 419 in Laravel means a CSRF token problem. The JSON output format returns one object. In my run, the answer was in the &lt;code&gt;result&lt;/code&gt; field, and the object also included fields such as &lt;code&gt;session_id&lt;/code&gt;, &lt;code&gt;total_cost_usd&lt;/code&gt; and &lt;code&gt;usage&lt;/code&gt;, plus many other diagnostic keys, so read only the fields you need. The other output formats are &lt;code&gt;text&lt;/code&gt; (the default) and &lt;code&gt;stream-json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two more options worth knowing: &lt;code&gt;claude -w feature-x&lt;/code&gt; starts the session in a new git worktree, so experiments do not touch your main checkout, and &lt;code&gt;claude doctor&lt;/code&gt; checks the health of your installation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never clearing.&lt;/strong&gt; One endless session accumulates stale context. Use &lt;code&gt;/clear&lt;/code&gt; between tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An unreviewed CLAUDE.md.&lt;/strong&gt; A bloated or wrong file misleads every session. Edit it like code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping the plan.&lt;/strong&gt; Large changes without a plan produce large diffs that are hard to review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting rewind instead of version control.&lt;/strong&gt; Commit before big tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow-listing broad commands.&lt;/strong&gt; A narrow rule such as &lt;code&gt;Bash(git *)&lt;/code&gt; is safer than allowing arbitrary shell access.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I see all available commands?
&lt;/h3&gt;

&lt;p&gt;Type &lt;code&gt;/help&lt;/code&gt; in a session. It shows help and the available commands for your version.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between /clear and /compact?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;/clear&lt;/code&gt; starts a new session with empty context. &lt;code&gt;/compact&lt;/code&gt; keeps the same session but replaces the history with a summary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do these commands work the same in an IDE?
&lt;/h3&gt;

&lt;p&gt;I only tested the terminal version. Check your IDE integration's documentation for differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run slash commands in print mode?
&lt;/h3&gt;

&lt;p&gt;Some, yes. In my tests &lt;code&gt;/context&lt;/code&gt; and &lt;code&gt;/usage&lt;/code&gt; worked with &lt;code&gt;claude -p&lt;/code&gt;. Others, such as &lt;code&gt;/rewind&lt;/code&gt;, are interactive only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/cli-reference" rel="noopener noreferrer"&gt;Claude Code documentation: CLI reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/claude-code/overview" rel="noopener noreferrer"&gt;Claude Code documentation: Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@anthropic-ai/claude-code" rel="noopener noreferrer"&gt;@anthropic-ai/claude-code on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/claude-code-commands-every-developer-should-know" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>aicoding</category>
      <category>cli</category>
      <category>developertools</category>
    </item>
    <item>
      <title>Laravel Herd Setup: Sites, PHP Versions, HTTPS and Databases</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Thu, 05 Nov 2026 15:29:05 +0000</pubDate>
      <link>https://dev.to/devtalk94/laravel-herd-setup-sites-php-versions-https-and-databases-6n1</link>
      <guid>https://dev.to/devtalk94/laravel-herd-setup-sites-php-versions-https-and-databases-6n1</guid>
      <description>&lt;p&gt;Laravel Herd is a local development environment for macOS and Windows that runs PHP and nginx natively, with no Docker and no virtual machine. You put a project in a folder, and it is available at &lt;code&gt;project-name.test&lt;/code&gt; a moment later. This guide covers what you get, how to set up a site, how to switch PHP versions per project, how to turn on HTTPS, and where databases fit, using the commands from Herd's own documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Herd is, and where it runs
&lt;/h2&gt;

&lt;p&gt;Herd bundles PHP, nginx, a local DNS service (so &lt;code&gt;.test&lt;/code&gt; domains resolve) and Node. It is available for macOS and Windows. Linux is not supported: the Herd website says the team is working on a Linux version but it is not ready to ship, so on Linux, Laravel Sail or a manual setup is still the route. Plans, as stated on the Herd site at the time of writing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Includes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Herd (free)&lt;/td&gt;
&lt;td&gt;No cost, no license&lt;/td&gt;
&lt;td&gt;PHP, nginx, DNS and Node; per-project site configuration; Herd Studio; the PHP Profiler; community support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Herd Pro&lt;/td&gt;
&lt;td&gt;$99 per year&lt;/td&gt;
&lt;td&gt;Everything in free, plus MySQL, PostgreSQL, Redis and more as managed services; Mail; Insights; Tinker; Xdebug detection; email support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Herd for Teams&lt;/td&gt;
&lt;td&gt;$299 per year&lt;/td&gt;
&lt;td&gt;10 Pro licenses, a license portal, shared project configuration, priority email support&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prices and plan contents change, so check the &lt;a href="https://herd.laravel.com/" rel="noopener noreferrer"&gt;Herd website&lt;/a&gt; before you budget around them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create your first site
&lt;/h2&gt;

&lt;p&gt;Herd parks one directory for you by default: &lt;code&gt;~/Herd&lt;/code&gt; on macOS and &lt;code&gt;%USERPROFILE%\Herd&lt;/code&gt; on Windows. Every folder inside a parked directory is served as &lt;code&gt;http://folder-name.test&lt;/code&gt;. So the fastest start is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Herd
laravel new my-app
&lt;span class="nb"&gt;cd &lt;/span&gt;my-app
herd open
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;laravel new&lt;/code&gt; creates the application and &lt;code&gt;herd open&lt;/code&gt; opens &lt;code&gt;http://my-app.test&lt;/code&gt; in your browser. The Herd documentation notes that new Laravel apps use SQLite by default, so this works before you install any database server.&lt;/p&gt;

&lt;h3&gt;
  
  
  A project that lives somewhere else
&lt;/h3&gt;

&lt;p&gt;You do not have to move existing projects into the Herd directory. Link one from its own folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/code/client-portal
herd &lt;span class="nb"&gt;link &lt;/span&gt;client-portal      &lt;span class="c"&gt;# served at http://client-portal.test&lt;/span&gt;
herd links                   &lt;span class="c"&gt;# list linked sites&lt;/span&gt;
herd &lt;span class="nb"&gt;unlink &lt;/span&gt;client-portal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or park a whole folder of projects so that every subfolder becomes a site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;herd park ~/code
herd parked                  &lt;span class="c"&gt;# list sites inside parked paths&lt;/span&gt;
herd forget ~/code           &lt;span class="c"&gt;# stop serving that folder&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  HTTPS for a local site
&lt;/h2&gt;

&lt;p&gt;From the project directory, generate a trusted certificate for the site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;herd secure
herd secured      &lt;span class="c"&gt;# lists secured sites and when their certificates expire&lt;/span&gt;
herd unsecure     &lt;span class="c"&gt;# remove the certificate and the HTTPS redirect&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this when your app needs HTTPS to behave properly, for example secure cookies, OAuth callbacks, or webhooks that refuse plain HTTP. Remember to set &lt;code&gt;APP_URL&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; to the &lt;code&gt;https://&lt;/code&gt; address afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  PHP versions
&lt;/h2&gt;

&lt;p&gt;Herd ships with the latest stable PHP, which its documentation lists as PHP 8.5 at the time of writing. You set a global version, and you can pin individual sites to another one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;herd php:list              &lt;span class="c"&gt;# versions and install status&lt;/span&gt;
herd php:install 8.3       &lt;span class="c"&gt;# install another version&lt;/span&gt;
herd use 8.4               &lt;span class="c"&gt;# global version for sites that are not isolated&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Herd/legacy-app
herd isolate 8.2           &lt;span class="c"&gt;# pin this site to PHP 8.2&lt;/span&gt;
herd isolated              &lt;span class="c"&gt;# list pinned sites&lt;/span&gt;
herd unisolate             &lt;span class="c"&gt;# go back to the global version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the folder name and the site name differ, add &lt;code&gt;--site="site-name"&lt;/code&gt; to &lt;code&gt;herd isolate&lt;/code&gt;. The same thing is available in the app: select the site in the Applications window and choose a version in the Environment section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use the right PHP in the terminal
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;php&lt;/code&gt; that your shell finds is not necessarily the version Herd serves for that site. Herd provides wrappers that pick the version configured for the current directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;herd php artisan migrate
herd composer &lt;span class="nb"&gt;install
&lt;/span&gt;herd which-php             &lt;span class="c"&gt;# which PHP binary serves this path&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use these when you see "works in the browser, fails in the terminal" or a Composer platform error that mentions the wrong PHP version. Node works the same way: &lt;code&gt;herd node:install 22&lt;/code&gt; installs a version through Herd's nvm, and &lt;code&gt;herd isolate-node&lt;/code&gt; pins one to the current directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Databases
&lt;/h2&gt;

&lt;p&gt;This is the part that differs between plans. Herd's PHP includes the extensions for MySQL, MariaDB, PostgreSQL, MongoDB, Redis, Valkey and SQLite, so your code can talk to any of them, but the database servers themselves are another matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free:&lt;/strong&gt; SQLite works out of the box. For MySQL, PostgreSQL or Redis, install the server from its vendor or use a free tool such as DBngin, and point your &lt;code&gt;.env&lt;/code&gt; at it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro:&lt;/strong&gt; Herd manages the services for you. You switch an application to a service in the Applications window, Herd updates &lt;code&gt;.env&lt;/code&gt; and can create the database. The CLI mirrors this, for example &lt;code&gt;herd services:list&lt;/code&gt;, &lt;code&gt;herd services:create &amp;lt;type&amp;gt;&lt;/code&gt; and &lt;code&gt;herd db:create&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The documentation pages I read do not list default ports or credentials, so read them from &lt;code&gt;herd db:connection&lt;/code&gt; (Pro) or from your own database tool instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other things worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Health check:&lt;/strong&gt; &lt;code&gt;herd status&lt;/code&gt; shows whether Herd and its services are running. &lt;code&gt;herd restart&lt;/code&gt; restarts them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs:&lt;/strong&gt; &lt;code&gt;herd log&lt;/code&gt; tails Herd's log files, which is where to look when a site returns an nginx error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proxies:&lt;/strong&gt; &lt;code&gt;herd proxy my-api http://127.0.0.1:3000&lt;/code&gt; creates an nginx proxy host, useful for a Node server or a frontend dev server next to Laravel. (Check the exact target format against &lt;code&gt;herd proxy&lt;/code&gt; in your version.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sharing:&lt;/strong&gt; &lt;code&gt;herd share&lt;/code&gt; exposes a site through an Expose tunnel, handy for webhook testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging:&lt;/strong&gt; &lt;code&gt;herd debug&lt;/code&gt; runs PHP with the site's version and a debug configuration, and &lt;code&gt;herd coverage&lt;/code&gt; runs it with Xdebug coverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project manifest:&lt;/strong&gt; &lt;code&gt;herd init&lt;/code&gt; configures a project from a &lt;code&gt;herd.yml&lt;/code&gt; file, so a team can share setup. See the docs for the format.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common problems
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.test&lt;/code&gt;&lt;strong&gt;site does not load.&lt;/strong&gt; Run &lt;code&gt;herd status&lt;/code&gt; to see whether nginx, DNS and PHP are running, then &lt;code&gt;herd restart&lt;/code&gt;. Check that the folder really sits in a parked path (&lt;code&gt;herd paths&lt;/code&gt;) or is linked (&lt;code&gt;herd links&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong PHP version.&lt;/strong&gt; Compare &lt;code&gt;herd which-php&lt;/code&gt; with &lt;code&gt;herd isolated&lt;/code&gt;, and run commands through &lt;code&gt;herd php&lt;/code&gt; and &lt;code&gt;herd composer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Another local server uses ports 80 or 443.&lt;/strong&gt; Herd and another web server cannot both listen there. Stop the other one (MAMP, an old Valet install, a Docker container publishing those ports). This is general networking behaviour, not a documented Herd message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database connection refused.&lt;/strong&gt; On the free plan nothing is running unless you installed a server yourself. Check &lt;code&gt;DB_CONNECTION&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;: a new app uses SQLite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser warns about the certificate.&lt;/strong&gt; Run &lt;code&gt;herd secure&lt;/code&gt; from the right directory, then restart the browser, since the certificate is issued per site.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Herd or Sail?
&lt;/h2&gt;

&lt;p&gt;They solve different problems. Herd runs natively, starts fast and needs no container knowledge, but it exists for macOS and Windows only, and your machine's PHP stack is not your server's stack. Sail runs your app in Docker containers, works on Linux and can match production services more closely, at the cost of Docker's overhead and learning curve. Teams on mixed operating systems, or with strict production parity needs, often pick Sail. A solo developer on a Mac who wants a quick setup will usually be happier with Herd. Neither choice locks you in, because your application code does not change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://herd.laravel.com/" rel="noopener noreferrer"&gt;Laravel Herd&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://herd.laravel.com/docs/macos/advanced-usage/herd-cli" rel="noopener noreferrer"&gt;Herd documentation: Herd CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://herd.laravel.com/docs/macos/technology/php-versions" rel="noopener noreferrer"&gt;Herd documentation: PHP versions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://herd.laravel.com/docs/macos/getting-started/databases" rel="noopener noreferrer"&gt;Herd documentation: Databases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://herd.laravel.com/docs/windows/1/getting-started/sites" rel="noopener noreferrer"&gt;Herd documentation for Windows: Sites&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/laravel-herd-setup-guide" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>herd</category>
      <category>localdevelopment</category>
      <category>php</category>
    </item>
    <item>
      <title>Laravel CORS Error with React or Vue: Causes and Fixes</title>
      <dc:creator>devTalk</dc:creator>
      <pubDate>Thu, 05 Nov 2026 15:28:41 +0000</pubDate>
      <link>https://dev.to/devtalk94/laravel-cors-error-with-react-or-vue-causes-and-fixes-17k6</link>
      <guid>https://dev.to/devtalk94/laravel-cors-error-with-react-or-vue-causes-and-fixes-17k6</guid>
      <description>&lt;p&gt;You split the project in two: Laravel serves an API on &lt;code&gt;localhost:8000&lt;/code&gt;, and a React or Vue app runs on &lt;code&gt;localhost:5173&lt;/code&gt;. The first request from the browser fails, and the console shows a message along these lines (Chrome's wording):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Access to fetch at 'http://localhost:8000/api/users' from origin 'http://localhost:5173'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API itself is usually fine. CORS is a browser rule: the browser asks the server whether a page from another origin may read its response, and the server answers with headers. Laravel already has the machinery for that answer. Most fixes are a few lines of config, and I checked each one against the real middleware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you only see it in the browser
&lt;/h2&gt;

&lt;p&gt;Two URLs have the same origin only if scheme, host and port all match. &lt;code&gt;localhost:5173&lt;/code&gt; and &lt;code&gt;localhost:8000&lt;/code&gt; differ in the port, so every request between them is cross-origin. The server never blocks anything. It sends &lt;code&gt;Access-Control-*&lt;/code&gt; headers, and the browser decides whether your JavaScript may see the response. That is why &lt;code&gt;curl&lt;/code&gt; and Postman work while the browser fails, and why the real fix always means changing what the server sends, not changing the frontend code.&lt;/p&gt;

&lt;p&gt;Requests that use &lt;code&gt;Authorization&lt;/code&gt;, JSON content types or methods like &lt;code&gt;PUT&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; trigger a &lt;strong&gt;preflight&lt;/strong&gt; first: the browser sends an &lt;code&gt;OPTIONS&lt;/code&gt; request asking permission, and only sends the real request if the answer allows it. A CORS problem is very often a failed preflight.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Laravel handles CORS
&lt;/h2&gt;

&lt;p&gt;Laravel's &lt;code&gt;HandleCors&lt;/code&gt; middleware is in the global middleware stack by default, and it uses the &lt;code&gt;fruitcake/php-cors&lt;/code&gt; library. In Laravel 11 and newer, a fresh application does not have a &lt;code&gt;config/cors.php&lt;/code&gt; file, but the framework supplies one. In the Laravel 13 source (13.38.0) the defaults are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'paths'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'api/*'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sanctum/csrf-cookie'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="s1"&gt;'allowed_methods'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'*'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="s1"&gt;'allowed_origins'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'*'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="s1"&gt;'allowed_origins_patterns'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="s1"&gt;'allowed_headers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'*'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="s1"&gt;'exposed_headers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="s1"&gt;'max_age'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'supports_credentials'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To change anything, publish the file and edit it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan config:publish cors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also note that a new Laravel 11+ skeleton has no &lt;code&gt;routes/api.php&lt;/code&gt; until you run &lt;code&gt;php artisan install:api&lt;/code&gt;. If your frontend calls &lt;code&gt;/api/...&lt;/code&gt; and gets a 404, that is a routing problem, not CORS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the preflight without a browser
&lt;/h2&gt;

&lt;p&gt;Reproduce what the browser sends and read the headers that come back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; OPTIONS http://localhost:8000/api/users &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Origin: http://localhost:5173'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Access-Control-Request-Method: POST'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Access-Control-Request-Headers: content-type,authorization'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A healthy answer is a 204 with &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;, &lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt; and &lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt;. I ran this through the real &lt;code&gt;HandleCors&lt;/code&gt; middleware and &lt;code&gt;fruitcake/php-cors&lt;/code&gt; with the default config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status 204
access-control-allow-origin: *
access-control-allow-methods: POST
access-control-allow-headers: content-type,authorization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With defaults, a plain API on &lt;code&gt;/api/*&lt;/code&gt; using bearer tokens works from any origin. If yours does not, the cause is one of the cases below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 1: The path is not in &lt;code&gt;paths&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;CORS headers are only added for URLs that match &lt;code&gt;paths&lt;/code&gt;. A request to &lt;code&gt;/login&lt;/code&gt;, &lt;code&gt;/broadcasting/auth&lt;/code&gt;, &lt;code&gt;/graphql&lt;/code&gt; or &lt;code&gt;/storage/...&lt;/code&gt; gets none, because those do not start with &lt;code&gt;api/&lt;/code&gt;. In my test, a preflight to &lt;code&gt;/login&lt;/code&gt; with the default config came back with no CORS headers at all. Add what you serve to the frontend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'paths'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'api/*'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sanctum/csrf-cookie'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'logout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'broadcasting/auth'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Patterns use the same matching as &lt;code&gt;$request-&amp;gt;is()&lt;/code&gt;, so &lt;code&gt;*&lt;/code&gt; is a wildcard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 2: Cookies or sessions (Sanctum SPA) need credentials
&lt;/h2&gt;

&lt;p&gt;If your React or Vue app logs in through cookies, the browser will not send or accept them across origins unless both sides opt in. On the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'allowed_origins'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'http://localhost:5173'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;// an explicit list, not '*'&lt;/span&gt;
&lt;span class="s1"&gt;'supports_credentials'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my test, a preflight with that configuration returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;access-control-allow-origin: http://localhost:5173
access-control-allow-credentials: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the frontend, the request must ask to include cookies. With axios that is &lt;code&gt;withCredentials: true&lt;/code&gt;, with fetch it is &lt;code&gt;credentials: 'include'&lt;/code&gt;. The Sanctum SPA setup needs more besides CORS: the stateful-domains setting, the session cookie domain, and a call to &lt;code&gt;/sanctum/csrf-cookie&lt;/code&gt; before logging in. Check the Sanctum documentation for those, since I only tested the CORS part. In Laravel 13, &lt;code&gt;$middleware-&amp;gt;statefulApi()&lt;/code&gt; in &lt;code&gt;bootstrap/app.php&lt;/code&gt; turns on Sanctum's frontend middleware.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not combine credentials with &lt;code&gt;'*'&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Browsers refuse a wildcard origin on a credentialed request. What surprised me is what Laravel does with that configuration. With &lt;code&gt;allowed_origins =&amp;gt; ['*']&lt;/code&gt; and &lt;code&gt;supports_credentials =&amp;gt; true&lt;/code&gt;, a request from &lt;code&gt;http://localhost:5173&lt;/code&gt; got this back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;access-control-allow-origin: http://localhost:5173
vary: Origin
access-control-allow-credentials: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library reflected the caller's origin instead of sending &lt;code&gt;*&lt;/code&gt;. That makes it work in the browser, and it also means any website can make credentialed requests to your API and read the answers. For production, list your real frontend origins (or use &lt;code&gt;allowed_origins_patterns&lt;/code&gt; with a strict regex) and never leave &lt;code&gt;*&lt;/code&gt; on when &lt;code&gt;supports_credentials&lt;/code&gt; is true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 3: A request header is not allowed
&lt;/h2&gt;

&lt;p&gt;The default &lt;code&gt;allowed_headers&lt;/code&gt; is &lt;code&gt;['*']&lt;/code&gt;. If you set an explicit list and forget &lt;code&gt;Authorization&lt;/code&gt; or a custom header, the preflight succeeds but the browser then blocks the real request. In my test, with &lt;code&gt;['Content-Type', 'X-Requested-With']&lt;/code&gt; and a preflight asking for &lt;code&gt;authorization&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;access-control-allow-headers: content-type, x-requested-with
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no &lt;code&gt;authorization&lt;/code&gt; in the list, so the browser refuses to send it. Add the header to the list, or go back to &lt;code&gt;['*']&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 4: The origin is not listed
&lt;/h2&gt;

&lt;p&gt;If you set &lt;code&gt;allowed_origins&lt;/code&gt; to one origin, the response carries that origin whatever the caller sends. With &lt;code&gt;['http://localhost:5173']&lt;/code&gt;, a request from &lt;code&gt;http://evil.test&lt;/code&gt; still got &lt;code&gt;access-control-allow-origin: http://localhost:5173&lt;/code&gt;, and the browser rejected it because it does not match the page. When the browser says the origin is not allowed, compare it character by character: &lt;code&gt;http&lt;/code&gt; against &lt;code&gt;https&lt;/code&gt;, a trailing slash, &lt;code&gt;127.0.0.1&lt;/code&gt; against &lt;code&gt;localhost&lt;/code&gt;, and the port are all different origins. For several environments, use a list or a pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'allowed_origins'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'FRONTEND_URL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'http://localhost:5173'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="s1"&gt;'allowed_origins_patterns'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'#^https://.*\.example\.com$#'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Things that make it look like a CORS error
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stale config.&lt;/strong&gt; After editing &lt;code&gt;config/cors.php&lt;/code&gt; on a server that caches config, run &lt;code&gt;php artisan config:clear&lt;/code&gt; (or &lt;code&gt;config:cache&lt;/code&gt; again). The cached copy wins over the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real error is something else.&lt;/strong&gt; Open the browser's Network tab and look at the status code of the request. A 404, 419 or 401 from Laravel is not a CORS problem. In my test, a 500 response from the application still carried the CORS header, so you should see the real status. Errors produced by the web server or a proxy in front of Laravel (502, 504, 413) do not pass through the middleware and arrive without CORS headers, so the browser reports them as CORS failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headers added twice.&lt;/strong&gt; If Nginx or Apache also adds &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;, the browser can receive two values and reject the response. Set CORS in one place only. I did not test this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirects.&lt;/strong&gt; A request that Laravel redirects (for example an unauthenticated browser request sent to a login page) can fail the CORS check on the redirect. Send &lt;code&gt;Accept: application/json&lt;/code&gt; from your API client so Laravel answers with JSON errors instead of redirects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev proxy as an alternative.&lt;/strong&gt; Vite can proxy &lt;code&gt;/api&lt;/code&gt; to Laravel during development, which removes CORS from local development. That hides a misconfiguration you will meet in production, so keep the real settings correct as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" rel="noopener noreferrer"&gt;MDN: Cross-Origin Resource Sharing (CORS)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://laravel.com/docs/routing#cors" rel="noopener noreferrer"&gt;Laravel Documentation: Routing, CORS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fruitcake/php-cors" rel="noopener noreferrer"&gt;fruitcake/php-cors on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://laravel.com/docs/sanctum#spa-authentication" rel="noopener noreferrer"&gt;Laravel Documentation: Sanctum, SPA authentication&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dev-talk.com/post/laravel-cors-error-react-vue-frontend" rel="noopener noreferrer"&gt;DEV Talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>cors</category>
      <category>react</category>
      <category>vue</category>
    </item>
  </channel>
</rss>
