Safeguard
Supply Chain Security•

Self-Host Photos, Files and Calendar to Replace Google and iCloud

Run Nextcloud for files, calendar and contacts and Immich for photos on one encrypted Docker server, reached over WireGuard, with tested 3-2-1 backups.

Safeguard Security Team
47 min read

To replace Google Drive, Google Photos, iCloud Drive, iCloud Photos and their calendars with servers you own, run two open source stacks on one small Linux machine with an encrypted data disk: Nextcloud All-in-One for files, calendar (CalDAV) and contacts (CardDAV), and Immich for photo backup, faces and maps. Reach both over WireGuard or a TLS reverse proxy, never expose the admin panels, import your Google Takeout and Apple data exports, and run a 3-2-1 backup you have actually restored once.

The rest of this guide is the detail that decides whether the setup still works three years from now. It goes one level deeper than our overview of how to replace Google in your life and our guide to exporting all your data from Google and Apple and verifying it. For the tunnel itself, keep our WireGuard setup guide open, and for the backup discs, read why offline backups on removable media still matter.

Key takeaways

  • Self-hosting moves patching, backups, uptime and account recovery from a provider to you. Budget an hour a month plus a quarterly restore test.
  • One Linux box with 8 GB of RAM or more, an SSD for the system and databases, and a mirrored pair of data disks covers a family. Immich's own minimum is 6 GB of RAM and 2 cores, with 8 GB and 4 cores recommended.
  • Encrypt the data disks with LUKS2 so a stolen or failed drive does not leak your library. Back up the LUKS header.
  • Nextcloud All-in-One (AIO), the official installer, runs a management container that starts, updates and backs up the rest.
  • Calendars and contacts travel over two open standards, CalDAV (RFC 4791, March 2007) and CardDAV (RFC 6352, August 2011), so the phone side needs no extra app on iPhone and one free app on Android.
  • Immich installs from a release-matched compose file. Its app backs up photos on Wi-Fi by default, and immich-go imports Takeout and iCloud exports with dates and albums.
  • Keep the AIO interface (port 8080) and the Immich port (2283) off the internet. Docker publishes ports around host firewalls such as ufw, so bind them to localhost or a LAN address.
  • Immich makes daily database dumps but not file backups. Nextcloud AIO backs up itself only if you turn it on. Neither replaces an offline copy.
  • You give up iCloud's optional end to end encryption for photos, easy sharing outside your server, and a provider's redundancy.

What you are taking on when you self-host

A hosted provider patches the servers, keeps copies of your data in several places, keeps the service up and lets you back in when you forget a password. Self-hosting makes each of those your job, so set it up to keep the jobs small and routine.

JobGoogle or iCloudYour own server
Patching the appsProvider, invisible to youYou pull new images after reading release notes
Copies of your dataProvider replicates across its sitesYour 3-2-1 plan: local copy, second disk, one offline and offsite
Restore when something breaksProvider support, trash foldersYour tested restore runbook
UptimeProvider's operations teamOne box, one power supply, one home connection
Account recoveryProvider recovery flowsYour admin account, your password manager, your written recovery notes
Encryption keysProvider holds most keys unless you opt into end to end encryptionYou hold disk keys, backup passwords and app passwords

The honest time budget, once everything below is in place, looks like this:

  • Weekly, five minutes. Read the update notifications. Nextcloud AIO sends a "Container updates available!" notification to admins on Saturdays when new images exist.
  • Monthly, thirty to sixty minutes. Read release notes, take a backup, update both stacks and the host, and check that the backup job reported success.
  • Quarterly, one to two hours. Restore both stacks onto a spare machine or virtual machine from the offline copy and check a sample of photos, files and calendar entries.
  • Yearly. Check disk health and re-read your recovery notes as if you were someone else in the family.

If that budget is more than you want to carry, a middle path is to self-host only photos, the largest and most personal data set, and keep calendar and files with a provider. Every section below stands on its own.

The stack this guide builds

PieceJobWhere it runs
Ubuntu or Debian server, 64-bitHost OS with distribution security updatesSmall server, mini PC or NAS that can run Docker
LUKS2 on the data disksEncryption at restHost
Docker Engine with the Compose pluginRuns both stacksHost
Nextcloud All-in-OneFiles, sync clients, Calendar, Contacts, optional OfficeContainers, data on the encrypted disk
ImmichPhoto and video backup, timeline, faces, map, searchContainers, library on the encrypted disk, database on SSD
WireGuardPrivate remote access for your own devicesHost kernel module, one UDP port
CaddyReverse proxy with automatic TLS certificatesHost, ports 443 (and 80 if you use the HTTP challenge)
BorgBackupDeduplicated, encrypted backupsAIO's built in backup plus a host job for Immich

Lighter standalone CalDAV servers and other photo servers exist; the same pattern applies to them.

Hardware and OS: a small server or NAS, disk layout, encryption at rest

Choosing the box

You need a 64-bit machine that runs Linux and Docker well. Immich's requirements page recommends a 64-bit Linux system, warns that non-Linux systems "tend to provide a poor Docker experience", and does not recommend running Docker inside LXC containers. It asks for at least 6 GB of RAM (8 GB recommended) and 2 CPU cores (4 recommended), on amd64 or arm64. Since Immich v3, the machine learning container on amd64 needs the x86-64-v2 microarchitecture level, which the docs say most CPUs from about 2012 onward support.

