Free guide
Intermediate

How to Build
Your Own Photo Server

Set up a NixOS PC running a self-hosted Google Photos replacement (called "Immich") and expose it safely to the internet via a Cloudflare tunnel.

No open ports. No subscription fees. Your photos, fully under your control.

🥴

Too difficult?

Don't worry, this is a challenging thing to achieve unless you have an understanding of computers, Linux command line, and how applications work.

Get help

Before you start

What you will need

🖥️

A spare PC

Most oldre machines works — even an old laptop or mini-PC. 2 GB RAM minimum, 4 GB recommended.

💾

Storage drive

A second drive (or partition) dedicated to photos. Size depends on your library — see our storage guide.

🔌

USB drive (4 GB+)

For flashing the NixOS installer. Will be wiped during the flashing process.

🌐

A domain name

You need a domain pointed at Cloudflare nameservers. Domains cost ~£10/year, or register one free via Cloudflare.

☁️

Free Cloudflare account

Sign up at cloudflare.com. Zero Trust tunnels are completely free with no bandwidth limits.

💻

A second computer

To SSH into the server and follow this guide once NixOS is installed headlessly.

A computer surrounded by wires

Above: A typical PC being configured to run the photos application.

1
Step 1

Install NixOS

Download the minimal ISO, flash it to a USB drive, then boot the server PC from it and run the installer.

1a — Flash the installer

Download the NixOS 24.11 minimal ISO from nixos.org/download. Then flash it with Balena Etcher (GUI) or dd on Linux/macOS.

1b — Partition and install

Boot the server from the USB. When you reach a shell, become root and partition the boot drive.

nixos-generate-config writes a starter /mnt/etc/nixos/configuration.nix and detects your hardware. You will replace most of it in Step 3, but run it now so the hardware scan (hardware-configuration.nix) is generated.

1c — Set a temporary password and install

nixos-install
Setting root password...
reboot

After rebooting, SSH into the server as root using the password you set. From here everything is done remotely.

2
Step 2

Create a Cloudflare Tunnel

A Cloudflare tunnel lets Immich be reachable from the internet via your domain without opening any ports on your router. All traffic goes through Cloudflare's network.

  1. 1 Log into dash.cloudflare.com and open Zero Trust from the sidebar
  2. 2 Go to Networks → Tunnels and click Create a tunnel
  3. 3 Choose Cloudflared as the connector type
  4. 4 Give your tunnel a name — e.g. immich — and click Save
  5. 5 On the next screen, Cloudflare shows you a connector install command — ignore it. Instead, click the Overview tab and download the credentials JSON file
  6. 6 Go to Public Hostname and add: Subdomain photos, Domain yourdomain.com, Service http://127.0.0.1:2283

Copy the credentials to your server

The downloaded JSON is named after your tunnel ID (e.g. a1b2c3d4-....json). Copy it to the server and lock down its permissions:

scp ~/Downloads/a1b2c3d4-tunnel.json root@your-server-ip:/etc/cloudflared/immich.json
chmod 600 /etc/cloudflared/immich.json
3
Step 3

Configure the OS

This is the heart of NixOS. One file declares your entire system — Immich, the Cloudflare tunnel daemon, SSH, firewall, and everything else. Replace /etc/nixos/configuration.nix with the following, adjusting the values marked with comments:

{ config, pkgs, ... }:

{
  imports = [ ./hardware-configuration.nix ];

  # ── Boot ────────────────────────────────────────────────────
  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;

  # ── Network ─────────────────────────────────────────────────
  networking.hostName = "photo-server"; # Change to your liking
  networking.networkmanager.enable = true;

  # ── Locale ──────────────────────────────────────────────────
  time.timeZone = "Europe/London"; # Change to your timezone
  i18n.defaultLocale = "en_GB.UTF-8";

  # ── User account ────────────────────────────────────────────
  users.users.alice = {  # Change 'alice' to your username
    isNormalUser = true;
    extraGroups = [ "wheel" "networkmanager" ];
    openssh.authorizedKeys.keys = [
      "ssh-ed25519 AAAA... paste-your-public-key-here"
    ];
  };
  security.sudo.wheelNeedsPassword = false;

  # ── SSH ─────────────────────────────────────────────────────
  services.openssh = {
    enable = true;
    settings.PasswordAuthentication = false; # Key-only login
  };

  # ── Photo storage drive ─────────────────────────────────────
  # Format your photos drive with: mkfs.ext4 -L photos /dev/sdXY
  fileSystems."/data/photos" = {
    device = "/dev/disk/by-label/photos";
    fsType = "ext4";
  };

  # ── Immich ──────────────────────────────────────────────────
  # NixOS manages PostgreSQL and Redis automatically for Immich
  services.immich = {
    enable = true;
    host = "127.0.0.1";    # Local only — tunnel handles external
    port = 2283;
    mediaLocation = "/data/photos";
    openFirewall = false; # Cloudflare tunnel handles ingress
  };

  # ── Cloudflare tunnel ───────────────────────────────────────
  services.cloudflared = {
    enable = true;
    tunnels = {
      "immich" = {
        # Path to the JSON you downloaded from the Cloudflare dashboard
        credentialsFile = "/etc/cloudflared/immich.json";
        default = "http_status:404";
        ingress = {
          # Change to your actual domain
          "photos.yourdomain.com" = "http://127.0.0.1:2283";
        };
      };
    };
  };

  # ── Firewall ────────────────────────────────────────────────
  # Only SSH — Cloudflare tunnel needs no open inbound ports
  networking.firewall = {
    enable = true;
    allowedTCPPorts = [ 22 ];
  };

  system.stateVersion = "24.11";
}

