<?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: Piotr Hałas</title>
    <description>The latest articles on DEV Community by Piotr Hałas (@halaspiotr).</description>
    <link>https://dev.to/halaspiotr</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%2F3942856%2F148040b8-abc2-454c-9026-b2551a566126.jpg</url>
      <title>DEV Community: Piotr Hałas</title>
      <link>https://dev.to/halaspiotr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/halaspiotr"/>
    <language>en</language>
    <item>
      <title>Debugging PHP in Docker with an AI Agent and Xdebug</title>
      <dc:creator>Piotr Hałas</dc:creator>
      <pubDate>Wed, 09 Sep 2026 15:54:56 +0000</pubDate>
      <link>https://dev.to/halaspiotr/debugging-php-in-docker-with-an-ai-agent-and-xdebug-2f2k</link>
      <guid>https://dev.to/halaspiotr/debugging-php-in-docker-with-an-ai-agent-and-xdebug-2f2k</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqgb3udisy8tmsksk72xa.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqgb3udisy8tmsksk72xa.webp" alt="A cartoon developer slumped back in a chair with both arms raised in surrender beside a white flag reading I SURRENDER, while a small AI robot calmly sets a breakpoint in a wall of tangled code on the monitor. Caption: VIBE CODE NEEDS VIBE DEBUGGING" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For a while now I have used LLMs to automate parts of my work. One of those parts is debugging hard problems with Xdebug. Before, I set the breakpoints by hand and built complicated requests to see what happens inside. It usually took a few requests, and I had to move the breakpoints again and again. Then I found that PhpStorm has Xdebug in its MCP server. For simple GET requests, that is enough. But it fails on endpoints that need authorization, and on any method other than GET. So I did not wait for JetBrains. I wrote my own small MCP server, and it makes this work much faster. I share this tool with you, and I hope it helps you too — especially if you work with Docker.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tired of reading, and you have not started yet? Fair enough.&lt;/em&gt; Straight to &lt;a href="https://blog.crazy-goat.com/en/xdbg-mcp-server-xdebug-ai-agent/#the-simplest-path-phpstorm-mcp" rel="noopener noreferrer"&gt;setting up the MCP server in PhpStorm&lt;/a&gt;, or straight to &lt;a href="https://blog.crazy-goat.com/en/xdbg-mcp-server-xdebug-ai-agent/#installation" rel="noopener noreferrer"&gt;installing xdbg&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;AI agents are good at debugging from code alone — until the bug lives in runtime state. Then you need Xdebug, and every existing Xdebug tool is built for humans clicking through a GUI. PhpStorm ships an MCP server since 2025.2, but it is GET-only, has no headers, no path for CLI in Docker, and no control over Xdebug inside the container. So I built &lt;code&gt;xdbg&lt;/code&gt;: an MCP server that gives an agent the full loop — enable Xdebug, set a breakpoint, fire a real request, step, inspect, detach.&lt;/p&gt;

&lt;p&gt;MIT licence, one-line install, works with Claude Code, opencode, Cursor and any MCP-capable client. Source: &lt;a href="https://github.com/crazy-goat/xdbg" rel="noopener noreferrer"&gt;github.com/crazy-goat/xdbg&lt;/a&gt;&lt;br&gt;
Written against xdbg v0.1.1 (Go 1.26), Docker 29.7.2 / Compose 5.5.0, and any PHP 8 image with Xdebug 3. PhpStorm checked on 2025.2 and again on 2026.2.1.&lt;/p&gt;
&lt;h2&gt;
  
  
  The mighty duo: var_dump and die
&lt;/h2&gt;

&lt;p&gt;This is the standard. You need to check something fast, so you print the value and stop the script. It is quick, it always works, and it needs no setup.&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;function&lt;/span&gt; &lt;span class="n"&gt;add&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;$a&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;$b&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;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;die&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;It also has costs. You are editing the code in order to debug it, and you only see the one place you thought to print. Inside a loop, &lt;code&gt;var_dump&lt;/code&gt; fills the screen a thousand times and you scroll for the single line that matters. And &lt;code&gt;die()&lt;/code&gt; gives you exactly one breakpoint: it kills the process on the spot, so you cannot continue and see what came after it. That hurts most in an event loop, where the state you actually want often shows up on the second pass — and &lt;code&gt;die()&lt;/code&gt; never lets you get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analysis by an LLM
&lt;/h2&gt;

&lt;p&gt;Most bugs come straight from the code, and an LLM finds those much faster than any debugger. You paste the stack trace, you show the file, and you have an answer in seconds. But this method has a clear limit. With harder problems, with your own framework, or with anything the model has not seen before, there is a real chance the LLM will invent a problem instead of finding the one you have.&lt;/p&gt;

&lt;p&gt;For example, this one. &lt;code&gt;php-fpm&lt;/code&gt; kept dying with a segfault, but only on Colima. The LLM was sure the fault sat in the container runtime: first Colima, then Podman, then Rosetta. Every answer sounded reasonable, and every one of them was wrong. It changed its mind only after I asked it to write a test — and then it found that the bug was in PHP itself, and had been there for years. &lt;a href="https://blog.crazy-goat.com/en/colima-php-fpm-segfault-exit-139/" rel="noopener noreferrer"&gt;The story of that hunt is here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr67q617lgoh5o0q1omgt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr67q617lgoh5o0q1omgt.webp" alt="A GitHub diff of ext/opcache/shared_alloc_mmap.c, shown with the curved scanlines of an old CRT screen: create_segments() now reserves requested_size + huge_page_size before remapping with MAP_HUGETLB" width="800" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The two lines in question, in &lt;code&gt;ext/opcache/shared_alloc_mmap.c&lt;/code&gt;. OPcache now reserves &lt;code&gt;requested_size + huge_page_size&lt;/code&gt; before it remaps the block with &lt;code&gt;MAP_HUGETLB&lt;/code&gt;, so a kernel that fails halfway cannot leave a hole behind. Nothing an agent could have guessed from my application code. Click to open full size.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And a made-up answer looks exactly like a correct one, so you need a cheap way to tell the two apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging with tests
&lt;/h2&gt;

&lt;p&gt;That cheap way is a test. Ask the LLM to write one that proves the problem is real: if it was right, the test fails for exactly the reason it gave you, and if it made the problem up, the test passes and you have lost two minutes. Either way you now know, and you also keep the test. From my experience, a bug is very often a feature that somebody forgot about. A test written this way also writes that feature down, so the next person does not have to guess what the code was supposed to do. It costs a little more time than a quick print, and it gives that time back later.&lt;/p&gt;

