Migrating from Docker Desktop to Colima: When Hardened Images Break

By 10:25 AM, I’d entered what Mystery Science Theater 3000 fans call ​“Deep Hurting.” The migra­tion plan was sol­id. The back­up dis­ci­pline was com­pre­hen­sive. The exe­cu­tion? Chaos.

I run a con­tainer­ized pro­duc­tion Mastodon instance on an 8 GB Mac mini. (Yes, I know what the cloud peo­ple say, and FYI it’s Cloudflare Tunneled for pro­tec­tion.) My Docker Desktop installation’s half-​gig RAM foot­print was eat­ing pre­cious resources. Colima promised the same Docker expe­ri­ence with­out the GUI over­head. I bud­get­ed a 1.5 hour migra­tion plan for what should’ve been a straight­for­ward run­time swap.

Two and a half hours and sev­en crit­i­cal issues lat­er, I’d dis­cov­ered that Docker Hardened Images and Colima don’t play nice­ly togeth­er. And that dis­cov­ery mat­ters to any­one run­ning hard­ened con­tain­ers in vir­tu­al­ized environments.


The Plan (That Didn’t Survive Contact with Reality)

The strat­e­gy was text­book: main­te­nance win­dow approach, com­pre­hen­sive back­ups (data­base dumps, vol­ume archives, con­fig­u­ra­tion snap­shots), explic­it roll­back pro­ce­dures. I’d stop Docker Desktop, switch the Docker con­text to Colima, update one path in the Makefile I use to auto­mate tasks, and restart ser­vices. Everything uses bind mounts, so data stays on the host file sys­tem. What could go wrong?

Everything. Everything could go wrong.

Obsolete Makefile references

First back­up try:

service "db" is not running

Wait–what’s db? I migrat­ed from ver­sion 14 to ver­sion 17 of the PostgreSQL rela­tion­al data­base sys­tem weeks ago. Switched and even switched from the default PostgreSQL image to a Docker Hardened Image (DHI), even. My com­pose files ref­er­ence db-pg17. But the Makefile’s back­up tar­gets? Still call­ing the old db ser­vice. The PostgreSQL migra­tion doc­u­men­ta­tion lived in the README file that I keep. The Makefile lived in… a dif­fer­ent men­tal con­text apparently.

Lesson: When you migrate infra­struc­ture com­po­nents, grep for ref­er­ences every­where. Compose files, Makefiles, scripts, doc­u­men­ta­tion. ​“It’s work­ing” means ​“it’s work­ing right now,” not ​“the migra­tion completed.”

The empty postgres17/ directory

After resolv­ing the data­base restore issues (we’ll get there), con­tain­ers start­ed suc­cess­ful­ly. Then I ran a restart test. PostgreSQL came up empty–no data, no tables, fresh initialization.

% ls -la postgres17/
total 0
drwxr-xr-x@ 2 markandsharon staff 64 Jan 7 16:31 .

64 bytes. An emp­ty direc­to­ry. That December PostgreSQL 14 → 17 ​“migra­tion”? Created the direc­to­ry, nev­er pop­u­lat­ed it. PostgreSQL 14 data stayed in postgres14/. Docker Desktop must’ve been using cached or inter­nal storage.

Lesson: Don’t trust that migra­tions suc­ceed­ed because ser­vices are healthy. Check the actu­al data files. Persistence isn’t per­sis­tence if noth­ing’s persisting.

Wrong database target

After fix­ing the Makefile, ser­vices start­ed… and instant­ly crash-looped:

PG::UndefinedTable: ERROR:  relation "users" does not exist

PostgreSQL was healthy. The appli­ca­tion dis­agreed. Turns out I’d restored the dump to the wrong database:

# What I did (wrong):
psql -U mastodon postgres < dump.sql
# What I should have done:
psql -U mastodon mastodon_production < dump.sql

The mastodon_production data­base existed–it was just emp­ty. All my data went into the postgres data­base that noth­ing was read­ing. The psql command-​line client defaults to the data­base match­ing your user­name or postgres if unspec­i­fied. Explicit is bet­ter than implic­it, espe­cial­ly when you’re in a hurry.