CategoryGood forWatch out for
Reused desktop or laptopTrying it out, low costPower draw, a single drive bay, no ECC memory
Small form factor mini PCQuiet always-on server for a familyUsually one or two drive slots, so data disks go in a USB enclosure
NAS appliance that runs containersSeveral drive bays, hot swap, vendor RAID toolsOlder Docker versions, vendor specific paths; AIO documents Synology DSM, Unraid and TrueNAS SCALE separately
Small tower serverFour or more bays, room for a GPU for faster face and search indexingNoise, power, cost

A GPU is optional; Immich can use one for machine learning and transcoding, but everything runs on the CPU.

Sizing worked example

Say a family has 400 GB of photos and videos across two phones, 150 GB of documents, and a few thousand calendar events and contacts.

  • Photos. Immich's requirements page says thumbnails and transcoded video "can increase the size of the photo library by 10-20% on average". So plan 400 GB × 1.2 = 480 GB.
  • Immich database. Postgres files are "typically between 1-3 GB" per the same page, and should sit on local SSD storage, never a network share.
  • Files. 150 GB, plus versions and trash. Plan 200 GB.
  • Calendar and contacts. Megabytes, stored in Nextcloud's database.
  • Growth. Phones shoot more every year. Double the total for five years of headroom: about 1.4 TB.

A mirrored pair of 4 TB disks gives room for growth and for local backup snapshots. A 500 GB to 1 TB SSD holds the OS, Docker, both databases and Immich's machine learning model cache.

Disk layout

DeviceContentsWhy
SSDOS, Docker images and volumes, Immich DB_DATA_LOCATION, Nextcloud AIO's database volumeDatabases need low latency; Immich forbids network shares for its database
Data disk pair, mirroredNextcloud data directory, Immich UPLOAD_LOCATIONLarge, sequential, survives one disk failure
External backup disk ALocal Borg repositoriesFast restores
External backup disk B (rotated offsite)Offline copy of the same repositoriesSurvives ransomware, theft, fire and the server itself

A mirror is not a backup: it copies a deletion or ransomware to both disks in the same second.

Encryption at rest with LUKS2

Cryptsetup's README calls LUKS "the standard for Linux disk encryption" and notes that its setup information lives in the partition header. An encrypted disk returned under warranty or stolen with the box is unreadable without the passphrase.

Warning: the commands below destroy all data on the target device. Run the device check first, every time, and confirm the model and serial number match the disk you mean. Device names such as /dev/sdb can change between boots, so use /dev/disk/by-id/ paths.

Step 1: identify the disks.

lsblk -o NAME,SIZE,MODEL,SERIAL,MOUNTPOINTS
ls -l /dev/disk/by-id/ | grep -v -- -part

Step 2: build the mirror (skip this for a single disk). Check both paths against the output above before you press Enter.

DISK1=/dev/disk/by-id/ata-EXAMPLE_MODEL_SERIAL1
DISK2=/dev/disk/by-id/ata-EXAMPLE_MODEL_SERIAL2
sudo mdadm --create /dev/md0 --level=1 --raid-devices=2 "$DISK1" "$DISK2"
cat /proc/mdstat

Step 3: put LUKS2 on the mirror, open it, and make a filesystem. Check again that /dev/md0 is the new mirror and holds nothing you need.

lsblk /dev/md0
sudo cryptsetup luksFormat --type luks2 /dev/md0
sudo cryptsetup open /dev/md0 data
sudo mkfs.ext4 -L data /dev/mapper/data
sudo mkdir -p /srv/data
sudo mount /dev/mapper/data /srv/data

Step 4: back up the LUKS header and keep it off the server with your passphrase. A damaged header with no backup loses the data even if you know the passphrase.

sudo cryptsetup luksHeaderBackup /dev/md0 --header-backup-file /root/data-luks-header.img

Step 5: decide how the disk unlocks after a reboot, because a headless box cannot type a passphrase. Unlocking by hand over SSH protects the whole stolen machine but leaves services down until you do it. A key file on an encrypted root disk, or a TPM bound key enrolled with systemd-cryptenroll, boots unattended but protects mainly a single pulled disk. With manual unlock, leave both Compose stacks stopped at boot and start them from a small script after the disk is mounted.

OS and Docker

Install a long term support release of Ubuntu or Debian with automatic security updates, and Docker Engine from Docker's own repository. Nextcloud AIO does not support snap-based Docker and gives this check:

sudo docker info | grep "Docker Root Dir" | grep "/var/snap/docker/"

Immich's install page notes that the old docker.io package on Ubuntu 22.04.3 gives errors with its compose file, and that the command is docker compose, not the deprecated docker-compose.

Files and calendar: Nextcloud All-in-One with Docker Compose

Nextcloud's README calls All-in-One "the official Nextcloud installation method". A management container, the mastercontainer, starts Apache, Nextcloud, PostgreSQL, Redis and a push backend, plus optional containers such as Nextcloud Office and Talk. The README notes that many containers run as non-root with a read-only root filesystem on their own Docker network.

At the time of writing, Nextcloud's server repository shows 35.0.2, 34.0.5 and 33.0.10 all released on 15 October 2026, and the AIO repository's latest stable release is v14.4.1 from 21 October 2026. AIO decides which Nextcloud version you get, so you do not pin it yourself.

Step 1: write the Compose file

The AIO README suggests the example Compose file over docker run for production. This version assumes Caddy will run on the same host as the reverse proxy, so it removes the public ports 80 and 8443, binds the AIO interface to the server's LAN address, and puts the data directory on the encrypted disk. Replace 192.168.1.10 with your server's LAN address.

