Absence is the goal Link to heading

I take a lot of photos and I intend to keep them for the rest of my life, which is longer than any application I use today1. So the goal of my photo workflow isn’t a feature, it’s an absence: no lock-in. In practice that means five things:

  • The files are the source of truth. Ordinary JPEG, HEIC and video files in ordinary folders named after the event. Anything can open them.
  • The metadata lives in the files. Tags, places and ratings are written into the files (or XMP sidecars next to them) by digiKam, not stored in an application’s database. They follow the photo when I move it and they are standard2.
  • Every application is replaceable. Many tools may read the archive, exactly one may write it.
  • The glue is open. OIDC for the login, a plain HTTP API, files on a network share.
  • Backups restore with generic tools. A zip and a database dump, nothing proprietary. Complicate at your will/pace (remote object storage, encryption, 3-2-1, you name it).

The test I apply to every piece is simple: if this program disappeared tomorrow, how long until I’m browsing my photos again? For the archive the honest answer is “a minute”: point another program at the same folders. I know because I did it. For a while I ran two photo managers side by side3 on the same read-only archive to choose between them, and it only worked because the files belonged to neither.

The workflow looks like this:

phone ──backup──> inbox (managed library, one folder per user)
                     │  review
                     ▼
        digiKam: edit, tag, geotag ──writes──> archive (plain folders + XMP)
                                                    │ read-only
                          viewers: Immich today, anything else tomorrow

Immich is the viewer I use now, and it’s also where the phone uploads land. That makes it the one part of the workflow that could introduce lock-in, so most of this post is about keeping it on a short leash. The rule became:

Immich is a viewer. digiKam is the only thing that writes to the archive.

What follows is what I learned making that rule hold up in practice, including the parts that did not go as I expected. It’s long, but each section stands on its own.


Two kinds of storage Link to heading

Immich has two ways to hold photos, and understanding the difference is the key to everything else:

  • External libraries are folders that Immich reads. It imports what it finds, creates thumbnails and metadata in its own database, and leaves the files where they are. This is where the archive lives.
  • The managed library is where Immich stores what you upload, for example the automatic backup from a phone. Immich owns the files there, chooses their folder layout, and keeps them in sync with its database.

My layout ended up like this:

archive (read-only)            managed library (read-write)
  <event folders>                <user label>/<date>/<file>
  edited only by digiKam         written only by Immich
        |                               |
        +---------- Immich -------------+
        |
   database + thumbnails (separate volume)

The archive is mounted read-only at three levels: the volume definition, the mount option and the container’s own readOnly flag. It sounds paranoid and later I’ll show you why it wasn’t.

The managed library lives on its own network share, with one folder per user, which Immich creates from a per-user storage label. If you set the label to something readable, the folders are readable too.


Immich: where it runs and how it’s updated Link to heading

I run Immich on a small Kubernetes cluster, installed from the project’s Helm chart. The chart was a few releases behind the application, so I upgraded by overriding the image tags instead of waiting. A few things to know if you do the same:

  • The server and the machine-learning image tags must match, so bump both.
  • Read the release notes for behavior changes. Mine had no breaking changes, but a couple of defaults moved (OAuth claims now sync at every login, for example).
  • Immich runs its database migrations when the server starts, and they only go forward. Take a database dump before you upgrade, and treat “roll back the image” as unavailable after the first start.
  • The chart doesn’t know about environment variables that appeared after it was published, so if a new release needs one, you have to add it yourself.

The first rollout got stuck, and the cause is common enough to be worth a paragraph. The data volume is a ReadWriteOnce volume, and the default RollingUpdate strategy starts the new pod before it stops the old one. When the new pod lands on another node, it waits forever with a Multi-Attach error. The fix is one line, strategy: Recreate on the server’s controller. It costs a short downtime on each rollout and removes the deadlock but for a family-wide solution it’s more than acceptable.

The usual “due diligence” applies: backup you data before updating and read the changelog.


Immich: yet another set of credentials… IdP customization Link to heading

I wanted one login for every service in the house, so Immich talks to my OIDC identity provider.

One surprise cost me an hour. I gave the Immich application its own email claim through a custom scope mapping, so that it would see a different address than my other services. It didn’t work: I logged in and landed in the old account. The reason is that Immich matches an OAuth login first by the stable subject ID (sub) and only then by email. If an account is already linked to your identity, the email claim is ignored. The fix was to unlink the account in the user settings, correct the email and log in again. The custom config in your IdP is only relevant if you have different emails for each application and the main email in your IdP account. I’d say an edge case so I won’t bother you with the details (hint: custom mapping and a dedicated attribute per user).

Once everything worked, I turned password login off in the admin settings. Before doing that, remember how you’d turn it back on if the identity provider is down. Immich ships a command for it:

