diff --git a/.claude/skills/blog-images/SKILL.md b/.claude/skills/blog-images/SKILL.md
index e95bde48..204ab757 100644
--- a/.claude/skills/blog-images/SKILL.md
+++ b/.claude/skills/blog-images/SKILL.md
@@ -1,10 +1,11 @@
---
name: blog-images
-description: Make images for ddev.com blog posts, including terminal screenshots of DDEV output rendered with VHS, hand-written SVG feature images, and logo-and-text feature banners. Use when a post needs a screenshot of `ddev list`, `ddev st`, or other command output, or a new feature image.
+description: Make images for ddev.com blog posts, including terminal screenshots of DDEV output rendered with VHS, animated before/after comparisons of two DDEV versions, hand-written SVG feature images, and logo-and-text feature banners. Use when a post needs a screenshot of `ddev list`, `ddev st`, or other command output, a GIF or WebP recording, or a new feature image.
when_to_use: >
- Triggered by "screenshot of ddev list", "terminal screenshot", "feature
- image", "banner image", "illustration for the post", "Mermaid diagram", or
- adding any image under public/img/blog/.
+ Triggered by "screenshot of ddev list", "terminal screenshot", "gif",
+ "webp comparison", "before/after recording", "vhs", "feature image",
+ "banner image", "illustration for the post", "Mermaid diagram", or adding
+ any image under public/img/blog/.
---
# Images for blog posts
@@ -19,6 +20,9 @@ Render DDEV command output (`ddev list`, `ddev st`, the `ddev` dashboard) with
[VHS](https://github.com/charmbracelet/vhs) (`brew install vhs`, which also
needs `ttyd` and `ffmpeg`).
+- Never show the maintainer's real projects. Record demo projects with
+ realistic names, registered under `DDEV_XDG_CONFIG_HOME`, as described in
+ [gif-comparison.md](gif-comparison.md#binaries-and-projects).
- Write a `.tape` file that hides the `cd` and `clear`, shows the command,
sleeps a few seconds, and ends with `Screenshot`. Run it as
`bash -c "cd ~/tmp/shots && timeout 100 vhs name.tape"` with stdin from
@@ -41,6 +45,12 @@ needs `ttyd` and `ffmpeg`).
characters badly. A hover or click state needs a real terminal such as
iTerm2 and must be captured by hand.
+## Animated before/after comparisons
+
+To show a change between two DDEV versions, such as a faster command or a
+rebuilt `ddev tui`, record both with VHS and stack them in one animated
+WebP. Follow [gif-comparison.md](gif-comparison.md) in this directory.
+
## SVG feature images
An illustration can be a hand-written SVG referenced directly from
diff --git a/.claude/skills/blog-images/gif-comparison.md b/.claude/skills/blog-images/gif-comparison.md
new file mode 100644
index 00000000..f0e1a8e0
--- /dev/null
+++ b/.claude/skills/blog-images/gif-comparison.md
@@ -0,0 +1,118 @@
+# Animated before/after comparisons
+
+An animated WebP that plays the same steps in two DDEV versions, the old one
+stacked above the new one, each under a label bar with a caption. Examples:
+`public/img/blog/2026/07/ddev-start-v1-25-2-vs-v1-25-3.webp` and
+`public/img/blog/2026/10/ddev-tui-*-v1-25-4-vs-v1-25-5.webp`, whose tapes and
+ffmpeg filter graphs are in `public/img/blog/2026/10/vhs/`. Commit the tapes
+and filter graphs of a new comparison the same way, in a `vhs/` directory next
+to its images, so it can be re-recorded for the next release.
+
+## Tools
+
+If VHS isn't installed, download the `vhs` and `ttyd` release binaries from
+GitHub into `~/tmp/vhs-tools`, check them against the release checksums, and
+put that directory on PATH. VHS looks for `ttyd` on the PATH that the tape's
+`Env PATH` sets, so that line must include `~/tmp/vhs-tools` too. VHS uses the
+installed Chromium or Chrome.
+
+## Binaries and projects
+
+- Old version: `ddev utility download-ddev --tag v1.25.4`, which writes to
+ `~/tmp/ddev-download-ddev/v1.25.4/`.
+- New version, before its release: build upstream `main` in a `~/tmp`
+ worktree with `make VERSION=v1.25.5`, so the header shows the release
+ version instead of `v1.25.4-127-g...`.
+- Never record the maintainer's real projects. Create demo projects with
+ realistic names (`company-blog`, `docs-portal`, `storefront`) in
+ `~/tmp/sites/`, registered in an isolated global config:
+ `export DDEV_XDG_CONFIG_HOME=~/tmp/tui-demo-xdg`. Use about 15 projects with
+ 10 running, an add-on one release behind its latest, and a snapshot, so the
+ screens have something to show.
+- `ddev list` and `ddev tui` also show every running project from Docker,
+ whatever the global config says. Ask the maintainer to stop their own
+ projects before recording, and `ddev poweroff` the demo projects
+ afterwards, or they show up in the maintainer's `ddev list`.
+- Put a fake `xdg-open` that only logs its arguments first on the tape's
+ PATH, so nothing opens a browser.
+
+## Tapes
+
+Record one tape per version with identical steps. Text must stay readable
+after the blog scales a 1200px image to its column: `Set FontSize 22` with
+`Set Width 1200` gives about 78 columns, and 14 or 18 was too small. Both
+`Width` and `Height` must be even, or ffmpeg fails with "moov atom not found".
+A height of 692 is 24 rows; when content doesn't fit, show the TUI scrolling
+instead of making the terminal taller.
+
+Make several short clips, one per feature, rather than one long recording.
+Get to the starting state behind `Hide`, then pause before every action so a
+viewer can read the caption first:
+
+```text
+Output detail-new.mp4
+Set Shell bash
+Set FontSize 22
+Set Width 1200
+Set Height 692
+Set Padding 24
+Set TypingSpeed 100ms
+Env PATH "/home/me/tmp/fakebin:/home/me/tmp/vhs-tools:/path/to/ddev/bin:/usr/local/bin:/usr/bin:/bin"
+Env DDEV_XDG_CONFIG_HOME "/home/me/tmp/tui-demo-xdg"
+Env DDEV_NO_INSTRUMENTATION "true"
+Hide
+Type "cd ~/tmp/sites && clear"
+Enter
+Type "ddev"
+Enter
+Sleep 8s
+Down@200ms 4
+Sleep 1s
+Show
+Sleep 2500ms
+Enter
+Sleep 6s
+Down@600ms 16
+Sleep 3s
+```
+
+`ddev` with no arguments opens the TUI. When the command itself is part of
+the clip, type it in view and `Sleep 1s` before `Enter`. Render both tapes in
+parallel, each as `bash -c "timeout 200 vhs x.tape" &1 |
+ grep -o 'pts_time:[0-9.]*'
+```
+
+Each half gets a 64px bar: `Before: v1.25.4` in gray (`0xb0b0b0`) and
+`After: v1.25.5` in green (`0x45c98a`), both on `0x2a2a2a`. Do not use a
+bright bar color. A caption on the right names the next action, such as
+`enter opens project details`, and switches to it about two seconds before
+the key is pressed. Use the same caption in both halves; if a key does
+nothing in the old version, phrase it neutrally (`↓ in project details`).
+
+Write the filter graph to a file; ffmpeg 8 reads it with `-/filter_complex`
+(the old `-filter_complex_script` option is gone):
+
+```text
+[0:v]tpad=start_mode=clone:start_duration=1:stop_mode=clone:stop_duration=3,trim=duration=23,pad=1200:756:0:64:color=0x171717,drawbox=x=0:y=0:w=1200:h=64:color=0x2a2a2a:t=fill,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans-Bold.ttf:text='Before\: v1.25.4':x=24:y=17:fontsize=30:fontcolor=0xb0b0b0,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:fontsize=26:fontcolor=0xe0e0e0:x=w-tw-24:y=20:text='enter opens project details':enable='between(t,0,6.2)'[a];
+[1:v]...the same, with 'After\: v1.25.5' in 0x45c98a...[b];
+[a][b]vstack,fps=12[v]
+```
+
+```bash
+ffmpeg -y -i detail-old.mp4 -i detail-new.mp4 -/filter_complex detail.filter -map "[v]" \
+ -c:v libwebp_anim -lossless 0 -q:v 80 -compression_level 6 -loop 0 out.webp
+```
+
+`tpad` pads both clips to the same length and holds the last frame. Check
+frames by rendering the filter graph at a few times (`-ss 4 -frames:v 1
+-update 1 f.png`), and view them before copying the WebP to
+`public/img/blog/YYYY/MM/`. Keep each file under 2MB; 12 fps and quality 80
+gave 0.3-1.1MB for 10-20 second clips.
diff --git a/public/img/blog/2026/10/banner-ddev-v1-25-5-dark.svg b/public/img/blog/2026/10/banner-ddev-v1-25-5-dark.svg
new file mode 100644
index 00000000..ca004cbe
--- /dev/null
+++ b/public/img/blog/2026/10/banner-ddev-v1-25-5-dark.svg
@@ -0,0 +1,33 @@
+
diff --git a/public/img/blog/2026/10/banner-ddev-v1-25-5.svg b/public/img/blog/2026/10/banner-ddev-v1-25-5.svg
new file mode 100644
index 00000000..002b3117
--- /dev/null
+++ b/public/img/blog/2026/10/banner-ddev-v1-25-5.svg
@@ -0,0 +1,33 @@
+
diff --git a/public/img/blog/2026/10/ddev-launch-qr.png b/public/img/blog/2026/10/ddev-launch-qr.png
new file mode 100644
index 00000000..3d76eed5
Binary files /dev/null and b/public/img/blog/2026/10/ddev-launch-qr.png differ
diff --git a/public/img/blog/2026/10/ddev-tui-detail-v1-25-4-vs-v1-25-5.webp b/public/img/blog/2026/10/ddev-tui-detail-v1-25-4-vs-v1-25-5.webp
new file mode 100644
index 00000000..386bfe97
Binary files /dev/null and b/public/img/blog/2026/10/ddev-tui-detail-v1-25-4-vs-v1-25-5.webp differ
diff --git a/public/img/blog/2026/10/ddev-tui-help-v1-25-4-vs-v1-25-5.webp b/public/img/blog/2026/10/ddev-tui-help-v1-25-4-vs-v1-25-5.webp
new file mode 100644
index 00000000..59c6eba5
Binary files /dev/null and b/public/img/blog/2026/10/ddev-tui-help-v1-25-4-vs-v1-25-5.webp differ
diff --git a/public/img/blog/2026/10/ddev-tui-list-v1-25-4-vs-v1-25-5.webp b/public/img/blog/2026/10/ddev-tui-list-v1-25-4-vs-v1-25-5.webp
new file mode 100644
index 00000000..2755eea3
Binary files /dev/null and b/public/img/blog/2026/10/ddev-tui-list-v1-25-4-vs-v1-25-5.webp differ
diff --git a/public/img/blog/2026/10/vhs/detail-new.tape b/public/img/blog/2026/10/vhs/detail-new.tape
new file mode 100644
index 00000000..4372c876
--- /dev/null
+++ b/public/img/blog/2026/10/vhs/detail-new.tape
@@ -0,0 +1,24 @@
+Output detail-new.mp4
+Set Shell bash
+Set FontSize 22
+Set Width 1200
+Set Height 692
+Set Padding 24
+Set TypingSpeed 100ms
+Env PATH "/home/stas/tmp/fakebin:/home/stas/tmp/vhs-tools:/home/stas/tmp/wt-v1255/.gotmp/bin/linux_amd64:/usr/local/bin:/usr/bin:/bin"
+Env DDEV_XDG_CONFIG_HOME "/home/stas/tmp/tui-demo-xdg"
+Env DDEV_NO_INSTRUMENTATION "true"
+Hide
+Type "cd ~/tmp/sites && clear"
+Enter
+Type "ddev"
+Enter
+Sleep 8s
+Down@200ms 4
+Sleep 1s
+Show
+Sleep 2500ms
+Enter
+Sleep 6s
+Down@600ms 16
+Sleep 3s
diff --git a/public/img/blog/2026/10/vhs/detail-old.tape b/public/img/blog/2026/10/vhs/detail-old.tape
new file mode 100644
index 00000000..63a7048a
--- /dev/null
+++ b/public/img/blog/2026/10/vhs/detail-old.tape
@@ -0,0 +1,24 @@
+Output detail-old.mp4
+Set Shell bash
+Set FontSize 22
+Set Width 1200
+Set Height 692
+Set Padding 24
+Set TypingSpeed 100ms
+Env PATH "/home/stas/tmp/fakebin:/home/stas/tmp/vhs-tools:/home/stas/tmp/ddev-download-ddev/v1.25.4:/usr/local/bin:/usr/bin:/bin"
+Env DDEV_XDG_CONFIG_HOME "/home/stas/tmp/tui-demo-xdg"
+Env DDEV_NO_INSTRUMENTATION "true"
+Hide
+Type "cd ~/tmp/sites && clear"
+Enter
+Type "ddev"
+Enter
+Sleep 8s
+Down@200ms 4
+Sleep 1s
+Show
+Sleep 2500ms
+Enter
+Sleep 6s
+Down@600ms 16
+Sleep 3s
diff --git a/public/img/blog/2026/10/vhs/detail.filter b/public/img/blog/2026/10/vhs/detail.filter
new file mode 100644
index 00000000..3a254079
--- /dev/null
+++ b/public/img/blog/2026/10/vhs/detail.filter
@@ -0,0 +1,3 @@
+[0:v]tpad=start_mode=clone:start_duration=1:stop_mode=clone:stop_duration=3,trim=duration=23,pad=1200:756:0:64:color=0x171717,drawbox=x=0:y=0:w=1200:h=64:color=0x2a2a2a:t=fill,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans-Bold.ttf:text='Before\: v1.25.4':x=24:y=17:fontsize=30:fontcolor=0xb0b0b0,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:fontsize=26:fontcolor=0xe0e0e0:x=w-tw-24:y=20:text='enter opens project details':enable='between(t,0,6.2)',drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:fontsize=26:fontcolor=0xe0e0e0:x=w-tw-24:y=20:text='↓ in project details':enable='between(t,6.2,99)'[a];
+[1:v]tpad=start_mode=clone:start_duration=1:stop_mode=clone:stop_duration=3,trim=duration=23,pad=1200:756:0:64:color=0x171717,drawbox=x=0:y=0:w=1200:h=64:color=0x2a2a2a:t=fill,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans-Bold.ttf:text='After\: v1.25.5':x=24:y=17:fontsize=30:fontcolor=0x45c98a,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:fontsize=26:fontcolor=0xe0e0e0:x=w-tw-24:y=20:text='enter opens project details':enable='between(t,0,6.2)',drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:fontsize=26:fontcolor=0xe0e0e0:x=w-tw-24:y=20:text='↓ in project details':enable='between(t,6.2,99)'[b];
+[a][b]vstack,fps=12[v]
diff --git a/public/img/blog/2026/10/vhs/help-new.tape b/public/img/blog/2026/10/vhs/help-new.tape
new file mode 100644
index 00000000..61ea5309
--- /dev/null
+++ b/public/img/blog/2026/10/vhs/help-new.tape
@@ -0,0 +1,22 @@
+Output help-new.mp4
+Set Shell bash
+Set FontSize 22
+Set Width 1200
+Set Height 692
+Set Padding 24
+Set TypingSpeed 100ms
+Env PATH "/home/stas/tmp/fakebin:/home/stas/tmp/vhs-tools:/home/stas/tmp/wt-v1255/.gotmp/bin/linux_amd64:/usr/local/bin:/usr/bin:/bin"
+Env DDEV_XDG_CONFIG_HOME "/home/stas/tmp/tui-demo-xdg"
+Env DDEV_NO_INSTRUMENTATION "true"
+Hide
+Type "cd ~/tmp/sites && clear"
+Enter
+Type "ddev"
+Enter
+Sleep 8s
+Down@200ms 4
+Sleep 1s
+Show
+Sleep 2500ms
+Type "?"
+Sleep 6s
diff --git a/public/img/blog/2026/10/vhs/help-old.tape b/public/img/blog/2026/10/vhs/help-old.tape
new file mode 100644
index 00000000..77a0eb29
--- /dev/null
+++ b/public/img/blog/2026/10/vhs/help-old.tape
@@ -0,0 +1,22 @@
+Output help-old.mp4
+Set Shell bash
+Set FontSize 22
+Set Width 1200
+Set Height 692
+Set Padding 24
+Set TypingSpeed 100ms
+Env PATH "/home/stas/tmp/fakebin:/home/stas/tmp/vhs-tools:/home/stas/tmp/ddev-download-ddev/v1.25.4:/usr/local/bin:/usr/bin:/bin"
+Env DDEV_XDG_CONFIG_HOME "/home/stas/tmp/tui-demo-xdg"
+Env DDEV_NO_INSTRUMENTATION "true"
+Hide
+Type "cd ~/tmp/sites && clear"
+Enter
+Type "ddev"
+Enter
+Sleep 8s
+Down@200ms 4
+Sleep 1s
+Show
+Sleep 2500ms
+Type "?"
+Sleep 6s
diff --git a/public/img/blog/2026/10/vhs/help.filter b/public/img/blog/2026/10/vhs/help.filter
new file mode 100644
index 00000000..b5ea7537
--- /dev/null
+++ b/public/img/blog/2026/10/vhs/help.filter
@@ -0,0 +1,3 @@
+[0:v]tpad=start_mode=clone:start_duration=1:stop_mode=clone:stop_duration=3,trim=duration=12,pad=1200:756:0:64:color=0x171717,drawbox=x=0:y=0:w=1200:h=64:color=0x2a2a2a:t=fill,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans-Bold.ttf:text='Before\: v1.25.4':x=24:y=17:fontsize=30:fontcolor=0xb0b0b0,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:fontsize=26:fontcolor=0xe0e0e0:x=w-tw-24:y=20:text='? shows help':enable='between(t,0,99)'[a];
+[1:v]tpad=start_mode=clone:start_duration=1:stop_mode=clone:stop_duration=3,trim=duration=12,pad=1200:756:0:64:color=0x171717,drawbox=x=0:y=0:w=1200:h=64:color=0x2a2a2a:t=fill,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans-Bold.ttf:text='After\: v1.25.5':x=24:y=17:fontsize=30:fontcolor=0x45c98a,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:fontsize=26:fontcolor=0xe0e0e0:x=w-tw-24:y=20:text='? shows help':enable='between(t,0,99)'[b];
+[a][b]vstack,fps=12[v]
diff --git a/public/img/blog/2026/10/vhs/list-new.tape b/public/img/blog/2026/10/vhs/list-new.tape
new file mode 100644
index 00000000..15293545
--- /dev/null
+++ b/public/img/blog/2026/10/vhs/list-new.tape
@@ -0,0 +1,21 @@
+Output list-new.mp4
+Set Shell bash
+Set FontSize 22
+Set Width 1200
+Set Height 692
+Set Padding 24
+Set TypingSpeed 100ms
+Env PATH "/home/stas/tmp/fakebin:/home/stas/tmp/vhs-tools:/home/stas/tmp/wt-v1255/.gotmp/bin/linux_amd64:/usr/local/bin:/usr/bin:/bin"
+Env DDEV_XDG_CONFIG_HOME "/home/stas/tmp/tui-demo-xdg"
+Env DDEV_NO_INSTRUMENTATION "true"
+Hide
+Type "cd ~/tmp/sites && clear"
+Enter
+Show
+Sleep 2s
+Type "ddev"
+Sleep 1s
+Enter
+Sleep 5s
+Down@800ms 4
+Sleep 2500ms
diff --git a/public/img/blog/2026/10/vhs/list-old.tape b/public/img/blog/2026/10/vhs/list-old.tape
new file mode 100644
index 00000000..ce4c139d
--- /dev/null
+++ b/public/img/blog/2026/10/vhs/list-old.tape
@@ -0,0 +1,21 @@
+Output list-old.mp4
+Set Shell bash
+Set FontSize 22
+Set Width 1200
+Set Height 692
+Set Padding 24
+Set TypingSpeed 100ms
+Env PATH "/home/stas/tmp/fakebin:/home/stas/tmp/vhs-tools:/home/stas/tmp/ddev-download-ddev/v1.25.4:/usr/local/bin:/usr/bin:/bin"
+Env DDEV_XDG_CONFIG_HOME "/home/stas/tmp/tui-demo-xdg"
+Env DDEV_NO_INSTRUMENTATION "true"
+Hide
+Type "cd ~/tmp/sites && clear"
+Enter
+Show
+Sleep 2s
+Type "ddev"
+Sleep 1s
+Enter
+Sleep 5s
+Down@800ms 4
+Sleep 2500ms
diff --git a/public/img/blog/2026/10/vhs/list.filter b/public/img/blog/2026/10/vhs/list.filter
new file mode 100644
index 00000000..37e71d3b
--- /dev/null
+++ b/public/img/blog/2026/10/vhs/list.filter
@@ -0,0 +1,3 @@
+[0:v]tpad=start_mode=clone:start_duration=0:stop_mode=clone:stop_duration=3,trim=duration=16,pad=1200:756:0:64:color=0x171717,drawbox=x=0:y=0:w=1200:h=64:color=0x2a2a2a:t=fill,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans-Bold.ttf:text='Before\: v1.25.4':x=24:y=17:fontsize=30:fontcolor=0xb0b0b0,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:fontsize=26:fontcolor=0xe0e0e0:x=w-tw-24:y=20:text='Type ddev to open the TUI':enable='between(t,0,2.2)',drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:fontsize=26:fontcolor=0xe0e0e0:x=w-tw-24:y=20:text='Project list':enable='between(t,2.2,5.9)',drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:fontsize=26:fontcolor=0xe0e0e0:x=w-tw-24:y=20:text='↓ selects a project':enable='between(t,5.9,99)'[a];
+[1:v]tpad=start_mode=clone:start_duration=0:stop_mode=clone:stop_duration=3,trim=duration=16,pad=1200:756:0:64:color=0x171717,drawbox=x=0:y=0:w=1200:h=64:color=0x2a2a2a:t=fill,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans-Bold.ttf:text='After\: v1.25.5':x=24:y=17:fontsize=30:fontcolor=0x45c98a,drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:fontsize=26:fontcolor=0xe0e0e0:x=w-tw-24:y=20:text='Type ddev to open the TUI':enable='between(t,0,2.2)',drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:fontsize=26:fontcolor=0xe0e0e0:x=w-tw-24:y=20:text='Project list':enable='between(t,2.2,5.9)',drawtext=fontfile=/usr/share/fonts/TTF/DejaVuSans.ttf:fontsize=26:fontcolor=0xe0e0e0:x=w-tw-24:y=20:text='↓ selects a project':enable='between(t,5.9,99)'[b];
+[a][b]vstack,fps=12[v]
diff --git a/src/content/blog/php2shell-what-ddev-users-need-to-know.md b/src/content/blog/php2shell-what-ddev-users-need-to-know.md
index 4b6f7598..870db1bc 100644
--- a/src/content/blog/php2shell-what-ddev-users-need-to-know.md
+++ b/src/content/blog/php2shell-what-ddev-users-need-to-know.md
@@ -1,7 +1,9 @@
---
title: "PHP2Shell: What DDEV Users Need to Know"
pubDate: 2026-10-21
-summary: PHP fixed two critical vulnerabilities on October 21, 2026. Your local DDEV projects can't be reached from the internet, but your production servers can. Here's how to get the fixed PHP into your DDEV projects now, before DDEV v1.25.5.
+modifiedDate: 2026-10-26
+modifiedComment: "[DDEV v1.25.5](/blog/release-v1-25-5/) ships the fixed PHP, along with many other fixes and new features."
+summary: PHP fixed two critical vulnerabilities on October 21, 2026. DDEV v1.25.5 ships the fixed PHP. Your local DDEV projects can't be reached from the internet, but your production servers can, so update PHP there too.
author: Stas Zhuk
featureImage:
src: /img/blog/2026/10/banner-php2shell.svg
@@ -15,26 +17,28 @@ PHP released 8.5.12, 8.4.27, 8.3.36, and 8.2.35 on October 21, 2026. They fix CV
**TL;DR:**
-- If you use DDEV only for local development, your DDEV projects aren't affected.
+- Upgrade to [DDEV v1.25.5](release-v1-25-5.md) now, especially if you use DDEV for casual hosting or `ddev share`. Along with the fixed PHP, it brings many other fixes and new features.
+- PHP 8.6, new in v1.25.5, is still a release candidate without the fixes, so don't expose a PHP 8.6 project.
- Update PHP on the servers where your sites are hosted.
-- If you use DDEV for casual hosting or `ddev share`, get the fixed PHP into those projects now, as described below, or upgrade to DDEV v1.25.5, which we hope to release on October 22.
+- DDEV projects used only for local development can't be reached from the internet, so they aren't at risk.
## What It Means for Your DDEV Projects
-Local development isn't affected because a DDEV project listens only on your own computer, so nobody on the internet can send it a request. A project is exposed in two cases:
+Local development isn't affected because a DDEV project listens only on your own computer, so nobody on the internet can send it a request. A project is exposed in these cases:
- [Casual hosting](https://docs.ddev.com/en/stable/users/topics/hosting/) runs a public site with DDEV. An attack would run code in the project's web container rather than on the host, but that container can still change the site's files and reach its database.
-- [`ddev share`](https://docs.ddev.com/en/stable/users/usage/commands/#share) puts a project on a public URL. Don't share a project until it has the fixed PHP.
+- [`ddev share`](https://docs.ddev.com/en/stable/users/usage/commands/#share) or a tunnel add-on puts a project on a public URL. Don't share a project until it has the fixed PHP.
+- [`router_bind_all_interfaces`](https://docs.ddev.com/en/stable/users/configuration/config/#router_bind_all_interfaces) makes a project reachable from your local network.
+
+DDEV serves PHP through PHP-FPM, which is affected too, even though PHP rates the risk highest for Apache's mod_php.
:::warning[Update PHP where your sites are hosted]
Your production servers are exposed to the internet, so update PHP there first; distributions and LTS vendors got the patches in advance. PHP 8.1 and older get no updates from the PHP project, so check with your vendor or upgrade. If you can't update right away, see [Reducing Exposure](#reducing-exposure-until-you-can-update) below.
:::
-## Getting the Fixed PHP Into DDEV Before v1.25.5
-
-DDEV gets PHP from [deb.sury.org](https://deb.sury.org/). When we checked around 16:00 UTC on October 21, it had fixed packages for PHP 5.6 through 8.5 for Intel and AMD processors (AMD64), which run the vast majority of the world's web servers, most Windows and Linux computers, and Intel Macs. The builds for ARM processors (ARM64), such as Apple Silicon Macs (M1 and later), [come later](https://codeberg.org/oerdnj/deb.sury.org/issues/145), and v1.25.5 waits for them.
+## Getting the Fixed PHP Without Upgrading DDEV
-On Intel and AMD computers, you don't have to wait for v1.25.5:
+DDEV gets PHP from [deb.sury.org](https://deb.sury.org/), which has fixed packages for PHP 5.6 through 8.5. A project on an older DDEV version can install them:
- **PHP 5.6 through 8.1** aren't in the `ddev-webserver` image. DDEV installs them when it builds the project's web image, so a rebuild installs the fixed packages. The version number doesn't change for these old versions, so check that `ddev php -v` shows `built: Oct 21 2026` or later:
@@ -56,8 +60,6 @@ On Intel and AMD computers, you don't have to wait for v1.25.5:
ddev php -v
```
-We tested both cases on DDEV v1.25.4 on an Intel computer, with and without `use_hardened_images`. On Apple Silicon Macs (M1 and later) and other ARM computers, wait for deb.sury.org's ARM64 builds or for v1.25.5.
-
## Reducing Exposure Until You Can Update
PHP's advisory lists settings that block the attack paths it knows about. They don't make an installation safe, and a low `post_max_size` can break large uploads, so update as soon as you can. In a DDEV project, put them in a file and run `ddev restart`:
@@ -71,13 +73,15 @@ post_max_size = 8M
upload_max_filesize = 6M
```
+DDEV v1.25.5 already sets `session.upload_progress.enabled = 0`, and `fatal_error_backtraces = 0` on PHP 8.6.
+
On PHP 8.5, also add `fatal_error_backtraces = 0`. DDEV adds `.ddev/php` files to PHP-FPM's configuration, so they apply before PHP parses a request, as the advisory requires. On other servers, the same lines go in `php.ini` or PHP's `conf.d` directory, followed by a restart of every PHP worker. The [advisory](https://www.php.net/archive/2026.php#2026-10-21-5) has the full list.
## Thanks, Ondřej
-[Ondřej Surý](https://codeberg.org/oerdnj), who maintains the PHP packages for Debian and Ubuntu that DDEV and much of the PHP world use, published fixed packages for Intel and AMD processors for every version back to PHP 5.6 on the day PHP released the fixes, with ARM builds to follow. If your work depends on those packages, consider [sponsoring him](https://github.com/sponsors/oerdnj).
+[Ondřej Surý](https://github.com/oerdnj) maintains the PHP packages for Debian and Ubuntu that DDEV and much of the PHP world use, and built the fixes for every PHP version back to 5.6. If your work depends on those packages, consider [sponsoring him](https://github.com/sponsors/oerdnj).
-We'll update this post when DDEV v1.25.5 is out. If you have questions, reach out in any of the [support channels](https://docs.ddev.com/en/stable/users/support/).
+If you have questions, reach out in any of the [support channels](https://docs.ddev.com/en/stable/users/support/).
---
diff --git a/src/content/blog/release-v1-25-5.md b/src/content/blog/release-v1-25-5.md
new file mode 100644
index 00000000..faca2168
--- /dev/null
+++ b/src/content/blog/release-v1-25-5.md
@@ -0,0 +1,145 @@
+---
+title: "DDEV v1.25.5: PHP Security Fixes, PHP 8.6, Rebuilt TUI, SSH Agent Passthrough, QR Codes"
+pubDate: 2026-10-26
+summary: DDEV v1.25.5 ships the PHP fixes for CVE-2026-109618 and CVE-2026-109619, initial PHP 8.6 support, a rebuilt `ddev tui`, containers that use your existing SSH agent, `ddev restart -s` for single services, QR codes for `ddev share` and `ddev launch`, zstd database dumps, and global hooks.
+author: Stas Zhuk
+featureImage:
+ src: /img/blog/2026/10/banner-ddev-v1-25-5.svg
+ srcDark: /img/blog/2026/10/banner-ddev-v1-25-5-dark.svg
+ alt: DDEV v1.25.5 Release Banner
+categories:
+ - Announcements
+---
+
+[DDEV v1.25.5](https://github.com/ddev/ddev/releases/tag/v1.25.5) is out sooner than we planned, because PHP published fixes for two critical vulnerabilities and we wanted `ddev-webserver` to ship them. It also brings 142 PRs from the DDEV community that were already waiting for the next release: your suggestions, bug reports, code, and financial support made them possible.
+
+## Table of Contents
+
+## PHP Security Fixes
+
+`ddev-webserver` now ships PHP 8.5.12, 8.4.27, 8.3.36, and 8.2.35, which fix CVE-2026-109618 and CVE-2026-109619 ("PHP2Shell"). Projects on PHP 5.6 through 8.1 get deb.sury.org's fixed packages too, because upgrading DDEV rebuilds their web image. Every project picks up the fixed PHP the next time it starts. [PHP2Shell: What DDEV Users Need to Know](php2shell-what-ddev-users-need-to-know.md) explains who is affected.
+
+DDEV now applies two of PHP's mitigations by default: `session.upload_progress.enabled` is `0` in every PHP version, as PHP's [October 23 advisory](https://www.php.net/archive/2026.php#2026-10-23-1) suggests, and `fatal_error_backtraces` is `0` in PHP 8.6. Few applications use session upload progress, and the PECL `uploadprogress` extension that Drupal uses isn't affected. To turn either back on, see [Custom PHP Configuration](https://docs.ddev.com/en/stable/users/extend/customization-extendibility/#custom-php-configuration-phpini).
+
+`/phpstatus` now answers only requests from inside the web container, and with `nginx-fpm`, PHP files in CMS upload directories no longer run. If you removed `#ddev-generated` from `.ddev/nginx_full/nginx-site.conf`, merge the new rule into your copy.
+
+## Initial PHP 8.6 Support
+
+You can test a project on PHP 8.6 before its final release:
+
+```bash
+ddev config --php-version=8.6 && ddev restart
+```
+
+DDEV ships 8.6.0 RC3, which doesn't have the PHP2Shell fixes yet, because deb.sury.org hasn't packaged RC4. The mitigations above don't cover everything, so don't expose a PHP 8.6 project. Some extensions aren't packaged for 8.6 yet either, and enabling Xdebug, XHProf, or Blackfire on it shows an error instead of breaking the container.
+
+## A Rebuilt `ddev tui`
+
+Running `ddev` with no arguments opens the TUI, and it has a new layout that fits any window size and shows the project list before Docker responds. Here are 15 projects, 10 of them running, in v1.25.4 (top) and v1.25.5 (bottom). The list now says how many projects are running, and the header shows the health of the SSH agent next to the router:
+
+
+
+The project view adds the database address from your host, from the project, and from other projects; snapshots; the published ports of every service; and add-ons that have an update available, like `ddev-redis` here. When it doesn't fit the window, it scrolls:
+
+
+
+Paths and URLs are clickable links, and `o`, `v`, and `p` open the project in your file manager, VS Code, or PhpStorm. `D` deletes a project, thanks to [@MitraKumar](https://github.com/MitraKumar). `?` lists every key, and the help now fits the window:
+
+
+
+Two keys changed: `R` restarts and `r` refreshes, swapped from before, and only `enter` opens the project view.
+
+## SSH Agent Passthrough
+
+If your SSH keys live in an agent instead of `~/.ssh`, such as the macOS agent, 1Password, Secretive, a YubiKey through `gpg-agent`, or an agent forwarded with `ssh -A`, DDEV's containers can now use it directly:
+
+```bash
+ddev config global --ssh-agent-upstream=host
+ddev auth ssh # lists the keys your containers can use
+```
+
+After that, every project uses your agent, and there are no key files to add with `ddev auth ssh`. DDEV also follows a symlinked agent socket, such as VS Code Remote's `/tmp/vscode-ssh-auth-sock-*`. Which agent your containers get on macOS depends on your Docker provider, and Rancher Desktop and Podman aren't supported; [Using an Existing SSH Agent](https://docs.ddev.com/en/stable/users/usage/cli/#using-an-existing-ssh-agent) has the table.
+
+## Restart Only the Services You Name
+
+[`ddev restart -s`](https://docs.ddev.com/en/stable/users/usage/commands/#restart) recreates the services you name and leaves the rest of the project running:
+
+```bash
+$ ddev restart -s redis
+Restarting redis in project docs-portal...
+Building images for [redis].... built in 1.0s
+Container Recreated: ddev-docs-portal-redis
+Container Started: ddev-docs-portal-redis
+Waiting for containers to become ready: [redis]... ready in 0.5s
+ddev-router already running, pushing new config... ready in 0.9s
+Restarted redis in docs-portal
+```
+
+That took under four seconds here. It takes a comma-separated list, as in `ddev restart -s solr,redis`, and [`ddev utility rebuild -s`](https://docs.ddev.com/en/stable/users/usage/commands/#utility-rebuild) now accepts several services too. `ddev restart --profiles=x` replaces `ddev restart && ddev start --profiles=x`.
+
+Interrupting DDEV is also safer now. Ctrl-C, SIGTERM, or closing the terminal no longer leaves temporary containers like `start-chown-*` holding project volumes, and interrupting `ddev import-db`, `ddev export-db`, or `ddev snapshot` leaves no partial dump or snapshot behind.
+
+## QR Codes for `ddev share` and `ddev launch`
+
+[`ddev share`](https://docs.ddev.com/en/stable/users/usage/commands/#share) prints a QR code for the tunnel URL, so you can open the shared site on your phone without typing it. [`ddev launch --qr`](https://docs.ddev.com/en/stable/users/usage/commands/#launch) prints the project URL with its QR code:
+
+
+
+This replaces the [ddev-qr](https://github.com/ddev/ddev-qr) add-on, which `ddev start` now removes. Thanks to [@tyler36](https://github.com/tyler36), who started all of this in ddev-qr.
+
+## zstd for Database and File Imports and Exports
+
+[`ddev import-db`](https://docs.ddev.com/en/stable/users/usage/commands/#import-db) and [`ddev import-files`](https://docs.ddev.com/en/stable/users/usage/commands/#import-files) accept zstd files, and [`ddev export-db`](https://docs.ddev.com/en/stable/users/usage/commands/#export-db) picks the format from the file extension:
+
+```bash
+ddev export-db -f db.sql.zst
+ddev import-db -f db.sql.zst
+```
+
+`--compression` chooses gzip, bzip2, xz, zstd, or none explicitly. One consequence: `ddev export-db --file=x.sql` now writes plain SQL. Before, it wrote gzip data to a file named `.sql`.
+
+## Global Hooks and `global_config.*.yaml`
+
+[Global hooks](https://docs.ddev.com/en/stable/users/configuration/hooks/#global-hooks) in `~/.ddev/global_config.yaml` run for every project, before the project's own hooks:
+
+```yaml
+hooks:
+ post-start:
+ - exec-host: "echo a global hook ran"
+```
+
+A project can opt out with `skip_global_hooks: true`. [`global_config.*.yaml` files](https://docs.ddev.com/en/stable/users/configuration/config/#environmental-overrides) in `~/.ddev` are merged over `global_config.yaml`, the way `config.*.yaml` files are in a project, so a team can hand out a `global_config.team.yaml` without touching the file DDEV rewrites. Check `~/.ddev` for leftovers like `global_config.old.yaml`, because those are merged now too.
+
+New [`pre-delete` and `post-delete` hooks](https://docs.ddev.com/en/stable/users/configuration/hooks/#supported-command-hooks) clean up files a project created, without doing it on every `ddev stop`, thanks to [@wazum](https://github.com/wazum).
+
+## Changes That Might Affect You
+
+- WP-CLI, Drush 8, the Backdrop Drush extension, magerun, magerun2, and Symfony CLI are installed only in the project types that use them. To use one elsewhere, add it in a `web-build` Dockerfile; see [Project-Specific Utilities](https://docs.ddev.com/en/stable/users/extend/customizing-images/#project-specific-utilities).
+- A custom service or add-on whose `container_name` isn't `ddev--` is renamed, with a warning. Reach it by its service name instead.
+- DDEV assigns host ports itself. Unconfigured db, web, and Mailpit host ports move to 20000-29999 the next time their containers are created, and the router's fallback ports move to 30000-32000. Ports you set with `host_*_port` don't change.
+- `--profiles` replaces `COMPOSE_PROFILES`, as in Docker Compose, so on a stopped project `ddev start --profiles=x` no longer starts the profiles in `COMPOSE_PROFILES` as well.
+- `ddev delete` exits with an error and keeps the project listed when it can't remove a volume. Before, it reported success.
+- `DDEV_DEBUG` no longer shows per-layer image download progress or routine start messages; [`DDEV_VERBOSE`](https://docs.ddev.com/en/stable/users/usage/troubleshooting/#debug-environment-variables) does.
+- The database buffer pool drops from 1024M to 512M, which can slow large imports, and each project's database volume grows by about 160-210M on the first start after upgrading. On MySQL 8.0, 8.4, and 9.7 and MariaDB 11 and later, the connection collation is now `utf8mb4_unicode_520_ci`, matching the server's.
+
+## Regressions, Fixed
+
+- Directory listing works again. The 403 explanation page in [v1.25.4](release-v1-25-4.md) had disabled it for every project.
+- `ddev composer create-project` again passes flags like `--no-dev` and `-vvv` to the Composer scripts it runs after creating the project, which v1.25.3 stopped doing.
+- Magento, Magento 2, and Maho show their own 404 pages again, instead of DDEV's explanation page, thanks to [@gunnie-agency](https://github.com/gunnie-agency).
+
+## Everything Else
+
+`ddev list`, `ddev describe`, and `ddev tui` respond faster, and the more projects you have running, the bigger the difference. nginx passes `PATH_INFO` to PHP, so URLs like `/index.php/foo/bar` work as they do with Apache, and running `ddev start` or `ddev poweroff` in several terminals at once no longer breaks the shared router. See the [release notes](https://github.com/ddev/ddev/releases/tag/v1.25.5) for the rest.
+
+From all of us, thanks for using, promoting, contributing, and supporting DDEV!
+
+If you have questions, reach out in any of the [support channels](https://docs.ddev.com/en/stable/users/support/).
+
+If you wonder how all this could be done, it's because of generous sponsors who let two of us work on this every day. If you and your team aren't already financially supporting DDEV, consider [joining our sponsors](/sponsor).
+
+Follow our [blog](https://ddev.com/blog/), [Bluesky](https://bsky.app/profile/ddev.bsky.social), [LinkedIn](https://www.linkedin.com/company/ddev-foundation), [Mastodon](https://fosstodon.org/@ddev), and join us on [Discord](/s/discord). Sign up for the [monthly newsletter](/newsletter).
+
+---
+
+_This article was edited and refined with assistance from Claude Code._