# /opt/nextcloud-aio/compose.yaml
name: nextcloud-aio
services:
  nextcloud-aio-mastercontainer:
    image: ghcr.io/nextcloud-releases/all-in-one:latest
    init: true
    restart: always
    container_name: nextcloud-aio-mastercontainer   # do not change
    volumes:
      - nextcloud_aio_mastercontainer:/mnt/docker-aio-config   # do not change
      - /var/run/docker.sock:/var/run/docker.sock:ro
    network_mode: bridge
    ports:
      - "192.168.1.10:8080:8080"    # AIO interface, LAN only
    environment:
      APACHE_PORT: 11000            # Nextcloud listens here for the reverse proxy
      APACHE_IP_BINDING: 127.0.0.1  # only the local reverse proxy can reach it
      NEXTCLOUD_DATADIR: /srv/data/nextcloud   # set before first start, never change later
volumes:
  nextcloud_aio_mastercontainer:
    name: nextcloud_aio_mastercontainer   # do not change

The "do not change" comments come from the project's Compose file, which also warns never to change NEXTCLOUD_DATADIR after the first installation.

The mastercontainer mounts the Docker socket read only to manage its siblings, a real privilege; AIO documents a manual install without it.

Step 2: start it and open the interface by IP address

sudo mkdir -p /srv/data/nextcloud
cd /opt/nextcloud-aio
sudo docker compose up -d
sudo docker compose logs -f

Open https://192.168.1.10:8080 and accept the self-signed certificate. The README asks you to use an IP address on this port, not a domain, because HSTS can block access later. The page shows a generated passphrase. Save it in your password manager: it is the key to the whole instance's management interface.

Step 3: domain, TLS and the reverse proxy

AIO asks for the domain Nextcloud will live on, for example cloud.example.com, and validates it. Point that name at your server and put Caddy in front. The AIO reverse proxy document recommends Caddy and gives this Caddyfile shape:

https://cloud.example.com:443 {
    reverse_proxy localhost:11000
}

Caddy's documentation says it serves public DNS names over HTTPS with certificates from a public ACME certificate authority, renews them, and redirects HTTP on port 80 to HTTPS on port 443. Set the DNS record before you start Caddy so it can get the certificate. If you will only reach the server through WireGuard (see remote access below), use the ACME DNS challenge instead; the AIO reverse proxy document has a Caddy DNS challenge variant for that case.

Without a domain, the AIO interface can register a free dedyn.io subdomain through deSEC. Then choose optional containers (each one is more to patch), set the time zone, start the containers, and log in with the initial admin credentials the interface gives you.

Step 4: turn on Calendar and Contacts

As the admin, enable the Calendar and Contacts apps. The manual notes that from Nextcloud 30 the calendar server settings stay hidden until an app uses CalDAV. Under Groupware, choose whether to send invitations and reminders, which need outgoing email configured. Then create a normal account per family member and stop using the admin account day to day.

How CalDAV and CardDAV work, and why they matter here

CalDAV, defined in RFC 4791 (March 2007), extends WebDAV with "a standard way of accessing, managing, and sharing calendaring and scheduling information based on the iCalendar format". CardDAV, RFC 6352 (August 2011), does the same for contacts in vCard format. RFC 6764 (February 2013) adds discovery: a client can find the right endpoint from DNS SRV and TXT records or from the well known URIs /.well-known/caldav and /.well-known/carddav.

iOS and macOS speak both natively, Android needs one sync adapter, and your data stays in the same iCalendar and vCard formats the provider exports give you.

Nextcloud's troubleshooting guide says clients, especially on iOS and macOS, depend on those two well known URLs redirecting to remote.php/dav. AIO's bundled Apache handles that. If you put a different proxy in front, test both:

curl -sI https://cloud.example.com/.well-known/caldav | grep -i '^location'
curl -sI https://cloud.example.com/.well-known/carddav | grep -i '^location'

Each should answer with a redirect that points at /remote.php/dav/.

Step 5: connect phones and computers

First create an app password for each device: in Nextcloud, open Settings, then Security, then Devices and sessions, type a name such as "iPhone" and keep the login and password it shows. The Android guide warns that with two-factor authentication turned on, the normal password fails with a generic "Unknown resource" error, so app passwords are the right habit anyway. Each device gets its own, and you can revoke one lost phone without touching the others.

iPhone and iPad. Nextcloud's iOS guide walks through Settings, then Apps, then Calendar, then Calendar Accounts, then Add Account, then Other, then Add CalDAV account. Enter your domain as the server and the app login and password. In Advanced Settings, set the server path to cloud.example.com/remote.php/dav/principals/users/USERNAME/, keep SSL on and use port 443. The guide notes that from iOS 12 SSL is required, which is one more reason the server needs a real certificate. Repeat with a CardDAV account under Contacts.

Android. Nextcloud's Android guide uses DAVx⁵, an open source sync adapter available from Google Play and F-Droid. With the Nextcloud app installed, choose "Sync calendars & contacts" in its settings and DAVx⁵ completes the login. Without it, add a DAVx⁵ account with the base URL https://cloud.example.com/remote.php/dav, your username and an app password, and set the contact group method to "Groups are per-contact categories". The guide says to use your email address as the DAVx⁵ account name if you want to send invitations.

Desktop. Install the Nextcloud desktop client for files; macOS Calendar and Contacts take the same accounts as iOS.

Step 6: import calendars and contacts from Google and iCloud

