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.

Your post must include a link to this post.

How does this work?
Write a response on your own website and include a link to this post. Then submit your response’s URL here. Your response may appear here after moderation. To update or remove it, edit or delete your original post and submit the same URL again. Learn more about Webmentions.