~/eugene-bert/articles← home
← back to articlesHow I Replaced Google Photos with Immich on a Steam Deck

How I Replaced Google Photos with Immich on a Steam Deck

Jun 6, 20265 min read
self-hostingsteam-deckimmichdocker

Why a Steam Deck?

I had a Steam Deck collecting dust. Instead of selling it, I decided to turn it into a home server. It's small, silent, energy-efficient, and has a built-in battery (free UPS). Why not?

The goal: self-host Immich as a Google Photos replacement, accessible from outside my home via Cloudflare Tunnel.

The Problem with SteamOS

SteamOS has a read-only filesystem. This means:

  • pacman doesn't work properly — PGP keys expire and can't be refreshed
  • You can't install Docker or any system packages permanently
  • Every SteamOS update can wipe your changes

I wasted hours trying to work around this before accepting the truth: SteamOS is for gaming, not for servers.

Enter Bazzite

Bazzite is a Fedora-based Linux distro designed specifically for Steam Deck (and other gaming handhelds). It gives you:

  • Full read-write filesystemdnf works, packages persist
  • Gaming Mode — same Steam + Proton experience as SteamOS
  • Desktop Mode — full KDE Plasma desktop
  • Designed for the Steam Deck hardware — controllers, display, sleep/wake all work

Installing Bazzite

  1. Download Bazzite from bazzite.gg (pick the Steam Deck image)
  2. Flash to USB with Balena Etcher or dd
  3. Boot from USB (hold Volume Down + Power)
  4. Install — I chose no encryption, local account, root enabled

The whole process takes about 15 minutes.

Docker on Bazzite

Here's where it gets interesting. Bazzite comes with Podman, but I ran into DNS resolution issues — containers couldn't find each other by hostname. Docker handles container networking better, so I switched.

Installing Docker via Homebrew

Bazzite comes with Homebrew pre-installed, which makes adding packages easy:

brew install docker docker-compose

The vfs Storage Driver Fix

After installing Docker, every container failed with a cryptic error:

runc create failed: invalid rootfs: not an absolute path, or a symlink

Even docker run hello-world crashed. The fix: change the storage driver to vfs:

mkdir -p ~/.config/docker
echo '{"storage-driver": "vfs"}' > ~/.config/docker/daemon.json
systemctl --user restart docker

vfs is slower than overlayfs but it works reliably on Bazzite's filesystem. Why? Bazzite uses an ostree/composefs filesystem that doesn't support fuse-overlayfs (the default for rootless Docker) properly. The runc process can't resolve the layered rootfs path, so it fails on every container — even hello-world. vfs bypasses this entirely by just copying files instead of layering them. Slower for building images, but no noticeable difference when running containers.

Setting Up Immich

Immich is an open-source, self-hosted Google Photos alternative. It has face recognition, search, mobile backup, and a beautiful UI.

Storage Setup

I added a microSD card for photo storage:

# Find the SD card
lsblk

# Format as ext4 (use KDE Partition Manager for GUI)
sudo mkfs.ext4 /dev/mmcblk0p1

# Create mount point and add to fstab
sudo mkdir /mnt/sdcard
echo "UUID=$(blkid -s UUID -o value /dev/mmcblk0p1) /mnt/sdcard ext4 defaults 0 2" | sudo tee -a /etc/fstab
sudo mount -a

Docker Compose

mkdir ~/immich && cd ~/immich

# Download official compose file
curl -o docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
curl -o .env https://github.com/immich-app/immich/releases/latest/download/example.env

Edit .env:

UPLOAD_LOCATION=/mnt/sdcard/immich
DB_DATA_LOCATION=/home/eugene/immich/postgres  # use absolute path!
TZ=Europe/Warsaw
DB_PASSWORD=your_secure_password

Important: Use an absolute path for DB_DATA_LOCATION, not a relative one like ./postgres. Relative paths caused issues with Docker on Bazzite.

docker compose up -d

Immich is now running at http://localhost:2283.

Importing 29,000+ Photos from Google Photos

Google Takeout gives you a zip file with all your photos, but the metadata (dates, locations) is stored in separate JSON sidecar files. Immich can't import these directly.

immich-go solves this — it reads Google Takeout exports and uploads photos to Immich with correct metadata:

# Download immich-go
curl -LO https://github.com/simulot/immich-go/releases/latest/download/immich-go_Linux_x86_64.tar.gz
tar xzf immich-go_Linux_x86_64.tar.gz

# Import (point to your extracted Takeout folder)
./immich-go -server http://localhost:2283 -key YOUR_API_KEY upload google-photos /path/to/takeout/

This took several hours for 29,000+ photos, but every photo kept its original date, GPS data, and album structure.

Cloudflare Tunnel — Access From Anywhere

The Steam Deck is behind a home router with no static IP (and possibly double NAT). Cloudflare Tunnel solves this — it creates an outbound connection from your server to Cloudflare, no port forwarding needed.

Setup

brew install cloudflared

# Authenticate
cloudflared tunnel login

# Create tunnel
cloudflared tunnel create immich

# Configure
cat > ~/.cloudflared/config.yml << 'EOF'
tunnel: YOUR_TUNNEL_ID
credentials-file: /home/eugene/.cloudflared/YOUR_TUNNEL_ID.json

ingress:
  - hostname: photos.yourdomain.com
    service: http://localhost:2283
  - service: http_status:404
EOF

# Add DNS record
cloudflared tunnel route dns immich photos.yourdomain.com

Autostart with systemd

cat > ~/.config/systemd/user/cloudflared.service << 'EOF'
[Unit]
Description=Cloudflare Tunnel
After=network.target

[Service]
Type=simple
ExecStart=/home/linuxbrew/.linuxbrew/opt/cloudflared/bin/cloudflared tunnel run immich
Restart=on-failure
RestartSec=10

[Install]
WantedBy=default.target
EOF

systemctl --user enable cloudflared
systemctl --user start cloudflared

Now photos.yourdomain.com serves your Immich instance from the Steam Deck.

Preventing Sleep

A server shouldn't sleep. Disable it:

sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing'

Was It Worth It?

Absolutely. Total cost: $0 (I already had the Steam Deck and a domain). I now have:

  • A Google Photos replacement with 29,000+ photos, face recognition, and mobile backup
  • A personal portfolio hosted on my own hardware
  • A Telegram bot for automation
  • Full control over my data

The Steam Deck draws about 5-10W while serving all this — less than a light bulb.

Tips and Gotchas

  1. Use Bazzite, not SteamOS — save yourself hours of fighting read-only filesystems
  2. Docker via Homebrew, not rpm-ostree — cleaner install, easier updates
  3. vfs storage driver — ugly but works on Bazzite
  4. Absolute paths in .env — relative paths break with Docker rootless
  5. Cloudflare Tunnel > port forwarding — works with any ISP, any NAT setup
  6. immich-go for Google Takeout — the only tool that preserves all metadata correctly
  7. Disable sleep — both systemd targets and GNOME settings

Have questions? Reach me at me@eugenelab.org or on LinkedIn.