immich-admin enable-password-login

It runs inside the server container and takes effect immediately. The important part is that your access to that container doesn’t depend on the identity provider. Write the procedure somewhere next to the config, not in your head.

In case you can take it of course just have a dedicated set of credentials for each application is ok as well but I’m trying to consolidate everything into my own IdP (which is Authentik).

Takeaway Don’t bother with OIDC/Oauth2 unless you have an homelab and time to spare. Normal application credentials are good for the vast majority of users.


Where uploads actually go Link to heading

It would be nice that photos uploaded from a phone would end up in “my library” next to the archive. Surely they don’t and that’s good an expected. Uploads go to the managed library, and nothing is ever written into an external library. That’s the exact behavior I wanted, but it means the volume that holds Immich’s data (upload folder, thumbnails, transcoded videos) grows with every user, and it was a small volume as a relic from the first testing phase.

The solution was to put the managed library on its own share, mounted where Immich expects it. Immich writes a hidden marker file (.immich) into each of its folders and checks it at startup. This matters when you move a folder to a new volume: an empty share has no marker, and Immich can refuse to start. I did it in two steps:

  1. Mount the new share at a temporary path and copy the existing contents over, including the marker, then compare the files by hash.
  2. Move the mount to the real path.

That’s two small changes instead of one, and the second one can’t take Immich down. Afterwards I enabled the storage template with a per-user label, which turns the opaque upload folders into <label>/<date>/<file>. When you change a user’s label, Immich only moves their existing files when you run the storage template migration job.

Takeaway For me the storage template feature and the ability to use a user “storage label” rather than an obscure user id were an integral part of the solution.


The duplicates hunt. Well… not so much Link to heading

Phones keep everything, and my archive already contained most of what was on mine. After a first full backup, Immich showed about 1,300 photos in my uploads, and I wanted to delete the ones that were already in the archive. I was pretty sure that all of those were duplicates because of how strict I am into populating the archive on a schedule. So I knew that only 5 photos were new and the rest could be deleted. But nevertheless I did some checking to be extra sure. The check just confirmed everything and highlighted what I already knew but better safe than sorry.

  • Checksums don’t match across libraries. A photo that is byte-for-byte identical in an external library and in the managed library gets a different checksum in each. As far as I can tell, files in external libraries are not checksummed by content. So you need other keys to compare.
  • Capture times differ at the edges. Uploads carry milliseconds and archive files often don’t. Some archive copies are one second off. Compare the time to the second, with a tolerance.
  • Dimensions swap between portrait and landscape depending on how the orientation is stored, so compare them in either order.
  • Videos are dated in the wrong hour. QuickTime files carry no time zone, so the archive copy was exactly one hour later than the upload. For large files, an identical size inside a folder of the same date is a safe match. I confirmed a few pairs byte by byte.
  • The archive had other names. Some files had been exported under UUID-style names. Others were a HEIC where the phone upload was a JPEG of the same photo. I decided that a HEIC in the archive always wins over the JPEG version.
  • Names are weak, but useful in a folder. My archive folders start with the date, so matching by name without extension, case-insensitively, inside a folder whose date equals the photo’s date finds the rest.

I wrote the comparison as a SQL query against Immich’s database, read-only, and exported a CSV that put every upload into a category: safe to delete (archive copy is identical, larger, or HEIC), to review (archive copy smaller), and not in the archive. About 1,280 of 1,344 were covered. The rest I could look at by hand.

The result were comforting by the minute. In the end everything confirmed my expectations: the new 5 photos were in and the rest could be deleted since it was already in the archive.

Takeaway All this hassle is not meaningful if you take the decision to cut over when you actually know that all you phone photos are already “free” in your archive before starting using Immich (or any other solution).


Deleting without breaking the story Link to heading

At some point you need to “promote” you uploaded files (owned by the Immich library) to you archive. The storage template option allows you to easily identify the day (or month or year according to your schedule) to work on. You then just copy the Immich files to the archive and start working on digikam (which is the only allowed application to write to your archive right?). Of course you can also have a staging area where you do your post processing with digikam before actually copying the result to the archive. It will be shown in Immich anyway and safe from changes.

What follows is a specific case where you a re not sure if the photos that you uploaded are already in the archive or if you want to double check that.

I took a zip of the whole managed folder first and verified it on another share. Then I cleared the phone’s photo library and removed the uploads from the Immich app, choosing to delete from both the phone and the server. The server copies go to the trash, which is a good safety net, so I looked through it before emptying it.