&lt;p&gt;But some bugs are very hard to put into a test. The state that breaks them only exists in the running system — the real database, the real queue, the real timing, a service that answers differently today than it did yesterday. A test has to pretend all of that, and it pretends what you already believe. For bugs like these you have to look at the living thing, so you need something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual debugging in the IDE
&lt;/h2&gt;

&lt;p&gt;That something else is a real debugger. For a human the most comfortable one is an IDE like PhpStorm. You set the breakpoint by hand, you fire the request, and you walk through the code line by line, looking at the state as you go. It works, and for a long time it was the only way. But it has costs. You have to know where to put the breakpoint before you know what is wrong, so if you guess badly you start again. Every repeat means the same clicks: listen, fire the request, step, look. And one wrong choice — step into instead of step over — can cost you a lot of time, because now you are ten frames deep inside framework code you never wanted to see.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfxmuzfwx7398wgetv8o.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfxmuzfwx7398wgetv8o.webp" alt="A PhpStorm debug session stopped on a breakpoint at StreamConnection.php line 154, showing three stack frames and the $this object expanded with connected=false and socket=null" width="800" height="676"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A real session: stopped at &lt;code&gt;StreamConnection.php:154&lt;/code&gt;, three frames deep, with &lt;code&gt;$this&lt;/code&gt; open. Every part of this I had to ask for by hand — where the breakpoint goes, when to run, and each step after that. Click to open full size.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And here is the part that annoys me most, which is not PhpStorm’s fault at all: I forget to turn the debugger off. Then I run the tests, or a linter, or just one ordinary request, and the process stops somewhere for no visible reason. The classic version goes like this. I have tested the fix, it works, so I tell myself: full test suite and then I am done for the day. I start the suite and go to make a drink, because the suite takes a while. I come back, and nothing has moved — the tests are sitting on a breakpoint. Then I complain quietly to myself for forgetting again. I would like to blame my memory, and at my age that excuse comes more easily every year. There must be a better way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The simplest path: PhpStorm MCP
&lt;/h2&gt;

&lt;p&gt;And there is. The fastest path to automating Xdebug is PhpStorm — since version 2025.2 it ships with its own MCP server. Go to &lt;strong&gt;Settings → Tools → MCP Server&lt;/strong&gt;, click &lt;em&gt;Enable MCP Server&lt;/em&gt;, auto-configure the client — Claude Code, Cursor, opencode — and you are in the game. The plugin is installed by default, so there is nothing to fiddle with.&lt;/p&gt;

&lt;p&gt;A predefined set of Xdebug tools is ready out of the box: setting breakpoints (&lt;code&gt;xdebug_set_breakpoint&lt;/code&gt;), starting a PHP server (&lt;code&gt;xdebug_start_server&lt;/code&gt;), making URL requests (&lt;code&gt;xdebug_request&lt;/code&gt;), stepping through code (&lt;code&gt;xdebug_step_into&lt;/code&gt;, &lt;code&gt;xdebug_step_over&lt;/code&gt;, &lt;code&gt;xdebug_step_out&lt;/code&gt;), inspecting the stack and context. You start a debugging session without leaving the chat — like opening the fridge and discovering there is a warm dinner inside.&lt;/p&gt;

&lt;p&gt;The flow is different from manual debugging. Instead of clicking around yourself and deciding where to place a breakpoint, you write a prompt describing the problem and ask the agent to use Xdebug through PhpStorm to find the cause. The agent analyses the code, sets breakpoints on its own where it suspects the problem, then drives the session step by step: reads the stack, inspects variables, dives deeper into calls. If the runtime goes down a different path than expected, it moves the breakpoint, repeats the request, and analyses again. All by itself.&lt;/p&gt;

&lt;p&gt;For a start this is close to ideal. But in its current form it comes with a few limits, and they are not small print — they change how you work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where PhpStorm MCP stopped being enough
&lt;/h2&gt;

&lt;p&gt;None of what follows is a complaint about PhpStorm — it is a very good IDE and its MCP server does what it set out to do. It is a description of the shape of &lt;em&gt;our&lt;/em&gt; problem, which happens to sit outside that shape. I first hit this on PhpStorm 2025.2, and it is still the same on 2026.2.1 — four releases later.&lt;/p&gt;

&lt;p&gt;The biggest one is simple: PhpStorm has to be open, and it has to be open on the right project. The MCP server &lt;em&gt;is&lt;/em&gt; the IDE, so the debugger only exists while the window is running and pointing at the code you care about. Switch project, or close the IDE to free some memory, and the agent loses its tools in the middle of a session. It also means the agent shares the debugger with you: your breakpoints, your listening state, your run configurations.&lt;/p&gt;

&lt;p&gt;The next thing is not really a flaw at all: not everyone has PhpStorm. It is currently the default IDE for many PHP developers, but let us be honest — for people starting their careers the licence can be too expensive, and some simply prefer something else. For them this path is dead from the first minute — a party they cannot enter because they are not on the list.&lt;/p&gt;