Generate your SSH key pair (on your local machine)

If you do not already have one, create an Ed25519 key and paste the public key into the config above:

ssh-keygen -t ed25519 -C "photo-server"
cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... photo-server
4
Step 4

Deploy the configuration

SSH into the server and apply the config. NixOS will download and build everything — Immich, its dependencies, PostgreSQL, Redis, and the Cloudflare tunnel daemon — in one shot.

ssh root@your-server-ip
nixos-rebuild switch
building the system configuration...
activating the configuration...
Done.

The first build takes several minutes as packages are downloaded and compiled. Subsequent rebuilds are fast because NixOS caches unchanged outputs.

Verify all services are running

systemctl status immich
systemctl status cloudflared-tunnel-immich
systemctl status postgresql

All three should show active (running). If any show failed, run journalctl -u service-name -n 50 to see the last 50 log lines.

5
Step 5

Login to Immich

Open https://photos.yourdomain.com in your browser. You should see the Immich welcome screen.

  • Click Get Started and create your admin account
  • Set a strong password — this is a public-facing URL
  • In Administration → Users, create personal accounts for family members
  • Download the Immich mobile app (iOS or Android) and point it at your domain
  • Enable automatic backup in the app — your phone photos will now upload to your server
The Immich user interface

Above: The photos interface – a fully featured photos app with facial recognition, folders, OCR and even a map.

Under the hood

How it all fits together

Curious about why each piece works the way it does? Read on.

Why NixOS? Why not Ubuntu or Debian?

NixOS is uniquely suited to a home server because it is declarative and reproducible. Your entire system state is described in one file. If Immich releases a breaking update, you can roll back to yesterday's configuration with nixos-rebuild switch --rollback. No tracking down which packages changed, no manual cleanup.

It also has first-class Immich support via the services.immich module, which handles all the PostgreSQL extensions (pgvecto.rs for AI search), Redis, and inter-service networking automatically — things that require hours of manual configuration on a traditional distro.

Why Cloudflare tunnel instead of port forwarding?

Traditional port forwarding (opening port 443 on your router and pointing it at your server) works, but has serious drawbacks: your home IP address is exposed publicly, many ISPs block inbound port 80/443 on residential connections, and you are responsible for TLS certificate renewal.

A Cloudflare tunnel flips this model. The cloudflared daemon on your server dials outbound to Cloudflare's edge. Cloudflare then routes incoming requests from your domain back through that established connection. Your server never listens on an external port. Your home IP is never exposed. TLS is handled entirely by Cloudflare.

What does the credentials JSON actually contain?

The credentials file is a small JSON with three fields:

{
    "AccountTag":   "your-cloudflare-account-id",
    "TunnelSecret": "base64-encoded-32-byte-secret",
    "TunnelID":     "a1b2c3d4-e5f6-..."
  }

The TunnelSecret is what authenticates your cloudflared daemon to Cloudflare. It proves the connection is coming from you and not an impersonator. This is why the file must be kept secret and readable only by the cloudflared process.

How do I update Immich when a new version is released?

With NixOS, upgrading is just updating your channel and rebuilding:

nix-channel --update
nixos-rebuild switch

NixOS will fetch the new Immich version (and any updated dependencies) atomically. If the new version has a bug, roll back instantly:

nixos-rebuild switch --rollback
How do I import my existing Google Photos library?

Use Google Takeout (see the How It Works guide) to export your library as a .zip archive, then:

  1. 1 Extract the archive to a folder on a computer that can reach your server
  2. 2 Run Google Photos Takeout Helper to merge the .json sidecar files back into the image EXIF data
  3. 3 Use Immich's built-in CLI importer, or copy the folder into the Immich upload directory and use the External Library feature
docker run --rm -v /path/to/takeout:/input ghcr.io/immich-app/immich-cli:latest upload --recursive /input