The Complete Self-Hosted Productivity Stack: Nextcloud, Vaultwarden, Immich, Jellyfin, and Paperless in 2026
Replace Google Drive, Google Photos, 1Password, Plex, and your document scanner with a self-hosted stack that runs on your hardware. Complete Docker Compose setup with Traefik, Nextcloud, Vaultwarden, Immich, Jellyfin, and Paperless-ngx.
Google Drive syncs your files to Google's servers. Google Photos runs face recognition on every photo you've ever taken and stores the results. LastPass has been breached twice -- once in 2015, again in 2022, and the 2022 breach gave attackers encrypted password vaults. Plex sold your viewing history to data brokers for years before anyone noticed. These aren't edge cases. This is what using free cloud services looks like in practice.
The alternative isn't complicated. A modest server -- your old desktop, a $50 Raspberry Pi 5, or a $6/month VPS -- can run every service in this stack. Your files live on hardware you control. Your photos stay on your server. Your passwords sit in a database you own. Your media streams from your storage.
We already covered self-hosting your email with Mailcow and running local AI models with Ollama. This article covers the rest of the stack -- the five services that replace the Google/Microsoft/Apple ecosystem for daily personal and professional use.
What we're building:
| Cloud Service | Self-Hosted Replacement | Function |
|---|---|---|
| Google Drive / Dropbox | Nextcloud | File sync, calendar, contacts, docs |
| 1Password / LastPass / Bitwarden Cloud | Vaultwarden | Password manager |
| Google Photos / iCloud Photos | Immich | Photo backup with ML search |
| Plex / Netflix / Disney+ | Jellyfin | Media streaming server |
| Physical document folders | Paperless-ngx | Document management with OCR |
By the end of this guide you'll have all five running behind a single Traefik reverse proxy with automatic HTTPS, isolated in Docker, accessible from anywhere.
What Hardware and Server Do You Need to Run This Stack?
What Are the Minimum Specs for the Full Stack?
You can run this on a Raspberry Pi 5 (8GB) for light personal use. For a household or small team, you want at least:
- CPU: 4 cores (Immich's machine learning pipeline is the bottleneck)
- RAM: 8GB minimum, 16GB comfortable
- Storage: Whatever you're storing. Plan for OS + Docker overhead (~20GB) plus your data. A 1TB NVMe for the server stack and a separate 4TB HDD for media and photos is a reasonable setup.
For a VPS, Hetzner's CPX31 (4 vCPU, 8GB RAM, 160GB SSD) at roughly €9/month handles the whole stack for a single user. Immich's ML features (face recognition, CLIP search) are CPU-intensive on the indexing pass -- the first run through a large photo library takes time regardless of hardware. After the initial index, it's light.
If you're self-hosting at home rather than on a VPS, you need a way to expose services externally. A static IP from your ISP or a Cloudflare Tunnel are the two standard approaches. If your ISP gives you a dynamic IP, a DDNS service (Duck DNS, Cloudflare's free tier) handles the DNS updates automatically.
What Operating System Should You Use?
Debian 12 or Ubuntu 24.04 LTS. Same recommendation as the Mailcow guide. Stable, long support lifecycle, and Docker's official packages work cleanly on both.
How Do You Set Up the Foundation Before Any Service?
How Do You Install Docker for This Stack?
If you haven't installed Docker yet, the What is Docker article covers the concepts. For installation:
# Remove old Docker packages
apt remove docker docker-engine docker.io containerd runc 2>/dev/null
# Install dependencies
apt update && apt install -y ca-certificates curl gnupg
# Add Docker's GPG key and repository
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/debian $(lsb_release -cs) stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update && apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Verify
docker compose version
Why Use Traefik Instead of Nginx Proxy Manager?
Two options dominate the self-hosted community for reverse proxying Docker services: Nginx Proxy Manager (NPM) and Traefik.
NPM gives you a GUI and is easier to understand initially. Traefik is better for a stack like this for three reasons: it reads Docker labels directly and configures itself automatically when containers start, it handles Let's Encrypt certificates without any manual steps, and it needs no manual config file updates when you add new services. Once the Traefik container is configured, adding a new service means adding four labels to that service's docker-compose definition. That's it.
The CVE-2026-33032 nginx-ui vulnerability that gave attackers full server control through a management UI is a good reminder that web-based admin panels for infrastructure services have a real attack surface. Traefik's configuration lives in files and Docker labels, not a web panel with authentication.
How Do You Set Up the Traefik Reverse Proxy with HTTPS?
Create your project directory structure:
mkdir -p /opt/selfhosted/{traefik,nextcloud,vaultwarden,immich,jellyfin,paperless}
mkdir -p /opt/selfhosted/traefik/certs
touch /opt/selfhosted/traefik/certs/acme.json
chmod 600 /opt/selfhosted/traefik/certs/acme.json
Create a Docker network that all services will share:
docker network create proxy
Create /opt/selfhosted/traefik/traefik.yml:
global:
checkNewVersion: false
sendAnonymousUsage: false
api:
dashboard: true
insecure: false
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
email: [email protected] # Change this
storage: /certs/acme.json
httpChallenge:
entryPoint: web
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false # Explicit opt-in per container
network: proxy
file:
directory: /config
watch: true
log:
level: WARN
Create /opt/selfhosted/traefik/docker-compose.yml:
version: "3.9"
services:
traefik:
image: traefik:v3.1
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- proxy
ports:
- "80:80"
- "443:443"
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/traefik.yml:ro
- ./certs:/certs
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.yourdomain.com`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.middlewares=traefik-auth"
# Basic auth for the dashboard -- generate with: echo $(htpasswd -nB admin)
- "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$2y$$..."
networks:
proxy:
external: true
Replace yourdomain.com throughout with your actual domain. Generate the dashboard password with:
apt install -y apache2-utils
htpasswd -nB admin
# Copy the output, escape $ as $$ in the label
Start Traefik:
cd /opt/selfhosted/traefik && docker compose up -d
docker logs traefik --follow
You should see Traefik start, detect zero services initially, and begin monitoring Docker. The logs show certificate issuance as each service comes online.
How Do You Install Nextcloud for Self-Hosted File Sync?
Nextcloud is the closest thing to a self-hosted Google Workspace. File sync across all your devices, calendar and contacts over CalDAV/CardDAV, collaborative document editing with Nextcloud Office (OnlyOffice under the hood), a mobile app for iOS and Android, and a plugin ecosystem covering everything from Kanban boards to video calls.
The AIO (All-In-One) Docker image is the recommended deployment. It bundles Nextcloud with its own reverse proxy, Redis, Postgres, and Nextcloud Office in a single managed setup.
How Do You Deploy Nextcloud AIO with Docker Compose?
Create /opt/selfhosted/nextcloud/docker-compose.yml:
version: "3.9"
services:
nextcloud-aio-mastercontainer:
image: nextcloud/all-in-one:latest
container_name: nextcloud-aio-mastercontainer
restart: unless-stopped
init: true
networks:
- proxy
- nextcloud-internal
volumes:
- nextcloud_aio_mastercontainer:/mnt/docker-aio-config
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- APACHE_PORT=11000
- APACHE_IP_BINDING=127.0.0.1
- NEXTCLOUD_DATADIR=/opt/nextcloud-data # Store files here
- SKIP_DOMAIN_VALIDATION=false
- NEXTCLOUD_UPLOAD_LIMIT=10G
- NEXTCLOUD_MAX_TIME=3600
- NEXTCLOUD_MEMORY_LIMIT=512M
- AIO_COMMUNITY_CONTAINERS=
labels:
- "traefik.enable=true"
- "traefik.http.routers.nextcloud.rule=Host(`cloud.yourdomain.com`)"
- "traefik.http.routers.nextcloud.entrypoints=websecure"
- "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
- "traefik.http.services.nextcloud.loadbalancer.server.port=11000"
- "traefik.http.routers.nextcloud.middlewares=nextcloud-headers"
# Security headers Nextcloud recommends
- "traefik.http.middlewares.nextcloud-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.nextcloud-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.nextcloud-headers.headers.stsPreload=true"
volumes:
nextcloud_aio_mastercontainer:
networks:
proxy:
external: true
nextcloud-internal:
internal: true
mkdir -p /opt/nextcloud-data
cd /opt/selfhosted/nextcloud && docker compose up -d
Navigate to https://cloud.yourdomain.com:8080 (the AIO admin panel) and complete the setup wizard. It walks you through domain configuration, generates an admin passphrase, and starts all the dependent containers. The first startup takes 3-5 minutes while it pulls images.
After setup, the AIO admin panel moves to https://cloud.yourdomain.com:8080/aio and the actual Nextcloud interface lives at https://cloud.yourdomain.com.
Install the Nextcloud desktop client on your computers to get automatic file sync. On Linux:
sudo add-apt-repository ppa:nextcloud-devs/client
sudo apt update && sudo apt install nextcloud-client
On macOS and Windows, download from nextcloud.com/install. The client behavior matches Dropbox – select which folders to sync, files appear in your filesystem, changes propagate automatically.
How Do You Install Vaultwarden for Self-Hosted Password Management?
Vaultwarden is an unofficial Bitwarden server implementation written in Rust. It's compatible with all official Bitwarden clients -- browser extensions, mobile apps, desktop apps -- but runs on a fraction of the resources the official Bitwarden server requires. The official server is a .NET application designed for enterprise scale. Vaultwarden handles a household or small team on 10MB of RAM.
This is the service in the stack where getting security right matters most. Your password manager holds every credential you own. The authentication fundamentals article covers why session security and HTTPS are non-negotiable for anything handling credentials. Vaultwarden behind Traefik gives you TLS on every request. Still, run this one with care.
How Do You Deploy Vaultwarden Securely?
Create /opt/selfhosted/vaultwarden/docker-compose.yml:
version: "3.9"
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
networks:
- proxy
volumes:
- ./data:/data
environment:
# DISABLE SIGNUPS after you create your account(s)
# Setting this to false locks the vault -- only existing users can log in
- SIGNUPS_ALLOWED=true
# Admin token for /admin panel -- generate with: openssl rand -base64 48
- ADMIN_TOKEN=CHANGE_THIS_TO_A_LONG_RANDOM_STRING
- DOMAIN=https://vault.yourdomain.com
# Email for account recovery -- configure your SMTP or use Nextcloud SMTP
- SMTP_HOST=mail.yourdomain.com
- [email protected]
- SMTP_PORT=587
- SMTP_SECURITY=starttls
- [email protected]
- SMTP_PASSWORD=YOUR_EMAIL_PASSWORD
# WebSocket for live sync notifications
- WEBSOCKET_ENABLED=true
# Disable password hint emails if you don't want hints stored server-side
- PASSWORD_HINTS_ALLOWED=false
labels:
- "traefik.enable=true"
- "traefik.http.routers.vaultwarden.rule=Host(`vault.yourdomain.com`)"
- "traefik.http.routers.vaultwarden.entrypoints=websecure"
- "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
- "traefik.http.services.vaultwarden.loadbalancer.server.port=80"
# WebSocket notifications
- "traefik.http.routers.vaultwarden-ws.rule=Host(`vault.yourdomain.com`) && Path(`/notifications/hub`)"
- "traefik.http.routers.vaultwarden-ws.entrypoints=websecure"
- "traefik.http.routers.vaultwarden-ws.tls.certresolver=letsencrypt"
- "traefik.http.services.vaultwarden-ws.loadbalancer.server.port=3012"
networks:
proxy:
external: true
cd /opt/selfhosted/vaultwarden && docker compose up -d
Navigate to https://vault.yourdomain.com and create your account. After creating all accounts you want:
Disable signups immediately. Edit the compose file and set SIGNUPS_ALLOWED=false, then docker compose up -d to apply. An open Vaultwarden registration on a public URL is a password vault anyone can create an account on. Close it.
Generate the admin token properly:
openssl rand -base64 48
Copy the output into ADMIN_TOKEN. Access the admin panel at https://vault.yourdomain.com/admin with this token. The admin panel lets you manage users, see diagnostic info, and force password resets.
Install the Bitwarden browser extension from your browser's extension store. In the extension settings, change the server URL from the default bitwarden.com to https://vault.yourdomain.com. Log in. Your vault connects to your server. The extension, mobile app, and desktop app all work identically to the cloud version – the only change is where the data lives.
How Do You Install Immich for Self-Hosted Photo Management?
Immich is the most impressive project in this stack and the fastest-moving. It went from "promising prototype" to "Google Photos replacement I'd actually trust" between 2023 and 2025. Face recognition works. The CLIP-powered semantic search works -- you can search "dog at the beach" and find that photo without tagging anything. Mobile app backup runs in the background on iOS and Android. Timeline view, albums, sharing, map view, and trash all work.
Immich requires more infrastructure than the other services in this stack: PostgreSQL with the pgvecto.rs extension for vector search, Redis for caching, and a separate ML worker container for face recognition and CLIP embeddings. The Immich team maintains a single docker-compose file that wires all of this together.
How Do You Deploy Immich with Docker Compose?
Download Immich's official compose files:
cd /opt/selfhosted/immich
wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env
Edit the .env file:
# Database password -- generate with: openssl rand -hex 32
DB_PASSWORD=CHANGE_THIS_STRONG_PASSWORD
# Immich version -- use "release" for stable
IMMICH_VERSION=release
# Where photos are stored on your host
UPLOAD_LOCATION=/opt/immich-library
# Timezone
TZ=America/New_York
Now add Traefik labels to the immich-server service in docker-compose.yml. Open the file and find the immich-server service definition. Add to its labels section (or create one):
# Add these labels to the immich-server service in the downloaded docker-compose.yml
labels:
- "traefik.enable=true"
- "traefik.http.routers.immich.rule=Host(`photos.yourdomain.com`)"
- "traefik.http.routers.immich.entrypoints=websecure"
- "traefik.http.routers.immich.tls.certresolver=letsencrypt"
- "traefik.http.services.immich.loadbalancer.server.port=2283"
Add the proxy network to the immich-server service and add it to the networks section at the bottom:
# In the immich-server service:
networks:
- proxy
- default
# At the bottom of the file:
networks:
proxy:
external: true
default:
mkdir -p /opt/immich-library
cd /opt/selfhosted/immich && docker compose up -d
The first startup takes a few minutes. The ML model downloads happen on first run.
Navigate to https://photos.yourdomain.com and create your admin account. Then install the Immich app on your phone: Settings → Backup → change server URL to https://photos.yourdomain.com. The app backs up photos and videos in the background, mirroring the Google Photos experience.
On the initial ML indexing: After uploading photos, Immich queues them for face detection and CLIP embedding. On a budget server, indexing a 10,000-photo library takes several hours. On a machine with a decent CPU or a GPU, it runs in under an hour. The job runs in the background -- the photos are accessible immediately, the search just gets better as indexing completes. Check progress at Administration → Jobs.
External library support: If you have existing photos on a NAS or external drive you don't want to move, Immich's external library feature lets you point it at a directory and index in place without copying. The photos stay where they are; Immich reads them and adds them to the timeline.
How Do You Install Jellyfin for Self-Hosted Media Streaming?
Plex requires a Plex Media Server account to use Plex Media Server. That sentence is not a typo. Your media server, running on your hardware, requires you to authenticate with Plex's servers to access it. If Plex's servers go down, you can't access your local media. Plex also sold viewing history to data brokers. Emby went closed-source. Jellyfin forked from Emby and has been fully open source ever since. No account required. No external authentication. Your media, your server, no strings.
How Do You Deploy Jellyfin with Docker Compose and GPU Transcoding?
For software transcoding only:
# /opt/selfhosted/jellyfin/docker-compose.yml
version: "3.9"
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
restart: unless-stopped
networks:
- proxy
user: "1000:1000" # Run as your user, not root
volumes:
- ./config:/config
- ./cache:/cache
- /opt/media:/media:ro # Your media library -- read-only is safer
environment:
- JELLYFIN_PublishedServerUrl=https://media.yourdomain.com
- TZ=America/New_York
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyfin.rule=Host(`media.yourdomain.com`)"
- "traefik.http.routers.jellyfin.entrypoints=websecure"
- "traefik.http.routers.jellyfin.tls.certresolver=letsencrypt"
- "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
networks:
proxy:
external: true
For NVIDIA GPU hardware transcoding, add device access:
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
environment:
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=all
And install the NVIDIA Container Toolkit on the host:
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \
tee /etc/apt/sources.list.d/nvidia-docker.list
apt update && apt install -y nvidia-container-toolkit
systemctl restart docker
For Intel Quick Sync (integrated GPU on Intel CPUs), add:
devices:
- /dev/dri:/dev/dri
mkdir -p /opt/media /opt/selfhosted/jellyfin/{config,cache}
cd /opt/selfhosted/jellyfin && docker compose up -d
Navigate to https://media.yourdomain.com and complete the setup wizard. Add your media library paths (Movies, TV Shows, Music), and Jellyfin starts scanning and pulling metadata. The metadata agent pulls from TMDb, TheTVDB, and MusicBrainz automatically.
On transcoding: Hardware transcoding reduces CPU usage during playback from 50-100% (software) to 5-15% (hardware). For a single viewer, software transcoding on a modern CPU is fine. For multiple simultaneous streams, hardware transcoding becomes necessary. Enable it at Administration → Playback → Transcoding after configuring the GPU device.
Organize your media files correctly. Jellyfin's metadata matching requires the naming convention:
/opt/media/
Movies/
The Dark Knight (2008)/
The Dark Knight (2008).mkv
TV Shows/
Breaking Bad/
Season 01/
Breaking Bad - S01E01 - Pilot.mkv
Incorrect naming causes metadata matching failures. The TRaSH Guides are the community standard for media organization – worth reading before you start adding files.
How Do You Install Paperless-ngx for Document Management?
Physical documents accumulate. Tax returns, insurance policies, medical records, contracts, receipts -- they pile up in folders or boxes, then you can't find anything when you need it. Paperless-ngx digitizes the whole stack: scan a document, Paperless runs OCR on it, extracts the text, lets you search across all of it, automatically applies tags based on rules you define, and archives the original file with a consistent naming scheme.
The workflow with a scanner: scan to a folder, Paperless picks up new files automatically, OCRs them, and moves them into the library. No manual filing. Search "mortgage statement 2024" and the document appears.
How Do You Deploy Paperless-ngx with Docker Compose?
Paperless-ngx needs PostgreSQL, Redis, and a Tika server for document type detection. The official compose file handles all of it:
cd /opt/selfhosted/paperless
# Download the official compose file
wget -O docker-compose.yml \
https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.postgres.yml
wget -O .env \
https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env.example
Edit the .env:
PAPERLESS_URL=https://docs.yourdomain.com
PAPERLESS_SECRET_KEY=GENERATE_WITH_openssl_rand_-hex_32
PAPERLESS_TIME_ZONE=America/New_York
PAPERLESS_OCR_LANGUAGE=eng # Add more: eng+deu+fra for multilingual
# Database
PAPERLESS_DBPASS=STRONG_PASSWORD
# Optional: where to store documents on the host
PAPERLESS_DATA_DIR=/opt/paperless-data
PAPERLESS_MEDIA_ROOT=/opt/paperless-media
PAPERLESS_CONSUMPTION_DIR=/opt/paperless-consume # Drop scans here
Add Traefik labels to the webserver service in docker-compose.yml:
labels:
- "traefik.enable=true"
- "traefik.http.routers.paperless.rule=Host(`docs.yourdomain.com`)"
- "traefik.http.routers.paperless.entrypoints=websecure"
- "traefik.http.routers.paperless.tls.certresolver=letsencrypt"
- "traefik.http.services.paperless.loadbalancer.server.port=8000"
Add the proxy network to the webserver service and networks section (same pattern as Immich).
mkdir -p /opt/paperless-{data,media,consume}
cd /opt/selfhosted/paperless && docker compose up -d
# Create your admin account
docker compose exec webserver python3 manage.py createsuperuser
Navigate to https://docs.yourdomain.com and log in. The first time you drop a PDF into /opt/paperless-consume, Paperless runs OCR (using Tesseract), extracts text, generates a thumbnail, and adds the document to the library. The whole process takes 5-30 seconds per document depending on page count and image quality.
Setting up automatic tagging: Go to Settings → Mail and configure your email if you want Paperless to ingest PDFs from emails automatically. More powerfully, set up Workflows (formerly "Consumption Templates") to automatically tag documents: any document containing "insurance" in its text gets tagged "Insurance". Documents from a specific correspondent get auto-assigned. Once you configure the rules, filing is fully automatic.
Scanner integration: If you have a network scanner, most modern scanners can scan directly to an SMB share or FTP location. Point that location to /opt/paperless-consume. Documents land in the consumption folder automatically after scanning.
How Do You Secure and Maintain the Full Stack?
How Do You Harden Each Service After Installation?
A few hardening steps apply across the whole stack:
Fail2ban for Vaultwarden. Vaultwarden logs failed login attempts. Fail2ban can read those logs and block IPs after repeated failures:
apt install -y fail2ban
# /etc/fail2ban/filter.d/vaultwarden.conf
cat > /etc/fail2ban/filter.d/vaultwarden.conf << 'EOF'
[INCLUDES]
before = common.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>.*$
ignoreregex =
EOF
# /etc/fail2ban/jail.d/vaultwarden.conf
cat > /etc/fail2ban/jail.d/vaultwarden.conf << 'EOF'
[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
logpath = /opt/selfhosted/vaultwarden/data/vaultwarden.log
maxretry = 5
bantime = 3600
findtime = 300
EOF
systemctl restart fail2ban
Network isolation. Each service communicates over the proxy Docker network only for Traefik routing. Internal service communication (Immich's containers, Paperless's database) uses internal Docker networks not exposed to the proxy. You can verify this by inspecting each container's network membership:
docker inspect immich-server | jq '.[0].NetworkSettings.Networks | keys'
Read-only media volumes. Jellyfin gets :ro on the media mount. No bug or vulnerability in Jellyfin can modify your media files when the volume is read-only.
Regular updates. The self-hosting community has learned the hard way that running outdated containers is where security incidents start. Watchtower automates container updates:
# Add to any docker-compose.yml or create a dedicated one
services:
watchtower:
image: containrrr/watchtower
container_name: watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * * # 4 AM daily
- WATCHTOWER_NOTIFICATIONS=email
- [email protected]
- [email protected]
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER=mail.yourdomain.com
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587
Automated updates carry a risk: a bad release breaks your service at 4 AM. The alternative is manual updates that don't happen because you forget. For personal infrastructure, automated updates with email notifications is the right call. For anything serving customers, pin versions and test updates manually.
How Do You Back Up the Entire Stack?
The rule for self-hosted services: if the data doesn't exist in three places, it doesn't exist. The 3-2-1 backup strategy means three copies, two different media types, one offsite.
Each service's data directories:
Nextcloud: /opt/nextcloud-data (user files)
nextcloud_aio_mastercontainer volume (config)
Vaultwarden: /opt/selfhosted/vaultwarden/data
Immich: /opt/immich-library
Jellyfin: /opt/selfhosted/jellyfin/config
/opt/media (your media -- you presumably have other copies)
Paperless: /opt/paperless-{data,media}
A daily rsync backup script to a remote location:
#!/bin/bash
# /opt/backup.sh -- run via cron at 3 AM daily
REMOTE="user@backup-server:/backups/selfhosted"
DIRS=(
"/opt/selfhosted/vaultwarden/data"
"/opt/paperless-data"
"/opt/paperless-media"
"/opt/selfhosted/jellyfin/config"
)
for DIR in "${DIRS[@]}"; do
rsync -avz --delete "$DIR" "$REMOTE/"
done
# Database dumps for Nextcloud and Immich (Postgres)
docker exec nextcloud-aio-database \
pg_dump -U nextcloud nextcloud | \
gzip > /tmp/nextcloud-$(date +%Y%m%d).sql.gz
docker exec immich_postgres \
pg_dump -U postgres immich | \
gzip > /tmp/immich-$(date +%Y%m%d).sql.gz
rsync -avz /tmp/*.sql.gz "$REMOTE/databases/"
find /tmp -name "*.sql.gz" -mtime +7 -delete
Immich's library is large. For the photo library, consider rclone to sync to Backblaze B2 (around $6/TB/month) as the offsite copy.
What Does the Full Stack Cost to Run?
For a home server on existing hardware: electricity. A low-power system (Intel N100, Raspberry Pi 5) running continuously draws 10-25 watts. At $0.15/kWh that's $1-3/month.
For a dedicated VPS on Hetzner: roughly €9-15/month for a CPX31 or CPX41.
Compare to what this replaces:
| Service | Monthly Cloud Cost |
|---|---|
| Google One (2TB) | $9.99 |
| 1Password Family | $4.99 |
| Plex Pass | $4.99 |
| Google Photos (beyond free tier) | included in Google One |
| Document scanner app with cloud storage | $4-15 |
| Total | $23-34/month |
The hardware pays for itself in under a year. After that, the stack costs electricity.
Frequently Asked Questions
Can you run this stack on a Raspberry Pi?
A Raspberry Pi 5 (8GB) can run Nextcloud, Vaultwarden, Jellyfin (without transcoding), and Paperless-ngx. Immich's ML features -- face recognition and semantic search -- are CPU-intensive. The initial indexing pass on a large library will take a long time. After indexing, daily operation is light. Disable Immich's ML worker if the Pi can't keep up; the app works without it, just without smart search.
Is it safe to expose these services to the internet?
With Traefik handling TLS, fail2ban blocking brute force attempts, and containers running as non-root users, the attack surface is manageable. The larger risk is unpatched vulnerabilities in the service itself -- set up Watchtower and pay attention to security announcements from each project. Vaultwarden and Nextcloud in particular issue security releases you want to apply quickly.
If you want an extra layer, put everything behind a Cloudflare Zero Trust tunnel. Your services get no public ports at all -- traffic flows through Cloudflare's network after authentication. More setup, meaningfully stronger security posture.
How do you migrate from Google Photos to Immich?
Google Takeout exports your Google Photos library as a zip archive. The Immich CLI has a built-in Google Photos import command that handles the Takeout format correctly, preserving creation dates from the JSON metadata files Google includes alongside photos. Without this, imported photos get the import date instead of the original capture date.
# Install the Immich CLI
npm install -g @immich/cli
# Authenticate
immich login https://photos.yourdomain.com
# Upload the Takeout directory
immich upload --recursive /path/to/google-photos-takeout
Can Nextcloud replace Google Workspace for a small team?
For file sync, calendar, contacts, and collaborative document editing: yes. Nextcloud Office (OnlyOffice) handles .docx, .xlsx, and .pptx files. Multi-user editing works. For email, you'd pair Nextcloud with a self-hosted mail server -- we covered the full setup in the Mailcow guide.
For video calls, Nextcloud's Talk app integrates directly but requires a TURN server for reliable peer-to-peer connections through NAT. For serious video conferencing, a dedicated Jitsi Meet instance or Matrix/Element is more reliable.
What happens if your server goes down?
Nextcloud client apps cache recent files locally. You can keep working. Vaultwarden clients cache your vault -- you can read passwords. Immich's mobile app has the photos on your phone. Jellyfin is the one service with no offline fallback. Make sure your server has good uptime or use a UPS to handle power interruptions.
The self-hosted approach takes an afternoon to set up. After that, it runs. These services have matured to the point where they don't demand constant maintenance -- they just work, quietly, on your hardware, with your data staying where you put it.
If you want to take the stack further: add the local AI coding assistant and the Ollama LLM setup from our earlier guides, and you have a complete development environment that doesn't require a single external service subscription. For the sysadmin techniques to keep all of it running reliably, check out the SysAdmin topic section.