&lt;p&gt;The rest is about what it does not reach — and almost all of it comes down to one thing: it works as if Docker were not in the picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No control over Xdebug inside the container.&lt;/strong&gt; PhpStorm MCP does not enable or disable Xdebug in the container — and by default we keep it disabled, because it slows everything down: requests, CLI, tests. Without automatic toggling it is easy to forget to turn it off, and then &lt;code&gt;phpunit&lt;/code&gt;, &lt;code&gt;phpstan&lt;/code&gt; or &lt;code&gt;composer install&lt;/code&gt; stop on breakpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host ↔︎ container paths.&lt;/strong&gt; The MCP surface works in host paths — hand it a container path like &lt;code&gt;/var/www/app/src/Kernel.php&lt;/code&gt; and it refuses outright with &lt;code&gt;File not found&lt;/code&gt;. That part is honest. The trap is what comes next: pass the host path and you get back &lt;code&gt;"Breakpoint created."&lt;/code&gt;, but whether that breakpoint ever &lt;em&gt;hits&lt;/em&gt; depends on path mappings configured on the IDE side, which the agent cannot see, verify or repair. It receives a confident success and then silence — and starts moving the breakpoint one line at a time trying to work out why nothing stops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No path to a command already running in a container.&lt;/strong&gt; Debugging &lt;code&gt;bin/console app:foo&lt;/code&gt; inside Docker is possible, but only through a run configuration someone created by hand in the GUI, backed by a docker-compose interpreter. The MCP surface cannot create one, and &lt;code&gt;xdebug_start_debugger_session&lt;/code&gt; takes only an existing configuration or a local &lt;code&gt;filePath&lt;/code&gt; + &lt;code&gt;line&lt;/code&gt; — there is no attach-to-running-process mode. So a worker or a cron job that is already executing is out of reach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GET only, no headers.&lt;/strong&gt; &lt;code&gt;xdebug_request&lt;/code&gt; supports only GET and does not allow setting headers. In real API work — POST/PUT/PATCH with JSON, a JWT in &lt;code&gt;Authorization&lt;/code&gt;, cookies, &lt;code&gt;Content-Type&lt;/code&gt; — that is simply not enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The port is held for the whole session.&lt;/strong&gt; PhpStorm MCP starts a session and keeps port 9003 occupied until you turn it off yourself. The agent often forgets, then runs tests — which hang on breakpoints because Xdebug keeps stopping the runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflicts on port 9003.&lt;/strong&gt; When the port is occupied you have to manually work out what is listening and which tool is holding the connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No good documentation.&lt;/strong&gt; The official docs list the functions but do not describe the process — how to combine them into a working flow. The LLM knows what tools exist, but has to figure out by itself how to use them in practice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If not PhpStorm, then what?
&lt;/h2&gt;

&lt;p&gt;Two MCP servers for Xdebug already existed, and it is worth knowing what they do. &lt;a href="https://github.com/koriym/xdebug-mcp" rel="noopener noreferrer"&gt;koriym/xdebug-mcp&lt;/a&gt; is trace-first — forward traces, profiling, coverage — and reaches containers by wrapping a command you hand it: &lt;code&gt;xstep --break=… -- docker compose run --rm php …&lt;/code&gt;. Its code excludes &lt;code&gt;php-fpm&lt;/code&gt; and &lt;code&gt;php-cgi&lt;/code&gt; on purpose: CLI binaries only. &lt;a href="https://github.com/kpanuragh/xdebug-mcp" rel="noopener noreferrer"&gt;kpanuragh/xdebug-mcp&lt;/a&gt; is closer to interactive debugging — breakpoints, stepping, watches — with proper container path translation configured through a static &lt;code&gt;PATH_MAPPINGS&lt;/code&gt; map.&lt;/p&gt;

&lt;p&gt;Both of them miss the part I needed. Neither turns Xdebug on and off inside a running container, so you leave it enabled and then wonder why &lt;code&gt;phpunit&lt;/code&gt; hangs. Neither fires the request either — you still bring your own trigger, and for a POST with a 1600-byte bearer token that trigger is most of the problem. So with either one, there is still no way to debug an ordinary web request going through php-fpm in Docker, which is what I do most days.&lt;/p&gt;

&lt;p&gt;The older tools do not close it either. &lt;code&gt;phpdbg&lt;/code&gt;, PHP’s built-in CLI debugger; &lt;code&gt;debugclient&lt;/code&gt;, the DBGp client shipped with Xdebug; editor integrations such as VS Code PHP Debug — all of them are designed more for humans than for AI. Each works interactively: typing commands, clicking through a GUI, managing the session by hand. Great for debugging by a human, terrible as an interface for an agent that needs a programmable, automated API.&lt;/p&gt;

&lt;p&gt;So I had no choice but to vibe-code my own tool. I had a working prototype after 10 minutes and a usable tool within an hour, using only open-source models available through opencode — which significantly reduced the cost. Funnily enough, writing this article took me longer than preparing &lt;code&gt;xdbg&lt;/code&gt; together with the repo and docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tool actually needed
&lt;/h2&gt;

&lt;p&gt;At first I only wanted &lt;code&gt;xdbg_request&lt;/code&gt;, because that was what I needed most, with minimal requirements: GET/POST/PUT/… and passing headers. Coding went smoothly. But it quickly turned out that firing requests is only half the job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The other half is knowing whether Xdebug is even enabled.&lt;/strong&gt; Hence &lt;code&gt;xdbg_container_status&lt;/code&gt;, &lt;code&gt;xdbg_container_enable&lt;/code&gt;, &lt;code&gt;xdbg_container_disable&lt;/code&gt;. Enabled Xdebug reduces performance and can block calls such as &lt;code&gt;phpunit&lt;/code&gt;. On top of that, the agent would often set a breakpoint, start debugging, and execution never paused — so it would rack its brain over what happened, burning through mountains of tokens on guesswork.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then came the authorization problem.&lt;/strong&gt; Debugging very often ended with a 401. First thought: the token expired. I generated another one, it seemed to work, and a moment later there was another 401. It took me a while to realise that our token is around 1600 bytes long and the LLM simply slips when copying it into an MCP function. The same happened when a longer JSON body had to be prepared for a POST.&lt;/p&gt;

&lt;p&gt;That is when I came up with passing the body and headers through files. A long token can be prepared in a file, and the JSON can be safely validated with something like &lt;code&gt;jq&lt;/code&gt; before sending. That is how &lt;code&gt;xdbg_request_from_files&lt;/code&gt; was born — and most of my request problems magically disappeared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next: running commands.&lt;/strong&gt; Very often, to confirm something, it is faster to prepare a simple PHP or Symfony command on the side containing only the minimal code that actually reproduces the problem — instead of entering the whole application, placing breakpoints in five places and praying that the flow reaches the right one. The command has to run inside a Docker container, which created another challenge. With &lt;code&gt;xdbg_run_command&lt;/code&gt; this became ridiculously simple: the agent enters the container by itself, runs the command, takes over the session, and you watch it play out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finally: manual triggers.&lt;/strong&gt; Sometimes it is simply easier to trigger something by hand — when a command is started by a scheduler, or when a request comes from the outside through a webhook that is not easy to reproduce from the agent level. Every attempt to force the agent to create such a command ended with wonderful constructions like &lt;code&gt;curl -X POST&lt;/code&gt; with three escaped brackets and a pipe to &lt;code&gt;php -r&lt;/code&gt;. That is where &lt;code&gt;xdbg_listen&lt;/code&gt; came from: the agent arms the listener, and you just snap your fingers and trigger whatever needs triggering.&lt;/p&gt;

