{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreid763b3wlifopez5ehvajcw2dke7g3psxjkiu2odwaw2sjgsd2w4m",
    "uri": "at://did:plc:whmaqs3yqcgi4ohxkp5jvxin/app.bsky.feed.post/3mehrxnj3yry2"
  },
  "path": "/2026/02/09/encrypted-private-nextcloud-vps-and-storagebox/",
  "publishedAt": "2026-02-09T00:00:00.000Z",
  "site": "https://justinscholz.de",
  "tags": [
    "Hetzner Community",
    "VPS on Hetzner",
    "Hetzner Storage Box",
    "How to install Ubuntu 24.04 with full disk encryption",
    "Hetzner Console",
    "Initial Server Setup with Ubuntu",
    "https://docs.docker.com/engine/install/debian/",
    "https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md",
    "`github.com/nextcloud/all-in-one/blob/main/compose.yaml`",
    "`nextcloud/all-in-one/blob/main/readme.md`"
  ],
  "textContent": "> _This is a republish of a tutorial I authored for theHetzner Community._\n\n## Introduction\n\nIn light of current events — _waves around_ — putting your own private data into a US based cloud, regardless of whether you are a US citizen or a EU citizen or somewhere else, might not be the most advisable option anymore. Especially if it’s not encrypted, as it can then be scanned, AI trained on and many more things.\n\nSo I set out to run my own Nextcloud — pretty much a European open-source cloudware that is a bit akin to Google Workspace without email hosting.\n\nWhat I wanted to achieve:\n\n  * Low costs\n  * Controllable costs (I want them predictable - not like an AWS bill)\n  * European data center\n  * Expandable storage without rebuilding the server\n  * Fully encrypted with proper key separation\n  * Syncing of files, calendar, contacts and the ability to host my own video calls à la Zoom\n\n\n\n**The key security insight:** Nextcloud’s server-side encryption stores the encryption keys in the same data directory as the encrypted files. If you simply point your data directory at a Storage Box, both your encrypted files AND the keys to decrypt them live in the same place — defeating much of the purpose.\n\nThis guide takes a different approach: encryption keys stay on your LUKS-encrypted VPS disk, while the bulk encrypted file storage lives on the cheap, expandable Storage Box. If someone gains access to your Storage Box, they get only encrypted blobs with no way to decrypt them.\n\n> **Note for existing users:** A previous version of this guide stored the entire data directory on the Storage Box, which meant encryption keys and encrypted files lived together. If you followed the earlier guide, see the Migration from Previous Guide section for steps to improve your security posture.\n\nThe setup has survived stress tests of 120,000+ files, multiple reboots, and months of daily use.\n\n**What this guide covers:**\n\n  * Setting up the system with proper encryption architecture\n  * Getting Nextcloud running with Nextcloud AIO\n  * Configuring per-user storage offloading to the Storage Box\n\n\n\n**What this guide does NOT cover:**\n\n  * Day-to-day Nextcloud usage and administration\n  * Maintaining and updating the system long-term\n\n\n\n> The Nextcloud desktop client for Mac and Windows supports virtual file systems. This means you can sync some folders fully to your device while having others available on-demand through the file provider API — useful for large archives you don’t need locally all the time.\n\n## Architecture Overview\n\nBefore diving into the setup, it helps to understand what we’re building and why.\n\n**The Problem with Naive Setups**\n\nWhen you enable Nextcloud’s server-side encryption, it creates a `files_encryption` folder containing the keys needed to decrypt your files. If your entire data directory lives on remote storage (like a Storage Box), then anyone with access to that storage has both:\n\n  * Your encrypted files\n  * The keys to decrypt them\n\n\n\nThis is like locking your front door and leaving the key under the doormat.\n\n**Our Solution: Key Separation**\n\n\n    ┌─────────────────────────────────────────────────────────────────────────┐\n    │ VPS (LUKS-encrypted disk) │\n    │ │\n    │ Nextcloud Data Directory (local Docker volume) │\n    │ ├── files_encryption/ ← Master encryption keys (NEVER leave disk) │\n    │ ├── appdata_*/ ← App cache and config │\n    │ │ │\n    │ └── <username>/ │\n    │ ├── files_encryption/ ← Per-user keys (stay local) │\n    │ ├── cache/ ← User cache (stays local) │\n    │ ├── files/ ─┐ │\n    │ ├── files_trashbin/ ├─ Bind mounts to Storage Box │\n    │ ├── files_versions/ │ (only encrypted content travels there) │\n    │ └── uploads/ ─┘ │\n    └─────────────────────────────────────────────────────────────────────────┘\n    │\n    │ bind mounts (per-user, 4 folders each)\n    ▼\n    ┌─────────────────────────────────────────────────────────────────────────┐\n    │ Storage Box (SMB mount) │\n    │ │\n    │ └── <username>/ │\n    │ ├── files/ ← Encrypted blobs only │\n    │ ├── files_trashbin/ ← Encrypted │\n    │ ├── files_versions/ ← Encrypted │\n    │ └── uploads/ ← Encrypted │\n    └─────────────────────────────────────────────────────────────────────────┘\n\n\n**Security Properties**\n\nComponent | Location | Protected by\n---|---|---\nEncryption keys | VPS local disk | LUKS full-disk encryption (boot passphrase)\nEncrypted files | Storage Box | Nextcloud server-side encryption\nNextcloud config | VPS local disk | LUKS full-disk encryption\n\n**What this means in practice:**\n\n  * **Storage Box compromised?** Attacker gets encrypted blobs, useless without keys.\n  * **VPS disk stolen while powered off?** Protected by LUKS encryption.\n  * **VPS compromised while running?** Keys are in memory — this is outside our threat model (if you need protection against this, don’t run on shared infrastructure).\n\n\n\n**The Trade-off: Per-User Setup**\n\nThis architecture requires adding bind mounts for each user you want to offload to the Storage Box. New users default to local storage (secure by default), and you explicitly choose which users to offload. This is a small administrative overhead for significantly better security.\n\n**Why I care about encryption**\n\nIt might not be often, but it does happen that law enforcement confiscates physical hardware in a data center for analysis, or that people get unauthorised physical access. By requiring manual entry of the encryption key after a power event, I get to decide whether to unlock my data or not. Storing the encryption key at boot is like taping it next to your door outside your house “just to have it handy all the time”.\n\n## Prerequisites\n\n  * VPS on Hetzner (in their parlance a cloud server) with 4GB of RAM, 2 CPU cores and 40GB of NVME storage\n  * A Hetzner Storage Box — essentially a 1TB+ NAS in the cloud for an incredibly low price\n  * A domain name with the ability to set A and AAAA records\n  * Basic familiarity with Linux command line and SSH\n\n\n\nBoth resources should be in the same Hetzner region to keep traffic internal and latency low.\n\n> **On RAM:** 4GB is comfortable. 2GB can work if you set up swap (covered later), but you may experience slowdowns during heavy operations like full-text search indexing.\n\n## Step 1 - Creating the server\n\nCreate a new server with the architecture type `x86` (!important!). With Hetzner, you can use CX23, for example. The 2 CPU version is sufficient. Even only 2 GB can work RAM wise — I will show you how.\n\nAfter you created the server, follow this guide:\n\nHow to install Ubuntu 24.04 with full disk encryption\n\nBeware of the special section for Debian 13 with the following caveats:\n\n  * When booted into the rescue system, you can check the full name of the Debian image by running `ls /root/images` and copying the Debian 13 image name into your pasteboard.\n\n  * Your `setup.conf` should look like this:\n\n        CRYPTPASSWORD secret\n        DRIVE1 /dev/sda\n        BOOTLOADER grub\n        HOSTNAME host.example.com\n        PART /boot ext4 1G\n        PART / ext4 all crypt\n        IMAGE /root/images/Debian-1302-trixie-amd64-base.tar.gz\n        SSHKEYS_URL /tmp/authorized_keys\n\n\n  * _Additional notice:_ If you add private networking it might happen that your Dropbear unlock only picks up the IP from your local network first and is unreachable over ssh. In this case temporarily disable the private network or discuss this issue with your preferred coding AI — the issue is that the private network wins the race of “which network interface responds with an IP address first”.\n\n\n\n\nOnce that is setup, you can create yourself a Storage Box in the same region via Hetzner Console. Choose your preferred size. Create a subaccount with limited access to a specific subfolder for your Nextcloud and enable SMB access. You DON’T need to enable “external access” though as this stays in the Hetzner network.\n\n## Step 2 - Booting into the server\n\nNow you can proceed with setting up your VPS. You can follow this guide for a basic setup:\n\nInitial Server Setup with Ubuntu\n\nLet’s open the relevant ports on UFW for all the stuff:\n\n\n    sudo ufw default deny incoming\n    sudo ufw default allow outgoing\n\n    # HTTP for ACME/Nextcloud challenge\n    sudo ufw allow 80/tcp comment 'ACME-HTTP-Nextcloud'\n\n    # HTTPS for Apache container (HTTP/1.1 & HTTP/2)\n    sudo ufw allow 443/tcp comment 'Apache-HTTPS'\n\n    # HTTP/3 (QUIC) for Apache container\n    sudo ufw allow 443/udp comment 'Apache-HTTP3-QUIC'\n\n    # Admin interface of master container\n    sudo ufw allow 8443/tcp comment 'Master-UI-HTTPS'\n\n    # TURN server (Talk container) – TCP & UDP\n    sudo ufw allow 3478/tcp comment 'TURN-TCP'\n    sudo ufw allow 3478/udp comment 'TURN-UDP'\n\n\nOnce that is done, you can check your IPv4 address (if you ordered one) and IPv6 by running this command on the server:\n\n\n    ip -6 addr show\n\n\nYour interface is probably `enp1s0`.\n\n## Step 3 - Pointing DNS to your server\n\nIf you now go to the domain registrar of your own domain, you can adjust the (sub)domain’s A and AAAA records for your IPv4 and IPv6 addresses respectively.\n\nOnce that is done, let’s go back to your VPS.\n\n## Step 4 - Creating the SMB mount to the Storage Box\n\nBack on the Hetzner VPS, go to a root shell with `sudo su`. Stay in the root shell during the rest of the guide.\n\nThis mount will hold the encrypted file content for users. It is NOT the Nextcloud data directory — that stays on the local LUKS-encrypted disk. We’ll bind-mount specific user folders from here into the data directory later.\n\nFirst, install SMB support:\n\n\n    apt update\n    apt install cifs-utils\n\n\nCreate the mount point (replace `myshare` with whatever you want to call it):\n\n\n    mkdir -p /mnt/myshare\n\n\nCreate a credentials file (our disk is encrypted, so storing credentials here is acceptable):\n\n\n    mkdir -p /etc/cifs-creds\n    nano /etc/cifs-creds/myshare\n\n\nAdd:\n\n\n    username=your_smb_username\n    password=your_smb_password\n\n\nThis username and password comes from your Storage Box sub account.\n\nSecure the credentials file:\n\n\n    chmod 600 /etc/cifs-creds/myshare\n\n\nNow create a systemd `.mount` unit:\n\n\n    nano /etc/systemd/system/mnt-myshare.mount\n\n\n> **Important:** The unit filename must match the mount path exactly, with `/` replaced by `-` and the leading slash removed. So `/mnt/myshare` becomes `mnt-myshare.mount`.\n\nAdd the following content:\n\n> Replace `YOURSTORAGEBOXUSER-subX` with your actual account name.\n\n\n    [Unit]\n    Description=Mount SMB Share myshare\n    DefaultDependencies=no\n    After=network-online.target\n    Wants=network-online.target\n\n    [Mount]\n    What=//YOURSTORAGEBOXUSER-subX.your-storagebox.de/YOURSTORAGEBOXUSER-subX\n    Where=/mnt/myshare\n    Type=cifs\n    Options=credentials=/etc/cifs-creds/myshare,iocharset=utf8,uid=33,gid=33,seal,vers=3.1.1,_netdev\n    TimeoutSec=30\n\n    [Install]\n    WantedBy=multi-user.target\n\n\nA few notes on this unit:\n\n  * `DefaultDependencies=no` prevents systemd from adding automatic dependencies that can cause ordering cycles with network-dependent mounts\n  * `uid=33,gid=33` sets ownership to `www-data` (the user Nextcloud runs as)\n  * `seal` enables SMB encryption in transit\n  * `_netdev` tells the system this is a network mount\n\n\n\nActivate it:\n\n\n    systemctl daemon-reload\n    systemctl enable --now mnt-myshare.mount\n\n\n> If you get an error message like `Failed to mount mnt-myshare.mount - Mount SMB Share myshare`, double-check in Hetzner Console if the subaccount has “Allow SMB” enabled.\n\nVerify it mounted:\n\n\n    mount | grep myshare\n    ls -la /mnt/myshare\n\n\nYou should see an empty directory owned by `www-data`.\n\n## Step 5 - Installing Docker\n\nFollow the official guide to install Docker: https://docs.docker.com/engine/install/debian/\n\nOnce installed, make Docker wait for the SMB mount to be ready:\n\n\n    systemctl edit docker.service\n\n\nAdd above the line that says `### Edits below this comment will be discarded`:\n\n\n    [Unit]\n    Requires=mnt-myshare.mount\n    After=mnt-myshare.mount\n\n\nReload systemd:\n\n\n    systemctl daemon-reload\n\n\nFinally, enable IPv6 support for Docker (we’re living in the 21st century): https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md\n\nYou won’t be able to verify IPv6 works until Nextcloud is running — that comes later.\n\n## Step 6 - Preparing Nextcloud AIO\n\nI recommend running Nextcloud AIO through compose. This makes it easier to track changes and see the startup parameters.\n\n\n    mkdir -p ~/containers/nextcloud\n    cd ~/containers/nextcloud\n    nano compose.yml\n\n\nStart with the official compose file from AIO:\n\n`github.com/nextcloud/all-in-one/blob/main/compose.yaml`\n\nMake the following adjustments:\n\n  * Uncomment `environment:`\n\n  * Uncomment `NEXTCLOUD_DATADIR:` and set it explicitly to the local Docker volume:\n\n        NEXTCLOUD_DATADIR: /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/\n\n\n> **Important:** Do NOT point this at your SMB mount. The data directory must stay on the local LUKS-encrypted disk so that encryption keys remain separate from encrypted files. We’ll bind-mount only the user file storage to the SMB mount later.\n\n  * Uncomment `NEXTCLOUD_MAX_TIME:` and increase the value (I use `7200`)\n\n  * Uncomment `NEXTCLOUD_MEMORY_LIMIT:` and set it to `2048`\n\n  * Uncomment `environment:` above `NEXTCLOUD_MAX_TIME:` and `NEXTCLOUD_MEMORY_LIMIT:`\n\n  * If you plan to use full-text search, uncomment `FULLTEXTSEARCH_JAVA_OPTIONS:` and set reasonable values:\n\n        FULLTEXTSEARCH_JAVA_OPTIONS: \"-Xms1024M -Xmx2048M\"\n\n\n\n\n\n## Step 7 - Setting up swap to help with memory pressure\n\nBefore we start running any containers, let’s enable Swap on the server so that we don’t run out of RAM:\n\n\n    fallocate -l 4G /swapfile\n    dd if=/dev/zero of=/swapfile bs=1M count=4096 status=progress\n    chmod 600 /swapfile\n    mkswap /swapfile\n    swapon /swapfile\n\n\nLet’s add it to system boot up:\n\n\n    echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab\n\n\nEncourage a bit more swapping:\n\n\n    echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf\n\n\nYou can verify it worked by checking:\n\n\n    htop\n\n\nIt should show memory and swap separately. Swap should show `x/4.00G`.\n\n_Especially if you are on the 2GB RAM box, 4G of Swap were a good idea when also running fulltext search_\n\n## Step 8 - Starting it up\n\nMake sure you’re in the right directory:\n\nIf you haven’t yet adjusted the DNS on your domain registrar to point to your Hetzner server, now is the time.\n\n\n    cd ~/containers/nextcloud\n    docker compose up -d\n\n\nNow continuing the guide on `nextcloud/all-in-one/blob/main/readme.md`, let’s open `https://example.com:8443` and go to your AIO install interface. Use your own domain.\n\n> If `https://example.com:8443` doesn’t load for you, try `https://example.com:8080` instead.\n\nFollow the steps. At the end it will show a temporary password for the user admin that you can then use to login under `https://example.com:443` with the data store in the back being on your Storage Box.\n\n## Step 9 - Enabling encryption\n\nOnce logged in to the Nextcloud, you should:\n\n| Description\n---|---\nEncrypt all files | Head to your user icon on the top right => Admin settings => security settings => server side encryption => switch it on\nActivate the encryption module | Head to your user icon on the top right => apps => deactivated apps and activate the “default encryption module”\nEncrypt user home folders | Head back to admin settings => security settings => encryption\nCheck that the checkbox is ticked for “encrypt user home folders”\n\nIf you want, you can stop your containers, enable fulltextsearch and some other containers.\n\n## Step 10 - Setting up per-user storage offloading\n\nNow that Nextcloud is running with encryption enabled, we’ll configure specific users to store their files on the Storage Box while keeping encryption keys local.\n\n**How it works:** Each Nextcloud user has four storage folders:\n\n  * `files/` — their actual files\n  * `files_trashbin/` — deleted files (before permanent deletion)\n  * `files_versions/` — version history\n  * `uploads/` — temporary storage during uploads\n\n\n\nWe create these folders on the Storage Box, then bind-mount them into the Nextcloud data directory. Nextcloud sees them as local folders, but the data actually lives on the Storage Box — encrypted.\n\n### Setting up a user (example: admin)\n\nFirst, check that Nextcloud has created the user’s folder structure:\n\n\n    ls -la /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/\n\n\nYou should see `files/` at minimum. If the other folders don’t exist yet, they’ll be created when needed.\n\nCreate the corresponding folder structure on the Storage Box:\n\n\n    mkdir -p /mnt/myshare/_data/admin/{files,files_trashbin,files_versions,uploads}\n    chown -R www-data:www-data /mnt/myshare/_data/admin\n\n\nCreate the target directories in the Docker volume (if they don’t exist):\n\n\n    mkdir -p /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/{files,files_trashbin,files_versions,uploads}\n    chown -R www-data:www-data /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin\n\n\nAdd the bind mounts to `/etc/fstab`:\n\n\n    nano /etc/fstab\n\n\nAdd these lines (adjust `myshare` to your mount name):\n\n\n    # Nextcloud user: admin\n    /mnt/myshare/_data/admin/files /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/files none bind,nofail,x-systemd.requires-mounts-for=/mnt/myshare,x-systemd.device-timeout=30s 0 0\n    /mnt/myshare/_data/admin/files_trashbin /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/files_trashbin none bind,nofail,x-systemd.requires-mounts-for=/mnt/myshare,x-systemd.device-timeout=30s 0 0\n    /mnt/myshare/_data/admin/files_versions /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/files_versions none bind,nofail,x-systemd.requires-mounts-for=/mnt/myshare,x-systemd.device-timeout=30s 0 0\n    /mnt/myshare/_data/admin/uploads /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/uploads none bind,nofail,x-systemd.requires-mounts-for=/mnt/myshare,x-systemd.device-timeout=30s 0 0\n\n\n**Understanding the mount options:**\n\n  * `bind` — this is a bind mount, not a device mount\n  * `nofail` — boot continues even if mount fails (prevents boot hang)\n  * `x-systemd.requires-mounts-for=/mnt/myshare` — wait for SMB mount first\n  * `x-systemd.device-timeout=30s` — timeout if mount takes too long\n\n\n\nMount them now:\n\n\n    systemctl daemon-reload\n    mount -a\n\n\nVerify they’re active:\n\n\n    mount | grep admin/files\n    mount | grep admin/uploads\n\n\nYou should see four bind mounts.\n\n### Usernames with spaces\n\nIf a username contains spaces (like “Hans Werner”), escape spaces with `\\040` in fstab:\n\n\n    /mnt/myshare/_data/Hans\\040Werner/files /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/Hans\\040Werner/files none bind,nofail,x-systemd.requires-mounts-for=/mnt/myshare,x-systemd.device-timeout=30s 0 0\n\n\n### What about existing files?\n\nIf the user already has files in Nextcloud before you set up the bind mount:\n\n  1. Stop Nextcloud containers:\n\n         cd ~/containers/nextcloud && docker compose down\n\n\n  2. Move existing files to Storage Box:\n\n         mv /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/files/* /mnt/myshare/_data/admin/files/\n\n\n  3. Set up bind mounts as described above\n  4. Start Nextcloud:\n\n         docker compose up -d\n\n\n  5. Optionally, run a file scan:\n\n         docker exec -it nextcloud-aio-nextcloud php occ files:scan admin\n\n\n\n\n\n### Important notes\n\n  * **New users default to local storage.** This is secure by default — encryption keys and files are both on the LUKS disk. You choose which users to offload.\n  * **Don ’t bind-mount `files_encryption`!** That folder contains encryption keys and must stay on the local disk.\n  * **Changes are per-user.** You can have some users on local storage and others offloaded to the Storage Box.\n\n\n\n## Step 11 - Adding the mounts verification service\n\nNow that you have bind mounts configured, we’ll add a safety check that prevents Docker from starting if the mounts aren’t ready. This avoids a situation where Nextcloud starts with unmounted directories and writes files to the wrong location.\n\n### Create the verification service\n\n\n    nano /etc/systemd/system/nextcloud-mounts-check.service\n\n\nAdd the following (adjust usernames to match your setup):\n\n\n    [Unit]\n    Description=Verify Nextcloud bind mounts are active\n    After=local-fs.target remote-fs.target mnt-myshare.mount\n    Requires=mnt-myshare.mount\n\n    [Service]\n    Type=oneshot\n    ExecStart=/bin/bash -c '\\\n     mountpoint -q /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/files'\n    RemainAfterExit=yes\n\n    [Install]\n    WantedBy=multi-user.target\n\n\nFor each additional user with bind mounts, add another `mountpoint` check with `&&`:\n\n\n    ExecStart=/bin/bash -c '\\\n     mountpoint -q /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/files && \\\n     mountpoint -q /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/justin/files && \\\n     mountpoint -q \"/var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/Hans Werner/files\"'\n\n\n> **Note:** Usernames with spaces need to be quoted within the bash command.\n\nEnable the service:\n\n\n    systemctl daemon-reload\n    systemctl enable nextcloud-mounts-check.service\n\n\n### Update Docker to require the verification\n\n\n    systemctl edit docker.service\n\n\nUpdate the override to include the mounts-check service:\n\n\n    [Unit]\n    Requires=mnt-myshare.mount nextcloud-mounts-check.service\n    After=mnt-myshare.mount nextcloud-mounts-check.service\n\n\nApply the changes:\n\n\n    systemctl daemon-reload\n\n\n### Test it\n\nReboot and verify everything comes up correctly:\n\n\n    reboot\n\n\nAfter the system is back up:\n\n\n    # Check the mounts verification passed\n    systemctl status nextcloud-mounts-check.service\n\n    # Check Docker started successfully\n    systemctl status docker.service\n\n    # Check the bind mounts are active\n    mount | grep nextcloud_aio_nextcloud_data\n\n\n### Maintaining the verification service\n\nWhenever you add bind mounts for a new user (Step 10), remember to:\n\n  1. Add a `mountpoint` check for that user to the service\n  2. Reload: `systemctl daemon-reload`\n\n\n\nThis is a small bit of maintenance, but it prevents silent failures where files end up in the wrong place.\n\n## Step 12 - Enabling Nextcloud backup (Optional)\n\nNextcloud AIO includes built-in borg backup. This backs up your Nextcloud configuration, database, and importantly, runs the automatic update process. We’ll configure it to back up to the Storage Box while excluding the large data directory.\n\n### Create a backup subaccount\n\nCreate a new sub account on your Storage Box with:\n\n  * Access restricted to a backup folder\n  * SSH access only (no SMB needed)\n\n\n\n### Configure borg in AIO\n\nIn the AIO interface, enter your backup destination:\n\n\n    ssh://YOURBOXID-subX@YOURBOXID-subX.your-storagebox.de:23/./nextcloud-aio-borg\n\n\nThe `.` gets you to the backup sub folder. The additional subfolder is necessary.\n\n### Set up SSH authentication\n\nBefore borg can connect, you need to add its public key to the Storage Box. Copy the key shown in the AIO interface, then on your VPS:\n\n\n    nano authorized_keys\n\n\nPaste the key and save. Then copy it to the Storage Box:\n\n\n    scp -P 23 authorized_keys YOURBOXID-subX@YOURBOXID-subX.your-storagebox.de:.ssh/authorized_keys\n\n\nNow borg should be able to connect.\n\n### Exclude the data directory\n\nTo prevent borg from backing up all user files (which would be slow and redundant), add an exclusion marker:\n\n\n    touch /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/.noaiobackup\n\n\n### Recommended: Enable VPS and Storage Box snapshots\n\nThe borg backup covers Nextcloud configuration and database, but for comprehensive data protection:\n\n  * **VPS snapshots:** Enable automatic snapshots in Hetzner Console for your VPS. This protects your encryption keys and local data.\n  * **Storage Box snapshots:** Enable automatic snapshots on your Storage Box. This protects your (encrypted) user files.\n\n\n\nTogether with borg backup, this gives you:\n\nData | Protected by\n---|---\nNextcloud config & database | Borg backup\nEncryption keys | VPS snapshots\nUser files (encrypted) | Storage Box snapshots\n\n### A note on backup strategy\n\nWith ransomware in mind, consider keeping an additional offline or off-site copy. If you have a Synology or similar NAS, you can sync via WebDAV (find the link in Nextcloud’s Files app → Settings → WebDAV) and make local versioned backups with Hyper Backup.\n\n## Step 13 - Final considerations\n\n### Expanding storage\n\n**Storage Box:** Increasing your Storage Box size immediately increases available space for user files. No action needed on the VPS — the SMB mount sees the new capacity automatically.\n\n**VPS local disk:** If you need more space for the LUKS-encrypted local disk (encryption keys, database, app data), you’ll need to:\n\n  1. Shut down the VPS\n  2. Resize the disk in Hetzner Console\n  3. Boot the VPS normally\n  4. Expand the partition and LUKS container:\n\n\n\n\n    # Grow the partition (adjust partition number as needed)\n    growpart /dev/sda 2\n\n    # Resize the LUKS container to fill available space\n    cryptsetup resize luks-<your-uuid>\n\n    # Resize the ext4 filesystem (can be done live)\n    resize2fs /dev/mapper/luks-<your-uuid>\n\n\n> **Tip:** Take a VPS snapshot before resizing, just in case.\n\n### Adding new users workflow\n\nWhen you create a new user in Nextcloud:\n\n  1. The user’s data defaults to local LUKS storage (secure by default)\n  2. If you want to offload their files to the Storage Box:\n     * Create their folders on the Storage Box (Step 10)\n     * Add fstab entries (Step 10)\n     * Add a mountpoint check to `nextcloud-mounts-check.service` (Step 11)\n     * Run `systemctl daemon-reload && mount -a`\n\n\n\n### Cleaning up leftovers\n\nIf you find leftover folders on the Storage Box from initial setup (like `files_encryption` or `appdata_*`), you can safely remove them — the real data lives on the local disk:\n\n\n    # Check what's there that shouldn't be\n    ls -la /mnt/myshare/_data/\n\n    # Remove leftovers (be careful!)\n    rm -rf /mnt/myshare/_data/files_encryption\n    rm -rf /mnt/myshare/_data/appdata_*\n\n\nOnly the per-user folders (`admin/`, `justin/`, etc.) should exist on the Storage Box.\n\n## Migration from Previous Guide\n\nIf you followed an earlier version of this guide where `NEXTCLOUD_DATADIR` pointed directly to the Storage Box (`/mnt/myshare`), your encryption keys are currently stored alongside your encrypted files. This section describes how to migrate to the more secure architecture.\n\n### Understanding the risk\n\nIn the old setup:\n\n  * Encryption keys: `/mnt/myshare/_data/files_encryption/` (on Storage Box)\n  * Encrypted files: `/mnt/myshare/_data/<user>/files/` (on Storage Box)\n\n\n\nAnyone with access to your Storage Box has both the ciphertext AND the keys.\n\n### Migration overview\n\nA. Stop Nextcloud\nB. Copy encryption keys and config to local disk\nC. Copy per-user local data\nD. Update `NEXTCLOUD_DATADIR` to use local Docker volume\nE. Set up per-user bind mounts for file storage\nF. Start Nextcloud\n\n### Step-by-step migration\n\n**A. Stop Nextcloud and take backups**\n\n\n    cd ~/containers/nextcloud\n    docker compose down\n\n\nTake a VPS snapshot and Storage Box snapshot before proceeding.\n\n**B. Create the local data directory and copy global files**\n\n\n    mkdir -p /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data\n\n\nCopy encryption keys and essential files to local disk:\n\n\n    # Copy encryption keys (CRITICAL)\n    cp -a /mnt/myshare/_data/files_encryption /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/\n\n    # Copy appdata\n    cp -a /mnt/myshare/_data/appdata_* /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/\n\n    # Copy config files\n    cp -a /mnt/myshare/_data/.htaccess /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/\n    cp -a /mnt/myshare/_data/.ncdata /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/\n    cp -a /mnt/myshare/_data/.noaiobackup /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/\n    cp -a /mnt/myshare/_data/index.html /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/\n\n\n**C. Copy per-user local data**\n\nFor each user, copy the folders that should remain local, then create empty directories for the bind mounts:\n\n\n    # Example for user 'admin'\n\n    # Copy folders that stay local\n    cp -a /mnt/myshare/_data/admin/cache /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/\n    cp -a /mnt/myshare/_data/admin/files_encryption /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/\n\n    # Create empty directories for bind mounts\n    mkdir -p /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/{files,files_trashbin,files_versions,uploads}\n\n    # Set ownership\n    chown -R www-data:www-data /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin\n\n\nRepeat for each user. For usernames with spaces:\n\n\n    cp -a \"/mnt/myshare/_data/Hans Werner/cache\" \"/var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/Hans Werner/\"\n    cp -a \"/mnt/myshare/_data/Hans Werner/files_encryption\" \"/var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/Hans Werner/\"\n    mkdir -p \"/var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/Hans Werner\"/{files,files_trashbin,files_versions,uploads}\n    chown -R www-data:www-data \"/var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/Hans Werner\"\n\n\n**D. Update compose.yml**\n\nEdit `~/containers/nextcloud/compose.yml` and change:\n\n\n    NEXTCLOUD_DATADIR: /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/\n\n\n**E. Set up bind mounts and verification service**\n\nFollow Step 10 to add fstab entries for each user, binding their Storage Box folders into the local data directory.\n\nFollow Step 11 to create `nextcloud-mounts-check.service` and update Docker dependencies.\n\n**F. Start Nextcloud**\n\n\n    systemctl daemon-reload\n    mount -a\n    cd ~/containers/nextcloud\n    docker compose up -d\n\n\n**G. Verify everything works**\n\n  * Log into Nextcloud and check that files are accessible\n  * Verify mounts are active: `mount | grep nextcloud_aio_nextcloud_data`\n  * Check encryption keys are local: `ls -la /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/files_encryption/`\n\n\n\n**H. Clean up old data on Storage Box (optional)**\n\nOnce you’ve confirmed everything works, you can remove the now-redundant files from the Storage Box:\n\n\n    # Remove global files that are now local\n    rm -rf /mnt/myshare/_data/files_encryption\n    rm -rf /mnt/myshare/_data/appdata_*\n    rm -f /mnt/myshare/_data/.htaccess\n    rm -f /mnt/myshare/_data/.ncdata\n\n    # For each user, remove the folders that are now local\n    rm -rf /mnt/myshare/_data/admin/cache\n    rm -rf /mnt/myshare/_data/admin/files_encryption\n    # Repeat for other users\n\n\nKeep the user `files/`, `files_trashbin/`, `files_versions/`, and `uploads/` folders — they contain your actual file data and are now bind-mounted.\n\n### After migration\n\nYour encryption keys now live exclusively on the LUKS-encrypted VPS disk. Even if someone gains access to your Storage Box, they cannot decrypt your files.\n\n## Conclusion\n\nCongratulations! You now have your own private Nextcloud with:\n\n  * **Proper encryption key separation** — keys on LUKS-encrypted local disk, encrypted files on expandable Storage Box\n  * **1TB+ of expandable storage** — increase Storage Box size anytime without rebuilding\n  * **Full control** — your data, your server, your rules\n\n\n\nHave fun exploring your Nextcloud — set up calendars, contacts, notes, video calls, and all the other features that make it a genuine alternative to Big Tech cloud services.\n\nThe setup requires a bit more administration than a simple “point everything at the Storage Box” approach, but the security improvement is significant. Your encryption keys never leave your control.\n\nEnjoy your private cloud!",
  "title": "Nextcloud with fully encrypted storage"
}