Self-host your photos, files and passwords the safe way
Immich, Nextcloud and Vaultwarden on one small box at home, without the six classic traps: one disk for everything, a database on a network share, open signups, forwarded ports, a backup that isn't one, and updates that are never or always.

Your photos, your files and your passwords, running on a small box in your house. That is the whole promise of self-hosting your life: Immich instead of Google Photos, Nextcloud (or Syncthing) instead of Drive, Vaultwarden instead of a cloud password manager.
None of the individual installs is hard. What goes wrong is the layout around them. Each stage of the build has one classic trap, and each trap has a short, documented fix. Here is the path end to end.
Trap 1: one disk, one bad day
The most common beginner layout is a single SSD holding Proxmox, the containers and every photo you own. When that SSD wears out or fills up, you lose the server and the data at the same moment, and the restore needs the very disk that died.
The fix: separate the system from the data. A small SSD (256 to 512 GB) holds Proxmox and the guests' virtual disks, databases included. A second, large drive holds the photo library, the Nextcloud files and backups waiting to go elsewhere.
Then run all three apps in one Debian 13 VM as a Docker host. Immich's documentation recommends a full VM and calls Docker inside an LXC container not recommended, possible only for advanced users. Give the VM at least 8 GB of RAM (12 to 16 GB is comfortable) and set its CPU type to host or x86-64-v2-AES, because Immich's machine-learning container needs the x86-64-v2 instruction level.
Inside the VM, mount the data disk by its label, with nofail:
mkfs.ext4 -L data /dev/sdb
mkdir -p /srv/data
echo "LABEL=data /srv/data ext4 defaults,nofail 0 2" >> /etc/fstab
mount -a && df -h /srv/data
The label means a reordered disk never mounts in the wrong place, and nofail means a missing disk still lets the VM boot so you can fix it over SSH.
Trap 2: the Immich database on a network share
Immich keeps your files on disk and everything else (who is in the photo, which album it belongs to, where the file lives) in Postgres. The documentation is blunt: the database belongs on local SSD storage, never on a network share of any kind. Point DB_DATA_LOCATION at the NAS and a network hiccup during a write can leave Postgres corrupted and the timeline empty.
The second Immich trap is the version: the server only talks to mobile apps of a compatible major version, so running latest with auto-updates can silently stop your phone backups.
The fix is a handful of lines in .env:
UPLOAD_LOCATION=/srv/data/immich
DB_DATA_LOCATION=/opt/immich/postgres
IMMICH_VERSION=v3
DB_PASSWORD=ChangeMeToALongRandomString
Library on the data disk, database on the SSD, pinned to the major version. Use only letters and digits in the password; the docs warn that special characters confuse Docker's parsing. Turn on the storage template (Administration → Settings → Storage Template) before the first upload, so the library on disk is a readable year/month/day tree.
Trap 3: the hand-built Nextcloud
Web server, PHP, database, Redis, cron jobs and a dozen tuning settings, all by hand. Each piece is easy; together they are a full-time hobby, and months later the admin page is a wall of warnings.
The fix: first decide honestly what you need. A folder that stays the same on your laptop and phone is a Syncthing job. Documents, family sharing, calendars and contacts is a Nextcloud job, and then Nextcloud All-in-One (AIO) is the way: the official Docker setup that runs and updates the whole stack for you. Set NEXTCLOUD_DATADIR=/srv/data/nextcloud in the start command before the first start, because it can't be changed later without a fiddly migration. And don't rename the nextcloud-aio-mastercontainer container; AIO's self-updates depend on that exact name.
Trap 4: open signups and plain HTTP on Vaultwarden
Leave registration open and anyone who finds your URL can create an account; bots scan for exactly that. Serve it over plain HTTP and the web vault fails, because it relies on browser crypto APIs that most browsers only allow over HTTPS.
The fix is in the compose file:
SIGNUPS_ALLOWED: "false"
INVITATIONS_ALLOWED: "true"
SHOW_PASSWORD_HINT: "false"
ports:
- "127.0.0.1:8081:80"
Generate the admin token as an Argon2 hash with docker run --rm -it vaultwarden/server /vaultwarden hash, paste it in, and double every one of its five dollar signs, or Docker Compose silently breaks it. Then chmod 600 /opt/vaultwarden/compose.yaml, turn on two-step login, and print an emergency sheet with your vault URL, email and master passphrase.
Trap 5: forwarding ports to everything
Forward port 443 on the router to the server and every service is reachable from the whole internet; within hours your logs fill with bots trying /admin and /wp-login.php.
The fix: Tailscale on the Docker VM as a subnet router, so your phone reaches home with no open router ports:
tailscale up --advertise-routes=192.168.10.0/24
Approve the route in the admin console and disable key expiry for the server. For real HTTPS names, use a reverse proxy with a wildcard certificate from the DNS challenge, with *.example.com pointing at the proxy's private address. If family members need a shared album link, forward only 443, only to the proxy, and never expose Vaultwarden or the AIO admin interface.
Trap 6: the backup that isn't one, and updates that are never or always
A file copy of a running database can restore as garbage. For Vaultwarden, use its built-in online backup in a nightly script:
docker exec vaultwarden /vaultwarden backup
tar -czf /srv/data/backups/vw-$(date +%F).tgz \
-C /opt/vaultwarden data compose.yaml
Put the whole Docker VM on Proxmox Backup Server with a nightly snapshot-mode job, add an off-site copy, and test a restore once a quarter. On the bad day, restore passwords first: Vaultwarden comes back from the newest vw-*.tgz on any Linux machine with Docker, and it unlocks everything else.
For updates, never updating leaves known holes open, and auto-pulling every new image lands breaking releases at 3 AM. Pick a monthly maintenance hour and snapshot before touching anything:
qm snapshot 201 pre-update-$(date +%Y%m%d)
cd /opt/immich
docker compose pull && docker compose up -d
On the Proxmox host itself, use apt full-upgrade, never plain apt upgrade. Then test each app, and delete the snapshot so it doesn't slowly fill thin storage.
Checklist
- System and guest disks on an SSD; photos and files on a separate data drive mounted by label with
nofail - One Debian 13 VM as the Docker host, CPU type
hostorx86-64-v2-AES - Immich:
DB_DATA_LOCATIONon local SSD,IMMICH_VERSION=v3, storage template on before the first upload - Nextcloud via AIO with
NEXTCLOUD_DATADIRset before the first start, or Syncthing with file versioning on - Vaultwarden: signups off, HTTPS only, Argon2 admin token with doubled dollar signs, two-step login, emergency sheet printed
- Tailscale subnet router, key expiry off; no ports forwarded except 443 to the proxy if you must share
- Nightly PBS job, Vaultwarden online backup in cron, an off-site copy, a dated quarterly restore test
- A monthly update hour that starts with a snapshot

