<?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>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>