&lt;p&gt;In total there are more than twenty &lt;code&gt;xdbg_*&lt;/code&gt; functions, and for agents that was a problem in itself — they often mixed up the call order, used &lt;code&gt;xdbg_listen&lt;/code&gt; instead of &lt;code&gt;xdbg_request&lt;/code&gt;, and got lost in the debugging flow like a customer in IKEA without a map. To make life easier for LLMs I added a dedicated skill that shows how to debug and which function is for what: an instruction manual you read once and then know which screw goes where.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Instead of reading ten pages of documentation and typing flags manually, paste one line into a chat with Claude Code or opencode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Install and configure this MCP server for me:
https://raw.githubusercontent.com/crazy-goat/xdbg/main/install.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that is it. The agent fetches the guide, reads it, installs via &lt;code&gt;go install&lt;/code&gt;, adds entries to the MCP configuration, translates host paths into container paths, adjusts flags for your project — and asks for permission before changing anything. Like a good assembly crew: they come in, measure, cut, set things up, and you only point to where the fridge stands.&lt;/p&gt;

&lt;p&gt;On top of that, &lt;code&gt;xdbg&lt;/code&gt; provides a dedicated AI skill (&lt;code&gt;skills/xdbg/SKILL.md&lt;/code&gt;) with context about debugging flows, error recovery and good practices. The skill is installed separately, following the rule that the agent asks before installing — but thanks to it the agent knows which functions to use and in what order, instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;You are probably wondering what this kind of agent work looks like in real life — whether you can really debug without touching the IDE, or whether this is just a marketing pitch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1sa7fui4jqukbnv8k32v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1sa7fui4jqukbnv8k32v.png" alt="xdbg architecture: an AI agent talks to the xdbg Go binary over MCP stdio; xdbg opens a DBGp listener on port 9003, fires HTTP requests or docker exec into the container, and the Xdebug engine dials back to host.docker.internal:9003" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Click to open full size. Solid arrows stay inside one side; dashed ones cross the host/container boundary.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is the shape of it. The agent never talks to Xdebug directly — &lt;code&gt;xdbg&lt;/code&gt; sits in the middle, holds the DBGp listener on port 9003, and translates between the MCP calls the agent makes and the debugger protocol the container speaks. Path translation happens in the same place, which is what stops the agent from setting breakpoints on host paths that the container has never heard of.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.crazy-goat.com/wp-content/uploads/2026/08/xdbg-demo.mp4" rel="noopener noreferrer"&gt;Watch the demo (MP4, 45s)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The agent is asked whether the &lt;code&gt;add&lt;/code&gt; endpoint writes anything to stderr. It enables Xdebug, breaks in &lt;code&gt;Calculator::add&lt;/code&gt;, fires the request, and steps until it finds the culprit — 45 seconds, one chat.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A Slim app with a simple &lt;code&gt;GET /calc/add/{a}/{b}&lt;/code&gt; endpoint. For larger results, a log suddenly appears in stderr, even though nobody asked for it to be written there. The agent enables Xdebug inside the container, sets a breakpoint in &lt;code&gt;Calculator::add&lt;/code&gt;, fires the request — and then drives the session step by step: reads the stack, inspects variables, dives into calls, evaluates expressions, and finds &lt;code&gt;logLargeResult&lt;/code&gt; writing to &lt;code&gt;php://stderr&lt;/code&gt; on its own. Everything in one chat, without switching tools, without the usual Postman-and-IDE loop.&lt;/p&gt;

&lt;p&gt;The pattern is always the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enable Xdebug → breakpoint → request or command → step → detach → disable Xdebug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent takes care of cleanup by itself — it knows to close the session and free port 9003 before running tests. For HTTP, CLI and manual launch the flow is identical; only the trigger changes.&lt;/p&gt;

&lt;p&gt;That flow leaves out one thing worth knowing. While the code is stopped, the agent can also &lt;em&gt;change&lt;/em&gt; values, not only read them. Set a variable to whatever the failing case would produce, then continue — and you get to watch the unhappy path without building it first. A service that is healthy today but returns an error on Fridays, a payment that comes back declined, an empty result from a query that always finds rows on your machine. By hand this means clicking through the IDE and doing it again on every single run. The agent does it in one step, and it remembers what it already tried.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;xdbg&lt;/code&gt; does not try to replace PhpStorm. It tries to pull debugging out of the IDE and put it where it fits the present: into a chat with AI. Between tracing where an exception escaped and discovering where it came from there is an entire afternoon of manual work — and &lt;code&gt;xdbg&lt;/code&gt; turns that into a few prompts.&lt;/p&gt;

&lt;p&gt;It is a small project on purpose. It solves one problem — driving Xdebug inside a container from an agent — and it has no ambition to grow past that. If JetBrains ships this properly in PhpStorm, or if one of the other MCP servers grows the container side, I will stop developing it and use theirs. A tool that exists because of a gap should disappear when the gap does.&lt;/p&gt;

&lt;p&gt;MIT licence, Go, one-line installation. Worth trying and judging for yourself: &lt;a href="https://github.com/crazy-goat/xdbg" rel="noopener noreferrer"&gt;github.com/crazy-goat/xdbg&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>php</category>
      <category>xdebug</category>
      <category>docker</category>
      <category>ai</category>
    </item>
    <item>
      <title>I spent a full day on a “five-minute” Docker migration</title>
      <dc:creator>Piotr Hałas</dc:creator>
      <pubDate>Sat, 29 Aug 2026 19:03:41 +0000</pubDate>
      <link>https://dev.to/halaspiotr/i-spent-a-full-day-on-a-five-minute-docker-migration-2i60</link>
      <guid>https://dev.to/halaspiotr/i-spent-a-full-day-on-a-five-minute-docker-migration-2i60</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ukp6gwh5zs53ya2nd2c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ukp6gwh5zs53ya2nd2c.jpg" alt="A developer watches Docker containers drift out to sea while a giant llama-shaped cloud (Colima) looms in the sky and a lighthouse flashes SEGFAULT" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We ditched Docker Desktop for Colima — and then PHP started segfaulting. A story about a simple migration that was not simple. On the way: a free tool that was not really free, a segfault with no logs, and a kernel that breaks its own contract. It ends deep inside the Linux syscall table — with a fix you can copy in five minutes.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;TL;DR&lt;/p&gt;

&lt;p&gt;&lt;code&gt;php-fpm&lt;/code&gt; exits &lt;strong&gt;139&lt;/strong&gt; (SIGSEGV) on Colima — silently, about a second after start, while the PHP CLI stays perfectly fine. OPcache asks the kernel for huge pages, the kernel half-performs the mapping before failing, and Rosetta falls into the hole it leaves behind. Jump straight to the fix →&lt;/p&gt;