What I learned from the clean-up:

  • Deleting on the server only makes the phone upload the photo again, as long as the photo still sits in a backed-up album. Clear the phone first, or exclude the album.
  • Live Photos are two assets. The short clip is stored as a separate hidden video that no list shows. Deleting the still from the app left the clips behind4, and I couldn’t find them in the UI. You need the API or the integrity report to get rid of them (ie admin access).
  • Don’t delete files on disk behind Immich’s back if you can avoid it. Immich keeps a database that must agree with the folder. If they disagree, the integrity report in the admin area lists the missing files, but the check only finds new problems when you press Check. Refresh only re-verifies existing entries, and the page needs a reload to show the result.
  • Empty folders stay. Immich removes files and leaves the date folders5. The NAS added its own thumbnail directories too. They’re harmless.

And now the part where the paranoia paid off. When I emptied the trash, Immich also tried to remove files belonging to a few archive assets, the clips I just mentioned, and the logs filled with EROFS: read-only file system. The files were fine, because the archive is mounted read-only. Only the database rows disappeared, and a library scan brought them back. If the mount had been writable, five files from my archive would be gone, and nothing in the interface would have warned me.

Takeaway Strong suggestion: draw a clear line from the moment you start uploading from your phone and what is the last photo in the archive. In other words: do not overlap uploads with archived assets and everything will be easier.


Sizing the volume Link to heading

The managed library goes to a share with plenty of space, but Immich’s own volume holds three kinds of data, and they grow differently:

  • Thumbnails and previews cost about 0.4 MB per asset. I measured that on my own library, which is mostly photos.
  • Transcoded videos are the big unknown. The default policy transcodes anything that isn’t H.264, and phone videos are HEVC. If a user’s uploads are mostly video, the transcodes can exceed the volume you planned. Watch the first import closely.
  • The built-in database dumps, a compressed dump per night with a default of 14 kept, took 1.5 GB of that same volume. If you already back up the database some other way, move that folder to a different share or switch the job off (I did the former because I didn’t want to lose the application-supported ability to quickly restore).

Per-user quotas are enforced when an upload arrives, and they count the uploaded originals only. They don’t count external libraries (which makes sense), thumbnails or transcodes (this a bit less). Immich’s own storage widget shows the free space of whatever disk is behind the upload folder, so once that folder is on a network share, the widget shows the share’s volume, not the data volume. Ignore it and use your monitoring instead. I also set an alert for each volume at about 80% of the real volumes.


Conclusions Link to heading

  • Decide what is allowed to write where, before installing anything. One sentence, “only digiKam writes to the archive”, made all the later choices obvious.
  • Make the read-only part real, at the volume, the mount and the container. It’s cheap, and it protected my files during the one operation I thought was safe.
  • Check how a delete propagates (phone, server, trash, disk) with one throwaway photo before doing it with a thousand.
  • Write the recovery procedure next to the configuration. Turning password login off is easy, and being locked out of your own photos at the wrong moment is not.
  • You don’t need a NAS, Kubernetes, OIDC or other complex stuff to implement this workflow and it may not work for you. I know it works for me.

The lock-in scorecard Link to heading

Where does each kind of data live, and what would leaving cost?

Data Where it lives Cost of leaving
Original photos and videos Plain folders, read-only None: point another tool at them
Tags, places, ratings In the files and XMP sidecars (digiKam) None: any tool that reads EXIF/XMP sees them
Phone uploads Plain files in a readable layout (<user>/<date>/<file>) Low: copy the folders
Albums, favorites, faces, shares made inside Immich Immich’s database This is the leak. A dump restores into Immich only
Thumbnails, previews, transcodes Immich’s volume None: they’re regenerated
Login OIDC Low: any OIDC client works or you can just keep using the application user database and credentials, this is totally optional.

The honest line is the fourth one. Everything I curate happens in digiKam and ends up in the files, and I deliberately don’t build albums or collect favorites in Immich. If I ever do, I’ll accept it as a convenience that I can lose, not as the place my work lives.

I’m happy with the result: the archive is untouched, my phone backs up on its own into a separate place, everyone in the house signs in with the same account, and a random photo shows up on the wall every morning6. Whether it was worth it for “just a random picture” is a question I’m choosing not to ask…

Disclaimer: parts of this article have been generated or reworked by an AI agent.



  1. remember Everpix, Trovebox (to name a few)? I do not consider (but I tried them) consumer solutions like Apple Photos, Google photos, Picasa, Synology photos because they create more problems than they solve. Any Adobe solution is out of the question because I am not a professional photographer nor I want to subscribe to their Creative Suite for an excessive price. ↩︎

  2. until the next standard comes around but at least you can decide when to adopt it, if at all ↩︎

  3. Photoprism, a close second and of course Immich. ↩︎

  4. one of the few clutter that Immich leaves out, not 100% nice. ↩︎

  5. did I mention clutter/leftovers? I think I did, here is another one. ↩︎

  6. the details on this might come in a future post, I’m still reworking the solution to support multiple users in Home Assistant. ↩︎