← Blog

Self-host Immich for your photo library

Railyard

June 5, 2026 · 7 min read

Follow this on your own server. Start free, no card needed

Google Photos backs up your library for free until it doesn’t, and the moment it stops being free it’s also indexed every face and every location your photos were taken at on hardware you don’t control. Immich is an open-source alternative that does the same job (automatic backup from your phone, a timeline, face recognition, search by object or place) as a self-hosted app: one main image, a machine-learning worker for the AI features, Redis for background jobs, and Postgres with a vector extension for search. This guide sets all of it up from an empty server, then shows the one-click path if you’d rather not run the containers yourself.

By the end you’ll have Immich reachable at your own domain over HTTPS, your phone backing up to it automatically, and a backup plan for the two things that actually matter: your photos and the database that indexes them.

What you’ll need

  • A server with real memory to spare. Immich’s machine-learning worker (face detection, object search) needs genuine CPU and RAM on top of the web app and database, so a Hetzner CX33 (4 vCPU, 8 GB RAM, 80 GB NVMe) at €8.99/month excluding VAT is a safer starting point than the smallest tier; a similarly sized server from any other provider works the same way.
  • A domain or subdomain with a DNS A record you can point at the server (this guide uses photos.example.com).
  • The Immich mobile app (iOS or Android) once the server is up.
  • SSH access and about 20 minutes.

Step 1: Provision the server and point DNS at it

Create the server on Ubuntu 24.04 (or any recent Debian-based distribution), note its public IPv4 address, and create a DNS A record:

photos.example.com.  A  203.0.113.20

SSH in and update the system:

ssh root@203.0.113.20
apt update && apt upgrade -y

Step 2: Install Docker

curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
docker --version
docker compose version

You should see a Docker engine version and a Compose v2.x version with no errors.

Step 3: Write the environment file

Immich’s own install docs point at a docker-compose.yml and .env pair maintained in its GitHub repository; this guide uses the current release, Immich v3.3.5. Create a working directory and the .env file:

mkdir -p /opt/immich && cd /opt/immich
UPLOAD_LOCATION=./library
DB_DATA_LOCATION=./postgres
DB_PASSWORD=change-this-to-a-random-alnum-string
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
IMMICH_VERSION=v3.3.5
TZ=America/Montreal

DB_PASSWORD has one real constraint worth knowing before you generate one: Immich’s own docs ask for only letters and digits, no punctuation, since the password is interpolated directly into a connection string. UPLOAD_LOCATION is where every photo and video actually lives on disk, so treat that path as the one directory on this server you can never afford to lose.

Step 4: Write the Docker Compose file

This mirrors Immich’s official compose file, with Caddy added in front for HTTPS:

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config

  immich-server:
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-v3.3.5}
    restart: always
    volumes:
      - ${UPLOAD_LOCATION}:/data
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    depends_on:
      - redis
      - database

  immich-machine-learning:
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-v3.3.5}
    restart: always
    volumes:
      - model-cache:/cache
    env_file:
      - .env
    environment:
      MACHINE_LEARNING_MODEL_REVISION: v2

  redis:
    image: docker.io/valkey/valkey:9
    restart: always
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep -q PONG || exit 1"]

  database:
    image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
    restart: always
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
      POSTGRES_INITDB_ARGS: "--data-checksums"
    volumes:
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    shm_size: 128mb

volumes:
  model-cache:
  caddy-data:
  caddy-config:

The database image isn’t plain Postgres: it’s Immich’s own build with the vectorchord and pgvecto.rs extensions baked in, which is what makes face and object search fast at scale. Don’t swap it for a generic postgres image even though the env vars look identical.

Validate before bringing it up:

docker compose config

Step 5: Write the Caddyfile

photos.example.com {
    reverse_proxy immich-server:2283

    request_body {
        max_size 10GB
    }
}

The request_body block matters here specifically: Caddy’s default request size limit is comfortable for a web page but not for a 4K video from a phone, so without raising it, large video uploads fail partway through with a confusing connection reset instead of a clear error.

Step 6: Bring the stack up

docker compose up -d
docker compose ps

You should see caddy, immich-server, immich-machine-learning, redis and database all listed as running. Tail the server’s logs the first time to confirm it finished its own startup migrations:

docker compose logs -f immich-server

Visit https://photos.example.com; you should land on Immich’s account creation screen over a valid HTTPS certificate. Create the first account, which becomes the server’s admin.

Step 7: Connect the mobile app