&lt;p&gt;Easiest fix: &lt;code&gt;opcache.preferred_memory_model=shm&lt;/code&gt; — one ini line, works on both Colima and Podman. Running many amd64 containers on Colima? The seccomp profile below fixes it machine-wide instead, so you don’t have to touch every image’s config.&lt;/p&gt;

&lt;p&gt;Tested on Colima 0.10.3 (&lt;code&gt;vmType: vz&lt;/code&gt;, &lt;code&gt;rosetta: true&lt;/code&gt;), macOS 26.6.2 on Apple Silicon, stock amd64 PHP 8.2 / 8.3 / 8.4 images. Running native arm64, or left &lt;code&gt;rosetta: false&lt;/code&gt;? This bug does not touch you — which is exactly why a colleague on the same team “does not have it”.&lt;/p&gt;

&lt;h2&gt;
  
  
  The invoice that never came
&lt;/h2&gt;

&lt;p&gt;It all started with a company decision. Docker Desktop is free for small companies. But when a company grows big enough, it must pay for every developer who uses it. Our company was getting close to that line. Nobody had paid anything yet — the invoice was still in the future. But it was clear that the invoice was coming, and that it would grow with every new hire.&lt;/p&gt;

&lt;p&gt;So the decision was made early, before the first payment: we move to something free. It is much easier to migrate when you are not in a hurry. And honestly, the tool we would pay for is just a nice wrapper around free software. Paying for it felt strange.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free for me, but I am a team player
&lt;/h2&gt;

&lt;p&gt;Here is the funny part. This decision was not really about me. I work as a B2B contractor. My own little company is one person: me. By Docker’s own rules, a company this small can use Docker Desktop for free. Legally, I could keep it forever and pay nothing. Nobody would come after me.&lt;/p&gt;

&lt;p&gt;But I did not want to be the special one. When one person in the team runs a different setup, small problems start. And this time, the problems would be mine, not theirs. The README would stop being true &lt;em&gt;for me&lt;/em&gt;. New scripts would break on &lt;em&gt;my&lt;/em&gt; machine only. Every little change in the stack would cost me an hour of debugging, alone. I did not want to be that person. So I decided to migrate like everyone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Podman: close, but no socket
&lt;/h2&gt;

&lt;p&gt;The first idea was Podman. It is free, it is popular, and people say it is a drop-in replacement for Docker. So we tried it. It was not a drop-in replacement for us. Not even close.&lt;/p&gt;

&lt;p&gt;The first problem was our SSH agent. Our &lt;code&gt;docker-compose.yml&lt;/code&gt; mounts a special socket file into the PHP container: &lt;code&gt;/run/host-services/ssh-auth.sock&lt;/code&gt;. Thanks to this socket, &lt;code&gt;composer install&lt;/code&gt; inside the container can talk to our private GitLab over SSH. Here is the catch: this path is not a Docker feature. It is a &lt;strong&gt;Docker Desktop&lt;/strong&gt; feature. Podman does not have it. Our composer.json has more than ten private repositories, so without the agent, nothing installs.&lt;/p&gt;

&lt;p&gt;The second problem was &lt;code&gt;host.docker.internal&lt;/code&gt;. Our PHP image has &lt;code&gt;xdebug.client_host=host.docker.internal&lt;/code&gt; inside its config. Xdebug connects &lt;em&gt;from&lt;/em&gt; the container &lt;em&gt;to&lt;/em&gt; the host, where the debugger listens. With Podman, this name does not exist. Podman has its own name, &lt;code&gt;host.containers.internal&lt;/code&gt;. The result: no debugging at all. Not in the browser, not for CLI commands. No error. Just silence.&lt;/p&gt;

&lt;p&gt;The third problem was our Git worktrees. Every worktree has its own &lt;code&gt;docker-compose.override.yml&lt;/code&gt;. These files use modern Compose features: top-level &lt;code&gt;name:&lt;/code&gt; and the &lt;code&gt;!override&lt;/code&gt; tag. The Python tool &lt;code&gt;podman-compose&lt;/code&gt; cannot parse these files at all. It just crashes with a YAML error.&lt;/p&gt;

&lt;p&gt;And there were small cuts too. For example, our lint script checks if the file &lt;code&gt;/.dockerenv&lt;/code&gt; exists, to know if it runs inside a container. Podman does not create this file. It creates &lt;code&gt;/run/.containerenv&lt;/code&gt; instead. Small thing. Easy to fix. But it was one more fix on the list.&lt;/p&gt;

&lt;p&gt;Every single problem had a solution. But together they meant one thing: we would have to change shared files, shared scripts, and the habits of every developer. That is exactly the cost we wanted to avoid. So we looked somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Colima: boring, and that is the point
&lt;/h2&gt;

&lt;p&gt;Then we looked at &lt;a href="https://github.com/abiosoft/colima" rel="noopener noreferrer"&gt;Colima&lt;/a&gt;. Colima is different from Podman in one important way. Podman is its own engine that tries to act like Docker. Colima runs the &lt;strong&gt;real Docker engine&lt;/strong&gt; inside a small Linux VM. It is not a reimplementation. It is the same &lt;code&gt;dockerd&lt;/code&gt; that Docker Desktop runs.&lt;/p&gt;

&lt;p&gt;This changed everything. The file &lt;code&gt;/.dockerenv&lt;/code&gt; exists again, because real Docker creates it. The name &lt;code&gt;host.docker.internal&lt;/code&gt; works out of the box, because Colima maps it by default. Modern Compose features work, because we use the real Compose with a real Docker socket. And best of all: Colima &lt;strong&gt;pretends to be Docker Desktop&lt;/strong&gt; for the SSH agent. It creates the same socket path, &lt;code&gt;/run/host-services/ssh-auth.sock&lt;/code&gt;, on purpose, for compatibility. You only need one setting: &lt;code&gt;forwardAgent: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the migration suddenly became boring. Uninstall Docker Desktop. Run &lt;code&gt;colima start&lt;/code&gt;. Done. No changes in the repository. No changes in scripts. This is what we wanted. Well — almost. Keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Need for speed (enter Rosetta)
&lt;/h2&gt;