Get the data out first.

  • Google. Google Takeout lets you pick products, a one-time archive or scheduled exports every 2 months for a year, .zip or .tgz files, and a maximum archive size. Archives larger than that are split, and Google's help page suggests the 50 GB limit to reduce splitting. It warns that archives expire in about 7 days and each can be downloaded only 5 times. Google says it exports contacts as vCard. Calendar comes as iCalendar files.
  • Apple. On Apple's Data and Privacy page, choose "Request a copy of your data". Apple's support article says contacts, calendars, bookmarks and mail come in formats such as .vcf, .ics, .html and .eml, photos, videos and documents in their original formats, and you have 14 days to download once it is ready. Access to the feature varies by country and region.

Then import:

  1. In Nextcloud Calendar, open Calendar settings, choose Import, and pick each .ics file. Import each source calendar into its own new calendar so you can check counts side by side.
  2. In Nextcloud Contacts, open the settings and import the .vcf file into an address book.
  3. Compare counts. A calendar that held 3,412 events in the source should show the same number of events in the new calendar for the same date range. Spot check recurring events, all day events and events in other time zones, which are where imports most often go wrong.
  4. Only then remove the old account's calendars from the phones.

Step 7: move your files

Unpack the Drive part of your Takeout or the iCloud Drive part of Apple's export into the desktop client's sync folder and let the first sync finish on a wired connection. Clients upload in chunks, so they handle files far larger than the 10 GB default for public link uploads. Google converts its own document formats on export, so check a few complex spreadsheets before deleting anything at the source.

Encryption options inside Nextcloud

Nextcloud's encryption guide is unusually direct about what each option protects against. Its own comparison:

MethodWhere it encryptsWho can decryptProtects against
Server-side encryption, master keyServerAdmins and usersExternal storage providers
Server-side encryption, user keysServerUsers (and admins with a recovery key, if set)External storage providers
End to end encryptionDesktop or mobile clientUsers onlyAdmins, external storage providers
Disk or block device encryption (LUKS)Server OSOS adminPhysical tampering, theft

The guide adds that server-side encryption leaves filenames unencrypted, does not protect against a compromised server, and that losing the keys "results in permanent data loss". The user manual says that without remote storage, file-level or whole disk encryption is the better choice. So LUKS is the default here; keep client side end to end encryption for the few folders even the admin must not read.

Photos: installing Immich, mobile backup, faces and maps, and importing your library

Immich is an open source photo and video server with mobile apps for iOS and Android. At the time of writing its latest release is v3.3.5, published 21 October 2026, following v3.3.0 on 7 October.

Step 1: get the release-matched files

The install guide is explicit: use the docker-compose.yml and example.env attached to the latest release, because the compose file on the main branch may not match.

sudo mkdir -p /opt/immich-app && cd /opt/immich-app
sudo wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
sudo wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env

The file runs four services: immich-server on port 2283, machine learning, a Valkey cache and Postgres with VectorChord, the last two pinned by digest.

Step 2: fill in .env

# /opt/immich-app/.env
UPLOAD_LOCATION=/srv/data/immich
DB_DATA_LOCATION=/var/lib/immich-postgres   # local SSD, never a network share
TZ=Europe/London
IMMICH_VERSION=v3.3.5                       # pin an exact release; see the patching section
DB_PASSWORD=ReplaceWithLongRandomAlnum      # letters and digits only
DB_USERNAME=postgres
DB_DATABASE_NAME=immich

The example ships IMMICH_VERSION=v3, which follows the current major; pinning an exact release means you update when you choose. Use only letters and digits in the database password, as the install page asks.

Step 3: keep the port private, then start

The release compose file publishes 2283:2283 on every interface. Change that one line so only the local reverse proxy can reach it:

    ports:
      - '127.0.0.1:2283:2283'

Then start the stack:

cd /opt/immich-app
sudo docker compose up -d
sudo docker compose ps

Until Caddy is ready, bind to the LAN address instead.

Step 4: first user, storage template and family accounts

Immich's post-install steps say the first user to register becomes the admin. Register yourself as admin from a computer on your network, then create one ordinary account per family member under Administration, Users. Decide before the big import whether to turn on the storage template (Administration, Settings, Storage Template), which files originals by a pattern such as year and month under UPLOAD_LOCATION/library/.

Add the Immich site to the same Caddyfile. Immich's reverse proxy page says it must be served at the root of a domain or subdomain, not a sub-path, and gives this Caddy example shape:

photos.example.com {
    reverse_proxy localhost:2283
}

If you use the ACME HTTP challenge, Immich asks you to check that /.well-known/immich still reaches Immich, or the mobile app may fail to connect.

Step 5: mobile backup

Install the Immich app from the app store, enter https://photos.example.com, and sign in. Then, from Immich's mobile backup page:

  • Pick albums. You choose which phone albums back up and can exclude some, which matters on iOS where one photo can be in several albums.
  • Deduplication. The app checksums each file and skips anything already on the server, whether it came from the web, the CLI or another device. That is what makes a later import of an old Takeout safe to combine with phone backup.
  • Wi-Fi only by default. Change it in backup settings if you want uploads over mobile data.
  • Background limits. Some Android models kill background work; Immich links to fixes. On iOS, turn on Background App Refresh, and know that iOS decides when uploads run.

Immich's FAQ explains the app's cloud icons. Turn off the provider's photo sync only when every photo shows as backed up.

Faces, maps and search, all on your hardware

Immich's facial recognition page describes the flow. The server sends a preview image to the machine learning container, which runs a face detection model, crops each face, and runs a recognition model that returns embeddings. The server stores and indexes those embeddings and groups them into people with a clustering algorithm derived from DBSCAN. You can name, merge, hide and favourite people.

