> ## Content Index
> Fetch the complete content index at: https://sudosecurity.org/llms.txt
> Use this file to discover other available public pages before exploring further.

# The Complete Self-Hosted Productivity Stack: Nextcloud, Vaultwarden, Immich, Jellyfin, and Paperless in 2026
- URL: https://sudosecurity.org/self-hosted-productivity-stack-nextcloud-vaultwarden-immich-jellyfin-paperless-2026/
- Published: 2026-04-27T04:10:00.000Z
- Updated: 2026-07-17T10:33:32.000Z
- Description: 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.
- Author: Traven
- Tags: Self Hosting, Privacy, Systems Administration, Linux, Docker, Open Source, Containers, Cloud Hosting

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](https://sudosecurity.org/self-hosted-email-server-mailcow-setup-guide-2026/) and [running local AI models with Ollama](https://sudosecurity.org/run-local-llm-on-your-pc-complete-guide-2026/). 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](https://sudosecurity.org/what-is-docker/) article covers the concepts. For installation:

```bash
# 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](https://sudosecurity.org/cve-2026-33032-nginx-ui-mcp-authentication-bypass/) 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:

```bash
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:

```bash
docker network create proxy

```

Create `/opt/selfhosted/traefik/traefik.yml`:

```yaml
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: your@email.com       # 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`:

```yaml
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:

```bash
apt install -y apache2-utils
htpasswd -nB admin
# Copy the output, escape $ as $$ in the label

```

Start Traefik:

```bash
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`:

```yaml
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

```

```bash
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:

```bash
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](https://nextcloud.com/install?ref=sudosecurity.org). 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](https://sudosecurity.org/how-authentication-actually-works/) 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`:

```yaml
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
      - SMTP_FROM=vault@yourdomain.com
      - SMTP_PORT=587
      - SMTP_SECURITY=starttls
      - SMTP_USERNAME=vault@yourdomain.com
      - 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

```

```bash
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:**

```bash
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:

```bash
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:

```bash
# 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):

```yaml
# 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:

```yaml
# In the immich-server service:
networks:
  - proxy
  - default
 
# At the bottom of the file:
networks:
  proxy:
    external: true
  default:

```

```bash
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:

```yaml
# /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:

```yaml
    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:

```bash
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:

```yaml
    devices:
      - /dev/dri:/dev/dri

```

```bash
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](https://trash-guides.info/?ref=sudosecurity.org) 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:

```bash
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`:

```bash
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`:

```yaml
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).

```bash
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:

```bash
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:

```bash
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:

```yaml
# 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
      - WATCHTOWER_NOTIFICATION_EMAIL_FROM=watchtower@yourdomain.com
      - WATCHTOWER_NOTIFICATION_EMAIL_TO=you@yourdomain.com
      - 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:

```bash
#!/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](https://rclone.org/?ref=sudosecurity.org) 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.

```bash
# 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](https://sudosecurity.org/self-hosted-email-server-mailcow-setup-guide-2026/).

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](https://sudosecurity.org/build-ai-coding-assistant-local-codebase-2026/) and the [Ollama LLM setup](https://sudosecurity.org/run-local-llm-on-your-pc-complete-guide-2026/) 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](https://sudosecurity.org/topic/sysadmin/).