&lt;p&gt;There was one more problem. Our main PHP image exists only for &lt;strong&gt;amd64&lt;/strong&gt;. Our Macs have Apple Silicon chips. So the image must be translated. Colima’s default settings are safe but slow. The VM itself is fine — Apple’s own virtualization and fast file sharing are on by default. But Rosetta is not, so every amd64 binary crawls through QEMU’s emulation. The first boot was painful. Simple page loads took seconds. Sometimes more.&lt;/p&gt;

&lt;p&gt;The fix was a few lines in the Colima profile config (&lt;code&gt;~/.colima/default/colima.yaml&lt;/code&gt;):&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;vmType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vz&lt;/span&gt; &lt;span class="c1"&gt;# Apple's Virtualization.framework (the default — keep it explicit)&lt;/span&gt;
&lt;span class="na"&gt;mountType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;virtiofs&lt;/span&gt; &lt;span class="c1"&gt;# fast file sharing (also the default)&lt;/span&gt;
&lt;span class="na"&gt;rosetta&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# the real fix: translate amd64 with Rosetta 2, not QEMU emulation&lt;/span&gt;
&lt;span class="na"&gt;forwardAgent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# the SSH agent socket from chapter 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Rosetta on, the amd64 PHP-FPM container ran almost as fast as a native one. The worktree stacks came up. The tests ran. For a moment, the migration looked finished.&lt;/p&gt;

&lt;p&gt;It was not finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  The silent killer
&lt;/h2&gt;

&lt;p&gt;After some real use, &lt;code&gt;php-fpm&lt;/code&gt; started to crash. The container would start, live for one or two seconds, and die. The exit code was &lt;strong&gt;139&lt;/strong&gt; , which means SIGSEGV. There was no error message. No log line. No core dump. Just a dead container.&lt;/p&gt;

&lt;p&gt;The strange part was this: some things worked, and some did not. The &lt;a href="https://blog.crazy-goat.com/en/methods-of-running-php-scripts-part-1-php-server/" rel="noopener noreferrer"&gt;PHP CLI&lt;/a&gt; was fine: &lt;code&gt;php -v&lt;/code&gt;, &lt;code&gt;php -m&lt;/code&gt;, even our full unit test suite — over six thousand tests, all green. Only the FPM master process died. Always a second after start. Always silent.&lt;/p&gt;

&lt;p&gt;The asymmetry has a boring explanation, and it is worth knowing because it sends you looking in the wrong place for an hour: &lt;code&gt;opcache.enable_cli&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt; by default. The CLI never builds the shared memory segment at all, so it never touches the thing that kills FPM. Six thousand green tests told me nothing.&lt;/p&gt;

&lt;p&gt;We made a small reproduction, to be sure it was not our app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 OUR_PHP_IMAGE php-fpm &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; exit code 139. Silence.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A clean image, one config test command, one crash. So the problem was below our code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same engine. Same Mac. Different crash.
&lt;/h2&gt;

&lt;p&gt;Now here is the thing that made no sense. Docker Desktop and Colima both run the real dockerd. Both translate amd64 through the same Rosetta 2 on the same Mac. &lt;strong&gt;The exact same image worked on Docker Desktop for months.&lt;/strong&gt; No crashes. Ever.&lt;/p&gt;

&lt;p&gt;So if the engine is the same, and the translator is the same, then the difference must be somewhere lower. In the VM. Or in the way Rosetta is connected to the VM. This was the clue that later solved everything. But we did not know that yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detective work
&lt;/h2&gt;

&lt;p&gt;Time for the oldest debugging method: turn things off, one by one, until the problem goes away. It was the only method available, by the way — &lt;code&gt;strace&lt;/code&gt; does not work under Rosetta, so there was no syscall trace to read. That is a large part of why this took a day. PHP makes bisection easy, though:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php-fpm &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="c"&gt;# no php.ini at all =&amp;gt; OK&lt;/span&gt;
php-fpm &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; opcache.enable&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="c"&gt;# all, but OPcache off =&amp;gt; OK&lt;/span&gt;
php-fpm &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="c"&gt;# full config =&amp;gt; 139&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the problem was OPcache. But not the JIT part — our image already has &lt;code&gt;opcache.jit=disable&lt;/code&gt;. The crash happens when OPcache creates its &lt;strong&gt;shared memory segment&lt;/strong&gt;. OPcache maps this memory as &lt;em&gt;executable&lt;/em&gt;, using the &lt;code&gt;mmap&lt;/code&gt; syscall.&lt;/p&gt;

&lt;p&gt;And then came the first real clue. We forced OPcache to use &lt;strong&gt;System V shared memory&lt;/strong&gt; (&lt;code&gt;shmget&lt;/code&gt;) instead of &lt;code&gt;mmap&lt;/code&gt;. The crash was gone. Even on the completely stock PHP image, with no other changes. Same PHP code. Same Rosetta. Different kernel mechanism. So the problem was not “executable memory under Rosetta” in general. It was something specific about that one &lt;code&gt;mmap&lt;/code&gt; call.&lt;/p&gt;

&lt;h2&gt;
  
  
  It worked on Docker Desktop. It &lt;em&gt;should&lt;/em&gt; work here
&lt;/h2&gt;

&lt;p&gt;At this point, I had a working workaround. I could stop. Ship the SHM change, go home, be happy.&lt;/p&gt;

&lt;p&gt;But it bothered me. Docker Desktop runs the same Rosetta 2 on the same Mac, and it never crashed. Bugs in emulation are not random. When a low-level feature misbehaves, it is usually not an accident. Usually someone made a trade-off on purpose. Often for speed. I wanted to know who, and where, and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The monster had a name: MAP_HUGETLB
&lt;/h2&gt;

&lt;p&gt;The answer was inside PHP itself — in OPcache’s shared memory code. When OPcache starts, it asks the kernel for a block of memory. If the size happens to be a multiple of 2 MB, it tries something special: an &lt;code&gt;mmap&lt;/code&gt; call with the flags &lt;code&gt;MAP_32BIT | MAP_HUGETLB | MAP_FIXED&lt;/code&gt;. It wants “huge pages” — bigger memory pages that are faster for the CPU. If the kernel says no, OPcache falls back to normal memory and everything is fine. This fallback is the key to the whole story.&lt;/p&gt;