Maps use local reverse geocoding. The docs say Immich loads data from the GeoNames geographic database into Postgres on each minor version upgrade, so turning a photo's latitude and longitude into a city, state and country is a query on your own server. The results feed Smart Search as well as the photo details.

The first full index of a large library takes hours on a CPU. With only 4 GB of RAM, Immich can run with machine learning off, losing faces and smart search.

Step 6: import Google Photos from Takeout

Takeout puts dates, descriptions and locations in JSON sidecar files, so uploading images alone often loses dates. The open source immich-go tool handles this. Its README says the from-google-photos command "intelligently matches photos with their JSON metadata to preserve albums, descriptions, and locations", handles collections over 100,000 photos, detects duplicates and stacks bursts and RAW plus JPEG pairs. It also warns that it is an early version and to keep a backup of your files.

  1. In Takeout, select only Google Photos, choose .zip, and pick the 50 GB archive size.
  2. Download every part before the roughly 7 day expiry. Check that the number of parts matches the email.
  3. In Immich, open Account Settings, API Keys, and create a key for the import with the permissions listed in immich-go's installation docs. Revoke it when the import is done.
  4. Run the import from a computer on your network, against the archives in place:
immich-go upload from-google-photos \
  --server=http://192.168.1.10:2283 \
  --api-key=YOUR_TEMPORARY_KEY \
  /path/to/takeout-*.zip
  1. Compare item counts. Small differences from skipped duplicates are normal; large ones are not.

Step 7: import from iCloud Photos

Apple's data export provides photos and videos in their original formats. immich-go's from-icloud command handles that export, using the CSV files Apple includes to get creation dates and album structure right. Run it the same way as the Google import, against the unpacked export folder.

Photos you already keep in folders

Immich's external libraries index folders in place. The docs warn that metadata added in Immich is not written back to the files and is lost if a file moves, so use them for a read only archive you back up separately.

Remote access safely: WireGuard or a reverse proxy with TLS

You need your phone to reach the server from outside the house, at least for backup and calendar sync. There are four common patterns:

OptionWhat is exposed to the internetGood forMain risk
LAN onlyNothingPhoto backup that waits until you are homeNo calendar sync while away
WireGuard VPNOne UDP port, accepting only packets from peers whose keys you addedFamilies whose devices can all run the WireGuard appEach new device needs a key and config
Reverse proxy with TLSPort 443 (and 80 for the HTTP challenge) for the two app domainsSharing links with people outside the family, devices without VPN appsEvery app bug is reachable from the internet; patching becomes urgent
Third party tunnel or relay serviceNothing on your routerConnections behind carrier-grade NATThe relay may terminate TLS and see your traffic

On the fourth row, the AIO README warns that one popular tunnel service decrypts all traffic on its own infrastructure, calling that "a privacy concern". For most households, WireGuard for everything, plus a reverse proxy only if you truly need public share links, is the right balance.

WireGuard: one silent port

WireGuard's quick start shows key generation with its wg tool. On the server:

sudo apt install wireguard
umask 077
wg genkey | tee server.key | wg pubkey > server.pub
wg genkey | tee phone.key | wg pubkey > phone.pub

Server config, using a private tunnel subnet:

# /etc/wireguard/wg0.conf on the server
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = SERVER_PRIVATE_KEY

[Peer]
# phone
PublicKey = PHONE_PUBLIC_KEY
AllowedIPs = 10.8.0.2/32

Phone config, which the WireGuard app can import from a QR code:

[Interface]
Address = 10.8.0.2/32
PrivateKey = PHONE_PRIVATE_KEY

