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.
| Job | Google or iCloud | Your own server |
|---|---|---|
| Patching the apps | Provider, invisible to you | You pull new images after reading release notes |
| Copies of your data | Provider replicates across its sites | Your 3-2-1 plan: local copy, second disk, one offline and offsite |
| Restore when something breaks | Provider support, trash folders | Your tested restore runbook |
| Uptime | Provider's operations team | One box, one power supply, one home connection |
| Account recovery | Provider recovery flows | Your admin account, your password manager, your written recovery notes |
| Encryption keys | Provider holds most keys unless you opt into end to end encryption | You 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
| Piece | Job | Where it runs |
|---|---|---|
| Ubuntu or Debian server, 64-bit | Host OS with distribution security updates | Small server, mini PC or NAS that can run Docker |
| LUKS2 on the data disks | Encryption at rest | Host |
| Docker Engine with the Compose plugin | Runs both stacks | Host |
| Nextcloud All-in-One | Files, sync clients, Calendar, Contacts, optional Office | Containers, data on the encrypted disk |
| Immich | Photo and video backup, timeline, faces, map, search | Containers, library on the encrypted disk, database on SSD |
| WireGuard | Private remote access for your own devices | Host kernel module, one UDP port |
| Caddy | Reverse proxy with automatic TLS certificates | Host, ports 443 (and 80 if you use the HTTP challenge) |
| BorgBackup | Deduplicated, encrypted backups | AIO'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.
| Category | Good for | Watch out for |
|---|---|---|
| Reused desktop or laptop | Trying it out, low cost | Power draw, a single drive bay, no ECC memory |
| Small form factor mini PC | Quiet always-on server for a family | Usually one or two drive slots, so data disks go in a USB enclosure |
| NAS appliance that runs containers | Several drive bays, hot swap, vendor RAID tools | Older Docker versions, vendor specific paths; AIO documents Synology DSM, Unraid and TrueNAS SCALE separately |
| Small tower server | Four or more bays, room for a GPU for faster face and search indexing | Noise, 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
| Device | Contents | Why |
|---|---|---|
| SSD | OS, Docker images and volumes, Immich DB_DATA_LOCATION, Nextcloud AIO's database volume | Databases need low latency; Immich forbids network shares for its database |
| Data disk pair, mirrored | Nextcloud data directory, Immich UPLOAD_LOCATION | Large, sequential, survives one disk failure |
| External backup disk A | Local Borg repositories | Fast restores |
| External backup disk B (rotated offsite) | Offline copy of the same repositories | Survives 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/sdbcan 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,
.zipor.tgzfiles, 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,.htmland.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:
- In Nextcloud Calendar, open Calendar settings, choose Import, and pick each
.icsfile. Import each source calendar into its own new calendar so you can check counts side by side. - In Nextcloud Contacts, open the settings and import the
.vcffile into an address book. - 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.
- 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:
| Method | Where it encrypts | Who can decrypt | Protects against |
|---|---|---|---|
| Server-side encryption, master key | Server | Admins and users | External storage providers |
| Server-side encryption, user keys | Server | Users (and admins with a recovery key, if set) | External storage providers |
| End to end encryption | Desktop or mobile client | Users only | Admins, external storage providers |
| Disk or block device encryption (LUKS) | Server OS | OS admin | Physical 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.
- In Takeout, select only Google Photos, choose
.zip, and pick the 50 GB archive size. - Download every part before the roughly 7 day expiry. Check that the number of parts matches the email.
- 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.
- 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
- 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:
| Option | What is exposed to the internet | Good for | Main risk |
|---|---|---|---|
| LAN only | Nothing | Photo backup that waits until you are home | No calendar sync while away |
| WireGuard VPN | One UDP port, accepting only packets from peers whose keys you added | Families whose devices can all run the WireGuard app | Each new device needs a key and config |
| Reverse proxy with TLS | Port 443 (and 80 for the HTTP challenge) for the two app domains | Sharing links with people outside the family, devices without VPN apps | Every app bug is reachable from the internet; patching becomes urgent |
| Third party tunnel or relay service | Nothing on your router | Connections behind carrier-grade NAT | The 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
| Data | Where it lives | How to capture it | Critical? |
|---|---|---|---|
| Nextcloud files, database, config | AIO volumes and /srv/data/nextcloud | AIO built in Borg backup | Yes |
| Calendars and contacts | Nextcloud database | Included in the AIO backup | Yes |
| Immich originals | UPLOAD_LOCATION/upload, /library, /profile | Host Borg job | Yes |
| Immich database | Postgres, plus Immich's daily dumps in UPLOAD_LOCATION/backups | Immich's dumps, copied by the host Borg job | Yes |
| Immich thumbnails and transcodes | UPLOAD_LOCATION/thumbs, /encoded-video | Optional; can be regenerated | No |
Compose files, .env, Caddyfile, WireGuard configs | /opt, /etc | Host Borg job | Yes, small and painful to lose |
| LUKS header backup, passphrases, AIO passphrase, Borg passwords | Password manager | Printed recovery sheet in a safe place | Yes |
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_POLICYvariable.
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:
- Install Docker and copy the Compose files and
.envfrom the backup. - 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.
- Immich. Extract the latest Borg archive so the
upload,library,profileandbackupsfolders sit under the newUPLOAD_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. - 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:
| Date | Event | Source |
|---|---|---|
| March 2007 | CalDAV published as RFC 4791 | RFC Editor |
| August 2011 | CardDAV published as RFC 6352 | RFC Editor |
| February 2013 | CalDAV and CardDAV service discovery, RFC 6764 | RFC Editor |
| 28 September 2026 | Immich 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 patched | Immich security advisories |
| 7 October 2026 | Immich v3.3.0 released | Immich releases |
| 15 October 2026 | Nextcloud releases 35.0.2, 34.0.5 and 33.0.10 on the same day | Nextcloud server releases |
| 19 October 2026 | Immich 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 patched | Immich security advisories |
| 21 October 2026 | Immich v3.3.5 and Nextcloud AIO v14.4.1 released | Project 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/immichandnextcloud/all-in-onerepositories 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
| Area | Google or iCloud | Self-hosted Nextcloud and Immich |
|---|---|---|
| End to end encryption of photos | iCloud with Advanced Data Protection: Apple says end to end encryption then covers 25 data categories, including Photos and iCloud Backup | Immich'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 encryption | Apple's security overview lists Calendars and Contacts as encrypted in transit and on server with Apple holding the keys, even with Advanced Data Protection | Same model, but you hold the keys and the server |
| Sharing with people outside | Polished links, shared albums, people with accounts everywhere | Public share links work, but need a publicly reachable server, which raises your patching burden |
| Search and recognition | Large models tuned on huge data sets | Faces, map and smart search run locally and work well, with slower first indexing and fewer features |
| Durability | Replicated across a provider's sites | Exactly as good as your 3-2-1 plan and your last restore test |
| Uptime | A provider's operations team | Your power, your internet line, your holidays |
| Account recovery | Provider recovery flows; with ADP, only your recovery contact or key | Only 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 situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull 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,libraryandprofilefolders 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.1or the LAN address and test from outside. - Exposing the AIO interface or port 2283 to the internet. Reach them over WireGuard.
- Leaving
IMMICH_VERSION=v3and 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
- Nextcloud All-in-One README, nextcloud/all-in-one on GitHub
- Nextcloud All-in-One example compose.yaml, GitHub
- Nextcloud All-in-One reverse proxy documentation, GitHub
- Nextcloud All-in-One releases, GitHub
- Nextcloud server releases, GitHub
- Nextcloud security advisories, GitHub
- Calendar / CalDAV, Nextcloud Administration Manual
- General troubleshooting: service discovery, Nextcloud Administration Manual
- Server-side encryption, Nextcloud Administration Manual
- Using server-side encryption, Nextcloud User Manual
- Synchronizing with Android, Nextcloud User Manual
- Synchronizing with iOS, Nextcloud User Manual
- Docker Compose install, Immich documentation
- Requirements, Immich documentation
- Post installation steps, Immich documentation
- Upgrading, Immich documentation
- Backup and restore, Immich documentation
- Reverse proxy, Immich documentation
- Mobile backup, Immich documentation
- Facial recognition, Immich documentation
- Reverse geocoding, Immich documentation
- External libraries, Immich documentation
- FAQ, Immich documentation
- Immich release docker-compose.yml and example.env, GitHub releases
- Immich security advisories, GitHub
- GHSA-72rm-x7xr-v679, Immich security advisory, GitHub
- GHSA-q89f-h332-8q2h, Immich security advisory, GitHub
- immich-go README, simulot/immich-go on GitHub
- RFC 4791: Calendaring Extensions to WebDAV (CalDAV), RFC Editor
- RFC 6352: vCard Extensions to WebDAV (CardDAV), RFC Editor
- RFC 6764: Locating Services for CalDAV and CardDAV, RFC Editor
- How to download your Google data, Google Account Help
- Get a copy of the data associated with your Apple Account, Apple Support
- iCloud data security overview, Apple Support
- How to turn on Advanced Data Protection for iCloud, Apple Support
- Quick Start, WireGuard
- WireGuard home page
- Automatic HTTPS, Caddy documentation
- Packet filtering and firewalls, Docker documentation
- docker compose pull, Docker documentation
- Cryptsetup README, cryptsetup on GitLab
- Quick Start, BorgBackup documentation
- #StopRansomware Guide, CISA
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.