&lt;p&gt;On our Colima VM, the Linux kernel is built with huge page support (&lt;code&gt;CONFIG_HUGETLBFS=y&lt;/code&gt;). So the kernel does &lt;strong&gt;not&lt;/strong&gt; say no right away. It starts the operation, removes the old memory at that address — and only then fails, returning &lt;code&gt;ENOMEM&lt;/code&gt;. A correct kernel never does this: a failed &lt;code&gt;mmap&lt;/code&gt; must leave the old memory untouched. Now the process has a hole in its memory map. Under Rosetta, that hole is not empty — translated code was living there. The process touches the hole, and it dies. SIGSEGV, one second after start, no logs. This exact non-atomic &lt;code&gt;mmap&lt;/code&gt; behaviour is described in the Colima issue we eventually found: &lt;a href="https://github.com/abiosoft/colima/issues/1452" rel="noopener noreferrer"&gt;abiosoft/colima#1452&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And why did Docker Desktop never crash? Here I have to be honest about what I verified and what I did not. I confirmed the Colima side directly — the crash, the flag, the fallback, the fix. For Docker Desktop I only observed that the same image never crashed in months of use; the explanation that its LinuxKit kernel rejects &lt;code&gt;MAP_HUGETLB&lt;/code&gt; early, before touching the existing mapping, is my inference, not something I traced. It fits every observation I have, but treat it as the likely story rather than a proven one.&lt;/p&gt;

&lt;p&gt;Same engine. Same Rosetta. Different kernel behaviour for one single flag.&lt;/p&gt;

&lt;p&gt;One more data point that supports the mechanism: the crash depends on the &lt;em&gt;size&lt;/em&gt; of the segment, not on huge pages being available. OPcache only attempts the huge-page mapping when the segment size is a multiple of 2 MB, so &lt;code&gt;opcache.memory_consumption=65&lt;/code&gt; sidesteps the whole thing by never asking. And enabling huge pages properly inside the VM does not fix it either — then &lt;code&gt;php-fpm&lt;/code&gt; hangs instead of crashing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two rules to fix it all
&lt;/h2&gt;

&lt;p&gt;Once you see it, the fix is elegant. We cannot rebuild the VM kernel. But we can make it answer “no” to huge-page &lt;code&gt;mmap&lt;/code&gt; calls — the same clean “no” that Docker Desktop gives. Docker has a feature exactly for this: a &lt;strong&gt;seccomp profile&lt;/strong&gt; , a list of rules saying which syscalls are allowed inside containers, and how.&lt;/p&gt;

&lt;p&gt;So we took Docker’s default profile and put two rules on top of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if &lt;code&gt;mmap&lt;/code&gt; is called &lt;strong&gt;with&lt;/strong&gt; the huge-page flag (&lt;code&gt;flags &amp;amp; 0x40000&lt;/code&gt;) → return &lt;code&gt;EPERM&lt;/code&gt; immediately,&lt;/li&gt;
&lt;li&gt;if &lt;code&gt;mmap&lt;/code&gt; is called without it → allow, as usual.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here they are, ready to paste:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"names"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mmap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mmap2"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SCMP_ACT_ERRNO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errnoRet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"colima#1452: MAP_HUGETLB -&amp;gt; EPERM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;262144&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"valueTwo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;262144&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SCMP_CMP_MASKED_EQ"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"names"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mmap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mmap2"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SCMP_ACT_ALLOW"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;262144&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"valueTwo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SCMP_CMP_MASKED_EQ"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things are worth decoding here, because the OCI schema is not obvious. &lt;code&gt;index: 3&lt;/code&gt; is the fourth argument of &lt;code&gt;mmap&lt;/code&gt;, which is &lt;code&gt;flags&lt;/code&gt;. And for &lt;code&gt;SCMP_CMP_MASKED_EQ&lt;/code&gt; the OCI spec uses &lt;code&gt;value&lt;/code&gt; as the &lt;strong&gt;mask&lt;/strong&gt; and &lt;code&gt;valueTwo&lt;/code&gt; as the &lt;strong&gt;expected result&lt;/strong&gt; — so the first rule reads “mask the flags with &lt;code&gt;0x40000&lt;/code&gt;; if what remains equals &lt;code&gt;0x40000&lt;/code&gt;, fail”, and the second reads “if it equals &lt;code&gt;0&lt;/code&gt;, allow”. &lt;code&gt;262144&lt;/code&gt; is &lt;code&gt;0x40000&lt;/code&gt; is &lt;code&gt;MAP_HUGETLB&lt;/code&gt;. Rules are matched in order, first match wins, which is why the &lt;code&gt;ERRNO&lt;/code&gt; rule has to come first.&lt;/p&gt;

&lt;p&gt;Building the whole file is two commands — grab Docker’s default profile and prepend the rules:&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;-sL&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; ~/.colima/seccomp-no-hugetlb.json &lt;span class="se"&gt;\&lt;/span&gt;
  https://raw.githubusercontent.com/moby/moby/master/profiles/seccomp/default.json

jq &lt;span class="s1"&gt;'.syscalls = ([
  {"names":["mmap","mmap2"],"action":"SCMP_ACT_ERRNO","errnoRet":1,
   "args":[{"index":3,"value":262144,"valueTwo":262144,"op":"SCMP_CMP_MASKED_EQ"}]},
  {"names":["mmap","mmap2"],"action":"SCMP_ACT_ALLOW",
   "args":[{"index":3,"value":262144,"valueTwo":0,"op":"SCMP_CMP_MASKED_EQ"}]}
] + .syscalls)'&lt;/span&gt; ~/.colima/seccomp-no-hugetlb.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/p.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; /tmp/p.json ~/.colima/seccomp-no-hugetlb.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then tell Colima to use it as the default profile for every container. Two things in &lt;code&gt;~/.colima/default/colima.yaml&lt;/code&gt;:&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;docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;seccomp-profile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/docker/seccomp-no-hugetlb.json&lt;/span&gt;
&lt;span class="na"&gt;provision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
    &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;cp /Users/&amp;lt;your-username&amp;gt;/.colima/seccomp-no-hugetlb.json /etc/docker/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That path has to be spelled out in full. The &lt;code&gt;provision&lt;/code&gt; script runs as root &lt;strong&gt;inside&lt;/strong&gt; the VM, where &lt;code&gt;$HOME&lt;/code&gt; is &lt;code&gt;/root&lt;/code&gt; — but your macOS home directory is mounted at the same path it has on the host, so &lt;code&gt;/Users/you/...&lt;/code&gt; is what resolves. Substitute your own username.&lt;/p&gt;