Version-​specific PGDATA paths

Once data land­ed in the right data­base, I hit a new prob­lem: data did­n’t per­sist across restarts. The bind mount direc­to­ry stayed emp­ty even though PostgreSQL was run­ning and accept­ing writes.

It turns out that my PostgreSQL DHI uses version-​specific paths:

# My bind mount:
- ./postgres17:/var/lib/postgresql/data
# Actual DHI PostgreSQL data directory:
# PGDATA=/var/lib/postgresql/17/data

The mount shad­owed the wrong direc­to­ry. PostgreSQL wrote data to /var/lib/postgresql/17/data, which was­n’t mount­ed. Data lived in ephemer­al con­tain­er stor­age. Restart? Data gone.

$ docker compose exec db-pg17 psql -U mastodon postgres -c "SHOW data_directory;"
       data_directory
-----------------------------
 /var/lib/postgresql/17/data

Lesson: Verify assump­tions. Every sin­gle one. Check SHOW data_directory; imme­di­ate­ly after con­tain­er start. Test a restart before cel­e­brat­ing success.

I cor­rect­ed the mount path to match DHI’s expect­ed loca­tion. That’s when I found the real problem.

The DHI + Colima Incompatibility Discovery: VirtioFS bind mount ownership failures

After cor­rect­ing the mount path, PostgreSQL entered an imme­di­ate crash-loop:

FATAL: data directory "/var/lib/postgresql/17/data" has wrong ownership
HINT: The server must be started by the user that owns the data directory.

Inside the con­tain­er, the mount­ed direc­to­ry appeared owned by the root user (user ID 0). But PostgreSQL runs as the postgres user. Permission denied.

% docker compose run --rm --entrypoint sh db-pg17 -c "ls -ld /var/lib/postgresql/17/data"
drwxr-xr-x 2 0 0 4096 Jan 10 16:22 /var/lib/postgresql/17/data
# Owner: UID 0 (root), but PostgreSQL requires postgres user ownership

Colima uses the VirtioFS sys­tem for file shar­ing. VirtioFS han­dles UID map­ping dif­fer­ent­ly than Docker Desktop’s vir­tu­al machine (VM) imple­men­ta­tion. Bind mounts that work per­fect­ly on Docker Desktop fail on Colima because the own­er­ship map­ping does­n’t translate.

Fine. This is a known issue with Colima and some images. I’ll switch to a named volume–Docker man­ages those inter­nal­ly, so host filesys­tem per­mis­sions should­n’t matter.

Named vol­umes still failed:

FATAL: data directory "/var/lib/postgresql/17/data" has wrong ownership

Wait. Named vol­umes are sup­posed to be iso­lat­ed from host file sys­tem issues. They’re man­aged entire­ly by Docker. Fresh named vol­ume, Docker cre­ates it, Docker pop­u­lates it–and it still shows wrong own­er­ship inside the DHI container.

# Fresh named volume:
% docker compose run --rm --entrypoint sh db-pg17 -c "ls -ld /var/lib/postgresql/17/data"
drwxr-xr-x 2 0 0 4096 Jan 10 16:22 /var/lib/postgresql/17/data

DHI PostgreSQL’s entry­point has envi­ron­men­tal assump­tions that Colima’s VM does­n’t sat­is­fy. The image’s secu­ri­ty hard­en­ing includes stricter own­er­ship val­i­da­tion. That val­i­da­tion does­n’t account for Colima’s vol­ume handling.

The pragmatic trade-off

So I had to make a decision:

  1. Debug DHI + Colima com­pat­i­bil­i­ty (unknown time invest­ment, might be unsolv­able), or
  2. Switch to the stan­dard postgres:17-alpine image (known work­ing, imme­di­ate resolution)

Production sys­tem. Already 1.5 hours into debug­ging. Swap the image:

# Before (DHI):
image: dhi.io/postgres:17-alpine3.22
volumes:
  - postgres17-data:/var/lib/postgresql/17/data
# After (Standard):
image: postgres:17-alpine
volumes:
  - postgres17-data:/var/lib/postgresql/data