Install Immich from the App Store or Play Store, open it, and enter your server’s URL (https://photos.example.com) at the login screen, then sign in with the account from Step 6. Turn on background backup under the app’s settings. You should see your camera roll start uploading, visible as a progress indicator in the app and as new photos appearing in the web dashboard within a minute or two.

Step 8: Back up the data that actually matters

Two things can’t be regenerated: the Postgres database (the index: albums, faces, metadata) and the library folder (the actual photo and video files). Back up both, on a schedule, to somewhere other than this server:

docker compose exec -T database pg_dumpall -U postgres | gzip > /opt/immich/backups/immich-db-$(date +%F).sql.gz
tar -czf /opt/immich/backups/immich-library-$(date +%F).tar.gz -C /opt/immich library

Copy both files off the server (object storage, another machine) on a cron job. Restoring the database from a dump alone is enough to get the app running again; restoring the library folder alongside it is what makes the restored app actually show your photos again.

Step 9: Upgrades

Bump IMMICH_VERSION in .env to the new tag, then:

docker compose pull
docker compose up -d

Immich ships frequent releases, and the mobile app and server are versioned together: if the app shows a version-mismatch warning after an upgrade, update the app from its store before anything else, rather than troubleshooting the server.

The shortcut: do it with Railyard

Everything above, the four containers, the environment file, the reverse proxy, the backup schedule, is what Railyard’s Immich stack template sets up for you. Connect a server, then pick Immich in the templates catalog: https://app.railyard.run/deploy?stack=immich. Railyard provisions all four services together, wires the database and Redis connection strings automatically, and issues the HTTPS certificate for your domain.

One honest limit worth knowing: the machine-learning worker is CPU and memory hungry the same way it is in the manual setup, so size the server for it rather than the smallest available option, exactly as in Step 1. And one quirk you’ll see in the logs either way: Immich’s own startup log always prints 127.0.0.1 for a server bound to 0.0.0.0, which is the framework’s own display substitution, not a sign that anything is misconfigured.

Once it’s running, database backups cover the Postgres side on a schedule; the library volume still deserves its own off-server copy, the same as in the manual setup above.

Troubleshooting

HTTPS never issues. DNS isn’t pointing at the server yet. dig +short photos.example.com should return the server’s IP; Caddy retries on its own once it does.

Uploads fail partway through, especially videos. This is almost always Caddy’s request body limit, covered in Step 5. Confirm the request_body block is actually in the active Caddyfile with docker compose exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile followed by a restart.

The machine-learning container is slow or gets killed. Check docker stats while a backup is running. If immich-machine-learning is pegged at 100% CPU or the container exits, the server is undersized for background tasks like facial recognition on a large library; a CX33 handles normal use, but a very large existing library’s first full scan benefits from more.

Mobile app shows a version mismatch. Immich pins compatibility between the app and server fairly tightly. Update whichever one is behind, usually the app, from its store.

Login page loads but the admin account won’t create. Check docker compose logs database for a failed migration, usually caused by DB_PASSWORD containing a character outside A-Za-z0-9 that broke the connection string Step 3 warns about.

FAQ

How much storage do I actually need? Whatever your photo and video library is today, plus headroom for growth; Immich doesn’t compress or re-encode originals by default, so the library volume grows at roughly the same rate your camera roll does.

Can more than one person use the same instance? Yes. Create additional accounts from the admin panel; each has its own library and sharing settings, with admin-controlled storage quotas per user if you want them.

Does the machine learning worker need a GPU? No, it runs on CPU by default and that’s what this guide sets up; Immich’s own documentation covers optional hardware acceleration (NVIDIA, Intel, AMD) for larger libraries, which trades more setup complexity for faster indexing.

What happens if I lose the server without a backup? Everything is gone: the photos and the index both. Step 8 isn’t optional for anything you’d actually miss.

Is this really free to run? The software is, the server isn’t; a CX33 at roughly nine euros a month is the real ongoing cost, with no per-photo or per-gigabyte fee layered on top the way some cloud storage plans work.

Immich gets you most of what a hosted photo service does, on hardware you control, for the cost of a small server instead of a subscription that scales with how many photos you take. The setup here is one environment file, one compose file, and a reverse proxy; from there it’s the same app on your phone, backing up the same way, to a server with your name on the bill instead of someone else’s.

Why people pick Railyard for this

  • Your own cloud account. Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
  • One flat price per team. Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $49 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
  • One-click templates. Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
  • Managed databases. Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
  • Push-to-deploy. Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.

Skip the server setup

Deploy Immich from the templates catalog on Railyard, with the database and machine-learning worker wired up.

Free plan, no card, and 14 days of Pro for every new team. One flat price per team.