&lt;p&gt;One &lt;code&gt;colima restart&lt;/code&gt; later, check it with the same command that started the whole investigation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 YOUR_PHP_IMAGE php-fpm &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; "configuration file /usr/local/etc/php-fpm.conf test is successful", exit 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything works on the &lt;strong&gt;stock image&lt;/strong&gt;. OPcache on. Default memory model. No ini files, no per-repo overrides. And because the fix lives in the VM and not in the repository, every new Git worktree works out of the box — no more silent crashes for the next person on the team.&lt;/p&gt;

&lt;p&gt;One caveat before you copy this: the profile is the daemon default, so it applies to &lt;strong&gt;every container on that machine&lt;/strong&gt; , not just PHP. Anything that genuinely wants huge pages — a database tuned for them, a JVM started with &lt;code&gt;-XX:+UseLargePages&lt;/code&gt; — will now get a silent &lt;code&gt;EPERM&lt;/code&gt; and quietly fall back. For a development machine that is a trade I will take without thinking. For anything resembling production, do not paste this without knowing what else runs there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes for the next traveller
&lt;/h2&gt;

&lt;p&gt;Most migration stories end with “and then it worked”. This one ended inside the syscall table. Someone else will hit this: they will see a silent &lt;code&gt;exit 139&lt;/code&gt; and they will have no idea where to start. So I left a comment with the root cause and the seccomp workaround in the GitHub thread that describes this bug: &lt;a href="https://github.com/abiosoft/colima/issues/1452" rel="noopener noreferrer"&gt;abiosoft/colima#1452&lt;/a&gt;. Maybe it saves someone a Friday afternoon.&lt;/p&gt;

&lt;p&gt;If you just want the crash to stop and you do not care why, there are three smaller escape hatches, in rising order of how much they cost you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;opcache.memory_consumption=65&lt;/code&gt; — not a multiple of 2 MB, so OPcache never asks for huge pages. One line, keeps OPcache and the default memory model.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;opcache.preferred_memory_model=shm&lt;/code&gt; — switches to System V shared memory and avoids the &lt;code&gt;mmap&lt;/code&gt; path entirely. This is the fallback I would use if the seccomp profile ever breaks.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rosetta: false&lt;/code&gt; — back to QEMU. Nothing crashes, and everything is slow again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What does &lt;em&gt;not&lt;/em&gt; help, in case you are about to try: JIT settings, &lt;code&gt;vm.overcommit_memory&lt;/code&gt;, ASLR, or enabling huge pages inside the VM. That last one is a trap — &lt;code&gt;php-fpm&lt;/code&gt; stops crashing and starts hanging instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was it worth it?
&lt;/h2&gt;

&lt;p&gt;Honest answer: I do not know. The migration took me a full day, not the calm afternoon I hoped for. Maybe fixing our scripts for Podman would have been faster — those problems are annoying, but at least you can see them. A silent segfault inside a VM is a different kind of enemy. And the big question is still open: as a team, we have not decided yet. At first, Colima looked like the option with fewer problems. Then PHP started dying, and the fix was a custom seccomp profile. Both roads have holes. You only choose which holes you prefer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note, added later:&lt;/strong&gt; I said “maybe Podman would have been faster.” I was wrong. I got curious and tested it myself. Podman does not use Rosetta by default — it is off, and being removed step by step. Without Rosetta, Podman uses QEMU instead, and that crashes too — but earlier and differently, with &lt;code&gt;qemu-x86_64-static: QEMU internal SIGSEGV&lt;/code&gt;, before PHP even runs. This is a known, reported bug (&lt;a href="https://github.com/containers/podman/discussions/22714" rel="noopener noreferrer"&gt;containers/podman#22714&lt;/a&gt;), not something wrong with my machine. When I turned Rosetta on by hand, PHP did start, but then hit the same kind of crash as Colima — exit 139, same signal. My seccomp fix did not stop it either — this crash comes from a different syscall (&lt;code&gt;munmap&lt;/code&gt;, not &lt;code&gt;mmap&lt;/code&gt; with huge pages), later, when PHP shuts down. So Podman was not the easier road after all. Both of its emulation paths — QEMU and Rosetta — have their own version of this problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimal repro, both platforms:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Colima (needs &lt;code&gt;rosetta: true&lt;/code&gt; in &lt;code&gt;colima.yaml&lt;/code&gt;, see above):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;colima start
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--platform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux/amd64 php:8.3-cli-bookworm &lt;span class="se"&gt;\&lt;/span&gt;
  php &lt;span class="nt"&gt;-d&lt;/span&gt; opcache.enable_cli&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-d&lt;/span&gt; opcache.enable&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-d&lt;/span&gt; opcache.memory_consumption&lt;span class="o"&gt;=&lt;/span&gt;128 &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'echo "ok\n";'&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; exit 139, after printing "ok"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Podman (Rosetta is off by default, you must turn it on by hand):&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/containers
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.config/containers/containers.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[machine]
provider = "applehv"
rosetta = true
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;podman machine init &lt;span class="nt"&gt;--now&lt;/span&gt;

podman run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--platform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux/amd64 php:8.3-cli-bookworm &lt;span class="se"&gt;\&lt;/span&gt;
  php &lt;span class="nt"&gt;-d&lt;/span&gt; opcache.enable_cli&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-d&lt;/span&gt; opcache.enable&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-d&lt;/span&gt; opcache.memory_consumption&lt;span class="o"&gt;=&lt;/span&gt;128 &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'echo "ok\n";'&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; exit 139, after printing "ok"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same flag (&lt;code&gt;opcache.memory_consumption=128&lt;/code&gt;, a multiple of 2 MB). Same result. Two different paths to the same family of bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One more thing:&lt;/strong&gt; I tested &lt;code&gt;opcache.preferred_memory_model=shm&lt;/code&gt; on Podman under both emulation paths — Rosetta and QEMU. It fixed both. Clean runs, no crash, either way. So &lt;code&gt;shm&lt;/code&gt; is not just a Colima trick — it works everywhere I tested it. If you want one fix that works regardless of platform, use &lt;code&gt;shm&lt;/code&gt;, not the seccomp profile.&lt;/p&gt;

&lt;p&gt;For now, my machine runs Colima, OPcache is on, and the Docker invoice will never come. The rest — the final team decision — is still an open question. Maybe that will be another article.&lt;/p&gt;

</description>
      <category>php</category>
      <category>docker</category>
      <category>devops</category>
      <category>applesilicon</category>
    </item>
  </channel>
</rss>