[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = home.example.com:51820
AllowedIPs = 10.8.0.1/32
PersistentKeepalive = 25
sudo systemctl enable --now wg-quick@wg0
sudo wg show

Forward UDP 51820 on your router to the server. AllowedIPs = 10.8.0.1/32 on the phone sends only server traffic through the tunnel, which keeps everything else on the phone's normal connection. WireGuard's docs explain that it is deliberately quiet and that a peer behind NAT may need persistent keepalives; they call 25 seconds "a sensible interval that works with a wide variety of firewalls".

To keep using real domain names and certificates through the tunnel, create public DNS A records for cloud.example.com and photos.example.com that point at 10.8.0.1, and have Caddy obtain certificates with the DNS challenge. The names resolve anywhere, but only answer inside the tunnel. iOS CalDAV gets the valid certificate it requires, and nothing but the WireGuard port is open on your router. For a deeper walkthrough, including multiple peers and routing, see our WireGuard guide.

Reverse proxy with TLS, if you need public links

If outsiders must open shared links, publish only port 443 (and 80 for the HTTP challenge) to Caddy for the two app domains, turn on two-factor authentication for every account, keep the admin ports on localhost or the LAN, treat security updates as urgent, and consider the Fail2Ban add-on the AIO README documents.

The Docker and firewall trap

Docker's own documentation states that Docker and ufw "use firewall rules in ways that make them incompatible with each other": published container ports are handled in the nat table before ufw's INPUT and OUTPUT chains, "effectively ignoring your firewall configuration". A ufw deny 2283 does nothing for a container published on 0.0.0.0:2283.

So the binding is the control. Check what is actually listening:

sudo ss -tlnp | grep -E ':(80|443|2283|8080|8443|11000)\b'
sudo docker ps --format '{{.Names}}\t{{.Ports}}'

You want 127.0.0.1:2283, 127.0.0.1:11000, your LAN IP on 8080, and nothing else from Docker on a public interface. Then test from outside your network, for example from a phone on mobile data with the VPN off: the AIO interface and Immich's port must not answer.

Never expose admin panels

The AIO interface on 8080 can stop, update and restore everything, so keep it on the LAN and reach it over WireGuard. The README adds that while Nextcloud runs, direct AIO login stays blocked until an admin unlocks it from Nextcloud's admin overview. Any database tool you add gets the same rule.

Backups: 3-2-1, an offline copy and a restore test

Immich's backup page recommends a 3-2-1 strategy, meaning three copies of the data, on two kinds of media, with one copy offsite. CISA's #StopRansomware guide adds the two words that matter most for a home server: keep backups "offline, encrypted", and regularly test their availability and integrity in a recovery scenario, because "many ransomware variants attempt to find and subsequently delete or encrypt accessible backups".

What to back up

DataWhere it livesHow to capture itCritical?
Nextcloud files, database, configAIO volumes and /srv/data/nextcloudAIO built in Borg backupYes
Calendars and contactsNextcloud databaseIncluded in the AIO backupYes
Immich originalsUPLOAD_LOCATION/upload, /library, /profileHost Borg jobYes
Immich databasePostgres, plus Immich's daily dumps in UPLOAD_LOCATION/backupsImmich's dumps, copied by the host Borg jobYes
Immich thumbnails and transcodesUPLOAD_LOCATION/thumbs, /encoded-videoOptional; can be regeneratedNo
Compose files, .env, Caddyfile, WireGuard configs/opt, /etcHost Borg jobYes, small and painful to lose
LUKS header backup, passphrases, AIO passphrase, Borg passwordsPassword managerPrinted recovery sheet in a safe placeYes

Nextcloud AIO: turn on the built in backup

In the AIO interface, set a backup location on the local backup disk, for example /mnt/backup-a/nextcloud-aio, and create the first backup. AIO shows the Borg encryption password once; store it. The README says you need only the archive and that password to restore the whole instance on a new AIO install. It also supports:

  • A remote Borg repository, which avoids a local copy and supports append-only keys "to counter ransomware".
  • Integrity checks and repair from the interface.
  • A default retention of 7 days of everything, 4 weekly and 6 monthly archives, adjustable with the BORG_RETENTION_POLICY variable.

Immich: database dumps plus a file backup you run

Immich's backup page says the database holds file paths and metadata, Immich does not rescan the library folder to rebuild it, and so database backups are essential. Immich creates automatic dumps in UPLOAD_LOCATION/backups, by default daily at 2:00 AM keeping the last 14, adjustable under Administration, Settings, Database Dump Settings. The page also says, in bold, that these dumps do not contain photos or videos, and that "Immich does not handle filesystem backups for you".

So run a host job after 2:00 AM that captures the originals and the dumps together. BorgBackup's quick start begins by initialising a repository with repokey encryption, which keeps the key inside the repository protected by your passphrase. Export a copy of that key anyway:

# one time: create an encrypted repository on backup disk A
sudo borg init --encryption=repokey /mnt/backup-a/borg-immich
sudo borg key export /mnt/backup-a/borg-immich /root/borg-immich.key   # store this off the server too
#!/bin/sh
# /usr/local/sbin/backup-immich.sh, run daily at 03:30 from cron or a systemd timer
set -eu
export BORG_REPO=/mnt/backup-a/borg-immich
export BORG_PASSCOMMAND="cat /root/.borg-immich-pass"
U=/srv/data/immich
borg create --stats --compression zstd ::'immich-{now:%Y-%m-%d}' \
  "$U/upload" "$U/library" "$U/profile" "$U/backups" \
  /opt/immich-app /opt/nextcloud-aio /etc/caddy /etc/wireguard
borg prune --keep-daily 7 --keep-weekly 4 --keep-monthly 6
borg check --repository-only

Borg deduplicates, so after the first run each night stores only new photos.

The offline and offsite copy

Two external disks, B1 and B2, take turns. Once a week or once a month, plug in the one at home, copy both repositories to it, unplug it, and swap it with the one kept at a relative's house or your office. A disk that is unplugged cannot be encrypted by ransomware on the server, wiped by a bad script, or lost in the same fire. Our post on why offline backups on removable media still matter covers media choice and rotation in more depth.

# check the device before writing to it: model and serial must match backup disk B
lsblk -o NAME,SIZE,MODEL,SERIAL,MOUNTPOINTS
sudo cryptsetup open /dev/disk/by-id/usb-EXAMPLE_B1_SERIAL backupb
sudo mount /dev/mapper/backupb /mnt/backup-b
sudo rsync -a --delete /mnt/backup-a/ /mnt/backup-b/
sudo umount /mnt/backup-b && sudo cryptsetup close backupb

The delete option makes the target match the source, so check both paths first. The AIO README says to lock its Borg archive before syncing it (see its "aio-lockfile" example), so copy only when no AIO backup is running.

The restore test

A backup you have never restored is a hope, not a backup. Once a quarter, on a spare machine or a virtual machine with no access to your real server:

  1. Install Docker and copy the Compose files and .env from the backup.
  2. Nextcloud. Start a fresh AIO, choose restore in its interface, point it at a copy of the Borg repository and enter the password. When it finishes, log in, open three random files, and check that a calendar event from last week is present.
  3. Immich. Extract the latest Borg archive so the upload, library, profile and backups folders sit under the new UPLOAD_LOCATION. Start Immich and choose "Restore from backup" on the welcome screen. Immich's backup page says it shows integrity checks for the storage folders, lets you pick a dump, creates a restore point first, runs migrations if needed and rolls back on failure. After the restore, run the thumbnail and transcode jobs if you did not back up those folders.
  4. Check and write down. Compare counts, open twenty random photos, and note how long it took. That note is your real recovery plan.

Immich warns that a dump from a different version may need migrations, and that its backup and restore process changed in v2.5.0, so restore with the version that made the dump when you can.

Keeping the stack patched: image tags, update notifications and release notes

Both projects ship fixes often. Two dated examples from their own release and advisory pages show why the monthly routine matters:

DateEventSource
March 2007CalDAV published as RFC 4791RFC Editor
August 2011CardDAV published as RFC 6352RFC Editor
February 2013CalDAV and CardDAV service discovery, RFC 6764RFC Editor
28 September 2026Immich publishes a high severity advisory (GHSA-q89f-h332-8q2h, CVE-2026-105764) for authenticated image uploads; it lists server v3.2.2 as affected and v3.2.4 as patchedImmich security advisories
7 October 2026Immich v3.3.0 releasedImmich releases
15 October 2026Nextcloud releases 35.0.2, 34.0.5 and 33.0.10 on the same dayNextcloud server releases
19 October 2026Immich publishes a medium severity advisory (GHSA-72rm-x7xr-v679) for an unauthenticated path handling flaw, listing versions up to 3.0.3 as affected and v3.3.0 as patchedImmich security advisories
21 October 2026Immich v3.3.5 and Nextcloud AIO v14.4.1 releasedProject releases

The lesson: a server running last spring's image has published, fixed vulnerabilities, and only you will update it.

Immich: pin, read, pull

Immich follows semantic versioning and aims to keep breaking changes to major releases. Its upgrade page sets the rules you need to know:

  • The project does not backport patches to earlier versions and encourages everyone to run the latest stable release.
  • Downgrading, even within the same minor version, is not supported. Your backup is your rollback.
  • The mobile app usually works with the current and previous major version, but the server works only with its own major version, so update phones before a major server upgrade.

After a backup:

cd /opt/immich-app
sudo sed -i 's/^IMMICH_VERSION=.*/IMMICH_VERSION=v3.3.5/' .env   # the release you just read
sudo docker compose pull && sudo docker compose up -d
sudo docker image prune

For a major version, also compare the new release's compose file with yours.

Nextcloud AIO: let the mastercontainer do it, after a backup

In the AIO interface, press "Stop containers", take a backup, apply any mastercontainer update offered, then "Start and update containers".

The README describes AIO's update policy: it values stability over new features, waits at least for the first patch release of a new Nextcloud major (its example is 24.0.1) and for important apps to be compatible, and tests other updates for up to about two weeks before they reach users. It also sends an "AIO is outdated!" notification every day once the Nextcloud container image is more than 90 days old, so update at least every three months even if nothing else prompts you.

AIO's daily backup option can also update everything automatically after each successful backup, a reasonable choice for a WireGuard only server.

Get told, do not go looking

  • On GitHub, watch the immich-app/immich and nextcloud/all-in-one repositories for releases, and the Immich and Nextcloud security advisory pages.
  • Turn on your distribution's automatic security updates for the host, and reboot when the kernel updates.
  • Update your router's firmware too.

A monthly checklist that fits on a sticky note:

1. Read Immich and Nextcloud AIO release notes since last month.
2. Confirm last night's AIO backup and Immich Borg job succeeded.
3. AIO: stop containers, back up, update mastercontainer if offered, start and update.
4. Immich: bump IMMICH_VERSION, pull, up -d, prune.
5. Host: apt upgrade, reboot if kernel changed, unlock disk, start stacks.
6. From mobile data with the VPN off: ports 8080 and 2283 must not answer.

What you lose versus Google and iCloud

AreaGoogle or iCloudSelf-hosted Nextcloud and Immich
End to end encryption of photosiCloud with Advanced Data Protection: Apple says end to end encryption then covers 25 data categories, including Photos and iCloud BackupImmich's server must read photos to make thumbnails, faces and search; protection is LUKS at rest and TLS in transit, not end to end
Calendars and contacts encryptionApple's security overview lists Calendars and Contacts as encrypted in transit and on server with Apple holding the keys, even with Advanced Data ProtectionSame model, but you hold the keys and the server
Sharing with people outsidePolished links, shared albums, people with accounts everywherePublic share links work, but need a publicly reachable server, which raises your patching burden
Search and recognitionLarge models tuned on huge data setsFaces, map and smart search run locally and work well, with slower first indexing and fewer features
DurabilityReplicated across a provider's sitesExactly as good as your 3-2-1 plan and your last restore test
UptimeA provider's operations teamYour power, your internet line, your holidays
Account recoveryProvider recovery flows; with ADP, only your recovery contact or keyOnly your written notes and passwords

Apple says Advanced Data Protection needs iOS 16.2, iPadOS 16.2 or macOS 13.1 or later, and that Apple cannot help recover data if you lose access with it on.

Where Safeguard fits, and where it does not

Safeguard is a software supply chain security platform. It does not manage home servers, back up photos or protect endpoints. The honest link is that this setup is a stack of open source container images, and knowing which known vulnerabilities sit in the exact images you run is the part Safeguard covers. The Safeguard CLI can scan a container image by name (safeguard scan --image followed by the image reference) and write results as JSON under ./output, which you can run against the pinned tags in your Compose files before an update.

A rebuilt home server pulls every image and dependency again, and a self-hosted photo or file server is nothing but open source components stacked in containers, so the same supply chain hygiene that matters for a company's software applies here at small scale. The fit is indirect for a family server, where you run images others build, and direct for anyone who builds their own images, scripts or apps on top of these stacks.

Fix it without waiting for upstream

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.

3. Gold Registry: start from components that are already fixed

registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

None of these demos install Nextcloud or Immich or touch your photos. They show the software side: checking and fixing the open source components in projects and images you build or run.

This is a real Safeguard CLI session: install, open the interactive terminal, ask it to fix a lodash CVE, and approve each command as it edits package.json and runs npm install and npm test, with nothing pushed. If you build anything on top of your home server, such as a small web app, an import script or a custom image, the same approve-each-step flow is how you would find and fix a vulnerable dependency in it.

Click through it at your own pace, or open it full screen.

This demo sets up the Safeguard runner on your own machine, approves it with a device code, and runs a scan from the terminal. On a self-hosted server, the runner is a way to scan the container images named in your Compose files from the machine that runs them, so you can see known vulnerabilities in the tags you pinned before and after each update.

Click through it at your own pace, or open it full screen.

This is a real Android session: send work to a computer running the Safeguard CLI, then approve the command on the phone with Allow once, Allow for turn or Deny. For someone who runs a home server and is often away from it, that is a way to start a scan or a fix from the phone while every command still needs your explicit approval.

Click through it at your own pace, or open it full screen.

To try it on your own projects, download Safeguard or read the CLI documentation.

Common mistakes

  • Treating the mirror as the backup. RAID 1 survives a dead disk. It does not survive a deletion, a bad upgrade or ransomware. Keep the offline copy.
  • Backing up Immich's database dumps and calling it done. The dumps hold metadata only. Without the upload, library and profile folders there are no photos to restore.
  • Backing up the files without the database. Immich does not rescan its library to rebuild the database, so files alone come back as a pile without albums, faces or people.
  • Relying on ufw to hide container ports. Docker publishes around it. Bind ports to 127.0.0.1 or the LAN address and test from outside.
  • Exposing the AIO interface or port 2283 to the internet. Reach them over WireGuard.
  • Leaving IMMICH_VERSION=v3 and updating whenever. Pin a release, read its notes, update on purpose, and keep a backup because downgrades are not supported.
  • Updating the server's major version before the phones. The server only works with apps of its own major version.
  • Losing the AIO passphrase, the Borg passwords or the LUKS header. Each one alone can make a perfect backup useless. Keep them in a password manager and on a printed recovery sheet.
  • Uploading Google Photos without the JSON sidecars. Dates and locations go missing. Use a tool that reads them.
  • Turning off the provider before the counts match. Keep Google or iCloud running until imports, phone backups and a restore test all check out.

FAQ

Can I replace Google Photos with a self-hosted server?

Yes. Immich gives you phone backup, albums, shared links, faces, a map and smart search on your own hardware. Plan for 6 GB of RAM or more, an SSD for its database, and your own file backups.

Can an iPhone sync calendar and contacts with a self-hosted server?

Yes, natively. iOS supports CalDAV and CardDAV accounts in Settings, under Apps, Calendar or Contacts, then Add Account and Other. Nextcloud's guide uses an app password and the server path remote.php/dav/principals/users/USERNAME/. The server needs a valid TLS certificate, because iOS requires SSL for these accounts.

How do I move Google Photos to a self-hosted server without losing the dates?

Export with Google Takeout and import with a tool that reads Takeout's JSON sidecar files. immich-go's from-google-photos command matches each photo to its JSON metadata to keep dates, albums, descriptions and locations. Download all archive parts within about 7 days, because Takeout archives expire.

Is Nextcloud end to end encrypted?

Not by default. Nextcloud offers server-side encryption, which mainly protects files on external storage and does not protect against a compromised server, and a client side end to end encryption option for chosen folders. For a home server on local disks, Nextcloud's own docs point to disk encryption such as LUKS as the better fit.

Is it safe to expose Nextcloud or Immich to the internet?

It can be done, but anything public must be patched fast after each advisory. For most households, WireGuard only access is safer. Never expose the AIO interface or the Immich port directly.

How much storage do I need to self-host my photos?

Take your current library size, add 10 to 20 percent for Immich's thumbnails and transcoded video, add 1 to 3 GB for its database, and then double the result for growth. A 400 GB library needs about 480 GB today and roughly 1 TB of headroom for the next few years, on a mirrored pair of disks.

How often should I update Immich and Nextcloud?

At least monthly, after reading release notes and taking a backup, and sooner when a security advisory affects your version. Immich does not backport fixes, and Nextcloud AIO sends a daily "AIO is outdated!" notice once the Nextcloud image is more than 90 days old.

How long do I have to download my Apple or Google export?

Apple gives you 14 days after it posts the data copy to your Data and Privacy page. Google Takeout archives expire in about 7 days, and each archive can be downloaded only 5 times, so start the download as soon as the email arrives.

Quick check

Your Immich backup job copies UPLOAD_LOCATION/backups every night. Is your photo library backed up?

No. Immich's automatic dumps in that folder contain only the database, meaning metadata. You also need the original files in the upload, library and profile folders, captured together with a recent dump.

You ran "ufw deny 2283" but Immich still answers from the internet. Why?

Docker handles published container ports in the nat table before ufw's rules apply, so ufw does not filter them. Bind the port to 127.0.0.1 or the LAN address in the compose file and test again from outside.

Sources

Last reviewed: 23 October 2026. Both projects release often; check the Immich and Nextcloud All-in-One release notes for the version you run before following any command here.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.