PostgreSQL ini­tial­ized suc­cess­ful­ly. Data per­sist­ed across restarts. Services came up healthy.

The trade-​off:

  • ✓ Gained: Colima com­pat­i­bil­i­ty, reli­able data per­sis­tence, onward progress
  • ❌ Lost (tem­porar­i­ly): DHI secu­ri­ty hardening–documented for future investigation

Docker Hardened Images offer secu­ri­ty fea­tures through stricter defaults and entry­point val­i­da­tion. Those same strict require­ments reduce the com­pat­i­bil­i­ty sur­face. When you intro­duce a dif­fer­ent vir­tu­al­iza­tion envi­ron­ment (Colima’s VirtioFS instead of Docker Desktop’s VM), the hard­en­ing becomes brittleness.

This isn’t DHI’s fault–it’s the expect­ed con­se­quence of defense-​in-​depth. But if you’re migrat­ing from Docker Desktop to Colima, test your image com­pat­i­bil­i­ty in iso­la­tion first. This is cru­cial if you are using Docker Hardened Images. Carry out these tests before migra­tion day.


The Outcome

Migration com­plet­ed at 11:30 AM. Zero data loss. All ser­vices healthy. Automation restored. RAM reclaimed (Docker Desktop’s over­head vs. Colima’s neg­li­gi­ble footprint).

The real out­come was discovering–systematically, through elimination–that DHI PostgreSQL and Colima are incom­pat­i­ble with­out fur­ther inves­ti­ga­tion. I’ve doc­u­ment­ed this as a known issue. Future work: test DHI with dif­fer­ent vol­ume strate­gies, check whether new­er DHI ver­sions resolve the issue, eval­u­ate whether the secu­ri­ty delta mat­ters for a single-​user instance.

For now, I’m run­ning stan­dard postgres:17-alpine. The migra­tion is suc­cess­ful. The secu­ri­ty regres­sion is doc­u­ment­ed and sched­uled for future inves­ti­ga­tion. Forward progress beats perfectionism.

Key Takeaways

Backups are your safe­ty net–use them. I restored the data­base once dur­ing this migra­tion. That restore took 30 sec­onds because I’d ver­i­fied the back­up exist­ed and was recent.

Systematic debug­ging beats pan­ic every time. Bind mounts failed → tried named vol­umes → still failed → iso­lat­ed to image-​specific behav­ior. That pro­gres­sion ruled out host file sys­tem issues and point­ed direct­ly at image compatibility.

Pragmatic trade-​offs beat per­fec­tion­ism. I could’ve spent hours debug­ging DHI com­pat­i­bil­i­ty. Instead, I doc­u­ment­ed the incom­pat­i­bil­i­ty, switched to stan­dard images, and moved on. The secu­ri­ty regres­sion is tracked. The pro­duc­tion sys­tem is running.

Document fail­ures hon­est­ly; they’re learn­ing oppor­tu­ni­ties. This post exists because the migra­tion did­n’t go smooth­ly. The DHI + Colima incom­pat­i­bil­i­ty is now doc­u­ment­ed for any­one else hit­ting the same issue. That’s more valu­able than a ​“here’s how I moved from X to Y” suc­cess story.

Migration dura­tion2.5 hours actu­al vs. 1.5 hours planned
Issues encoun­tered7 crit­i­cal
Data loss0 bytes
ServicesAll healthy
Memory reclaimed~500 MB
Novel dis­cov­er­ies1 (DHI + Colima incompatibility)
Trade-​offs documented1 (secu­ri­ty hard­en­ing vs. compatibility

Running pro­duc­tion infra­struc­ture on an 8 GB Mac mini teach­es you to val­ue both resources and reli­a­bil­i­ty. Colima deliv­ers on the resources. This migra­tion deliv­ered on the reli­a­bil­i­ty… eventually.


Discover more from The Phoenix Trap

Subscribe to get the latest posts sent to your email.

Mark Gardner Avatar

Hi, I’m Mark.

Hi, I’m Mark Gard­ner, and this is my personal blog. I show software developers how to level up by building production-ready things that work. Clear code, real projects, lessons learned.