Get Rewarded! We will reward you with up to €50 credit on your account for every tutorial that you write and we publish!

Nextcloud with fully encrypted storage

profile picture
Author
Justin Scholz
Published
2025-08-20
Time to read
23 minutes reading time

About the author- Always striving to hit the right balance.

Introduction

In 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.

So 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.

What I wanted to achieve:

  • Low costs
  • Controllable costs (I want them predictable - not like an AWS bill)
  • European data center
  • Expandable storage without rebuilding the server
  • Fully encrypted with proper key separation
  • Syncing of files, calendar, contacts and the ability to host my own video calls à la Zoom

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.

This 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.

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.

The setup has survived stress tests of 120,000+ files, multiple reboots, and months of daily use.

What this guide covers:

  • Setting up the system with proper encryption architecture
  • Getting Nextcloud running with Nextcloud AIO
  • Configuring per-user storage offloading to the Storage Box

What this guide does NOT cover:

  • Day-to-day Nextcloud usage and administration
  • Maintaining and updating the system long-term

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.

Architecture Overview

Before diving into the setup, it helps to understand what we're building and why.

The Problem with Naive Setups

When 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:

  • Your encrypted files
  • The keys to decrypt them

This is like locking your front door and leaving the key under the doormat.

Our Solution: Key Separation

┌─────────────────────────────────────────────────────────────────────────┐
│  VPS (LUKS-encrypted disk)                                              │
│                                                                         │
│  Nextcloud Data Directory (local Docker volume)                         │
│  ├── files_encryption/    ← Master encryption keys (NEVER leave disk)  │
│  ├── appdata_*/           ← App cache and config                        │
│  │                                                                      │
│  └── <username>/                                                        │
│      ├── files_encryption/ ← Per-user keys (stay local)                │
│      ├── cache/            ← User cache (stays local)                  │
│      ├── files/           ─┐                                            │
│      ├── files_trashbin/   ├─ Bind mounts to Storage Box               │
│      ├── files_versions/   │  (only encrypted content travels there)   │
│      └── uploads/         ─┘                                            │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    │ bind mounts (per-user, 4 folders each)
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  Storage Box (SMB mount)                                                │
│                                                                         │
│  └── <username>/                                                        │
│      ├── files/           ← Encrypted blobs only                        │
│      ├── files_trashbin/  ← Encrypted                                   │
│      ├── files_versions/  ← Encrypted                                   │
│      └── uploads/         ← Encrypted                                   │
└─────────────────────────────────────────────────────────────────────────┘

Security Properties

Component Location Protected by
Encryption keys VPS local disk LUKS full-disk encryption (boot passphrase)
Encrypted files Storage Box Nextcloud server-side encryption
Nextcloud config VPS local disk LUKS full-disk encryption

What this means in practice:

  • Storage Box compromised? Attacker gets encrypted blobs, useless without keys.
  • VPS disk stolen while powered off? Protected by LUKS encryption.
  • 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).

The Trade-off: Per-User Setup

This 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.

Why I care about encryption

It 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".

Prerequisites

  • VPS on Hetzner (in their parlance a cloud server) with 4GB of RAM, 2 CPU cores and 40GB of NVME storage
  • A Hetzner Storage Box — essentially a 1TB+ NAS in the cloud for an incredibly low price
  • A domain name with the ability to set A and AAAA records
  • Basic familiarity with Linux command line and SSH

Both resources should be in the same Hetzner region to keep traffic internal and latency low.

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.

Step 1 - Creating the server

Create 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.

After you created the server, follow this guide:

How to install Ubuntu 24.04 with full disk encryption

Beware of the special section for Debian 13 with the following caveats:

  • 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.

  • Your setup.conf should look like this:

    CRYPTPASSWORD secret
    DRIVE1 /dev/sda
    BOOTLOADER grub
    HOSTNAME host.example.com
    PART /boot ext4 1G
    PART /     ext4 all crypt
    IMAGE /root/images/Debian-1302-trixie-amd64-base.tar.gz
    SSHKEYS_URL /tmp/authorized_keys
  • 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".

Once 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.

Step 2 - Booting into the server

Now you can proceed with setting up your VPS. You can follow this guide for a basic setup:

Initial Server Setup with Ubuntu

Let's open the relevant ports on UFW for all the stuff:

sudo ufw default deny incoming
sudo ufw default allow outgoing

# HTTP for ACME/Nextcloud challenge
sudo ufw allow 80/tcp comment 'ACME-HTTP-Nextcloud'

# HTTPS for Apache container (HTTP/1.1 & HTTP/2)
sudo ufw allow 443/tcp comment 'Apache-HTTPS'

# HTTP/3 (QUIC) for Apache container
sudo ufw allow 443/udp comment 'Apache-HTTP3-QUIC'

# Admin interface of master container
sudo ufw allow 8443/tcp comment 'Master-UI-HTTPS'

# TURN server (Talk container) – TCP & UDP
sudo ufw allow 3478/tcp comment 'TURN-TCP'
sudo ufw allow 3478/udp comment 'TURN-UDP'

Once that is done, you can check your IPv4 address (if you ordered one) and IPv6 by running this command on the server:

ip -6 addr show

Your interface is probably enp1s0.

Step 3 - Pointing DNS to your server

If 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.

Once that is done, let's go back to your VPS.

Step 4 - Creating the SMB mount to the Storage Box

Back on the Hetzner VPS, go to a root shell with sudo su. Stay in the root shell during the rest of the guide.

This 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.

First, install SMB support:

apt update
apt install cifs-utils

Create the mount point (replace myshare with whatever you want to call it):

mkdir -p /mnt/myshare

Create a credentials file (our disk is encrypted, so storing credentials here is acceptable):

mkdir -p /etc/cifs-creds
nano /etc/cifs-creds/myshare

Add:

username=your_smb_username
password=your_smb_password

This username and password comes from your Storage Box sub account.

Secure the credentials file:

chmod 600 /etc/cifs-creds/myshare

Now create a systemd .mount unit:

nano /etc/systemd/system/mnt-myshare.mount

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.

Add the following content:

Replace YOURSTORAGEBOXUSER-subX with your actual account name.

[Unit]
Description=Mount SMB Share myshare
DefaultDependencies=no
After=network-online.target
Wants=network-online.target

[Mount]
What=//YOURSTORAGEBOXUSER-subX.your-storagebox.de/YOURSTORAGEBOXUSER-subX
Where=/mnt/myshare
Type=cifs
Options=credentials=/etc/cifs-creds/myshare,iocharset=utf8,uid=33,gid=33,seal,vers=3.1.1,_netdev
TimeoutSec=30

[Install]
WantedBy=multi-user.target

A few notes on this unit:

  • DefaultDependencies=no prevents systemd from adding automatic dependencies that can cause ordering cycles with network-dependent mounts
  • uid=33,gid=33 sets ownership to www-data (the user Nextcloud runs as)
  • seal enables SMB encryption in transit
  • _netdev tells the system this is a network mount

Activate it:

systemctl daemon-reload
systemctl enable --now mnt-myshare.mount

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.

Verify it mounted:

mount | grep myshare
ls -la /mnt/myshare

You should see an empty directory owned by www-data.

Step 5 - Installing Docker

Follow the official guide to install Docker: https://docs.docker.com/engine/install/debian/

Once installed, make Docker wait for the SMB mount to be ready:

systemctl edit docker.service

Add above the line that says ### Edits below this comment will be discarded:

[Unit]
Requires=mnt-myshare.mount
After=mnt-myshare.mount

Reload systemd:

systemctl daemon-reload

Finally, 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

You won't be able to verify IPv6 works until Nextcloud is running — that comes later.

Step 6 - Preparing Nextcloud AIO

I recommend running Nextcloud AIO through compose. This makes it easier to track changes and see the startup parameters.

mkdir -p ~/containers/nextcloud
cd ~/containers/nextcloud
nano compose.yml

Start with the official compose file from AIO:

github.com/nextcloud/all-in-one/blob/main/compose.yaml

Make the following adjustments:

  • Uncomment environment:

  • Uncomment NEXTCLOUD_DATADIR: and set it explicitly to the local Docker volume:

    NEXTCLOUD_DATADIR: /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/

    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.

  • Uncomment NEXTCLOUD_MAX_TIME: and increase the value (I use 7200)

  • Uncomment NEXTCLOUD_MEMORY_LIMIT: and set it to 2048

  • Uncomment environment: above NEXTCLOUD_MAX_TIME: and NEXTCLOUD_MEMORY_LIMIT:

  • If you plan to use full-text search, uncomment FULLTEXTSEARCH_JAVA_OPTIONS: and set reasonable values:

    FULLTEXTSEARCH_JAVA_OPTIONS: "-Xms1024M -Xmx2048M"

Step 7 - Setting up swap to help with memory pressure

Before we start running any containers, let's enable Swap on the server so that we don't run out of RAM:

fallocate -l 4G /swapfile
dd if=/dev/zero of=/swapfile bs=1M count=4096 status=progress
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

Let's add it to system boot up:

echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Encourage a bit more swapping:

echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf

You can verify it worked by checking:

htop

It should show memory and swap separately. Swap should show x/4.00G.

Especially if you are on the 2GB RAM box, 4G of Swap were a good idea when also running fulltext search

Step 8 - Starting it up

Make sure you're in the right directory:

If you haven't yet adjusted the DNS on your domain registrar to point to your Hetzner server, now is the time.

cd ~/containers/nextcloud
docker compose up -d

Now 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.

If https://example.com:8443 doesn't load for you, try https://example.com:8080 instead.

Follow 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.

Step 9 - Enabling encryption

Once logged in to the Nextcloud, you should:

Description
Encrypt all files Head to your user icon on the top right => Admin settings => security settings => server side encryption => switch it on
Activate the encryption module Head to your user icon on the top right => apps => deactivated apps and activate the "default encryption module"
Encrypt user home folders Head back to admin settings => security settings => encryption
Check that the checkbox is ticked for "encrypt user home folders"

If you want, you can stop your containers, enable fulltextsearch and some other containers.

Step 10 - Setting up per-user storage offloading

Now 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.

How it works: Each Nextcloud user has four storage folders:

  • files/ — their actual files
  • files_trashbin/ — deleted files (before permanent deletion)
  • files_versions/ — version history
  • uploads/ — temporary storage during uploads

We 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.

Setting up a user (example: admin)

First, check that Nextcloud has created the user's folder structure:

ls -la /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/

You should see files/ at minimum. If the other folders don't exist yet, they'll be created when needed.

Create the corresponding folder structure on the Storage Box:

mkdir -p /mnt/myshare/_data/admin/{files,files_trashbin,files_versions,uploads}
chown -R www-data:www-data /mnt/myshare/_data/admin

Create the target directories in the Docker volume (if they don't exist):

mkdir -p /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/{files,files_trashbin,files_versions,uploads}
chown -R www-data:www-data /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin

Add the bind mounts to /etc/fstab:

nano /etc/fstab

Add these lines (adjust myshare to your mount name):

# Nextcloud user: admin
/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
/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
/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
/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

Understanding the mount options:

  • bind — this is a bind mount, not a device mount
  • nofail — boot continues even if mount fails (prevents boot hang)
  • x-systemd.requires-mounts-for=/mnt/myshare — wait for SMB mount first
  • x-systemd.device-timeout=30s — timeout if mount takes too long

Mount them now:

systemctl daemon-reload
mount -a

Verify they're active:

mount | grep admin/files
mount | grep admin/uploads

You should see four bind mounts.

Usernames with spaces

If a username contains spaces (like "Hans Werner"), escape spaces with \040 in fstab:

/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

What about existing files?

If the user already has files in Nextcloud before you set up the bind mount:

  1. Stop Nextcloud containers:
    cd ~/containers/nextcloud && docker compose down
  2. Move existing files to Storage Box:
    mv /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/files/* /mnt/myshare/_data/admin/files/
  3. Set up bind mounts as described above
  4. Start Nextcloud:
    docker compose up -d
  5. Optionally, run a file scan:
    docker exec -it nextcloud-aio-nextcloud php occ files:scan admin

Important notes

  • 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.
  • Don't bind-mount files_encryption! That folder contains encryption keys and must stay on the local disk.
  • Changes are per-user. You can have some users on local storage and others offloaded to the Storage Box.

Step 11 - Adding the mounts verification service

Now 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.

Create the verification service

nano /etc/systemd/system/nextcloud-mounts-check.service

Add the following (adjust usernames to match your setup):

[Unit]
Description=Verify Nextcloud bind mounts are active
After=local-fs.target remote-fs.target mnt-myshare.mount
Requires=mnt-myshare.mount

[Service]
Type=oneshot
ExecStart=/bin/bash -c '\
  mountpoint -q /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/files'
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

For each additional user with bind mounts, add another mountpoint check with &&:

ExecStart=/bin/bash -c '\
  mountpoint -q /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/files && \
  mountpoint -q /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/justin/files && \
  mountpoint -q "/var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/Hans Werner/files"'

Note: Usernames with spaces need to be quoted within the bash command.

Enable the service:

systemctl daemon-reload
systemctl enable nextcloud-mounts-check.service

Update Docker to require the verification

systemctl edit docker.service

Update the override to include the mounts-check service:

[Unit]
Requires=mnt-myshare.mount nextcloud-mounts-check.service
After=mnt-myshare.mount nextcloud-mounts-check.service

Apply the changes:

systemctl daemon-reload

Test it

Reboot and verify everything comes up correctly:

reboot

After the system is back up:

# Check the mounts verification passed
systemctl status nextcloud-mounts-check.service

# Check Docker started successfully
systemctl status docker.service

# Check the bind mounts are active
mount | grep nextcloud_aio_nextcloud_data

Maintaining the verification service

Whenever you add bind mounts for a new user (Step 10), remember to:

  1. Add a mountpoint check for that user to the service
  2. Reload: systemctl daemon-reload

This is a small bit of maintenance, but it prevents silent failures where files end up in the wrong place.

Step 12 - Enabling Nextcloud backup (Optional)

Nextcloud 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.

Create a backup subaccount

Create a new sub account on your Storage Box with:

  • Access restricted to a backup folder
  • SSH access only (no SMB needed)

Configure borg in AIO

In the AIO interface, enter your backup destination:

ssh://YOURBOXID-subX@YOURBOXID-subX.your-storagebox.de:23/./nextcloud-aio-borg

The . gets you to the backup sub folder. The additional subfolder is necessary.

Set up SSH authentication

Before 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:

nano authorized_keys

Paste the key and save. Then copy it to the Storage Box:

scp -P 23 authorized_keys YOURBOXID-subX@YOURBOXID-subX.your-storagebox.de:.ssh/authorized_keys

Now borg should be able to connect.

Exclude the data directory

To prevent borg from backing up all user files (which would be slow and redundant), add an exclusion marker:

touch /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/.noaiobackup

The borg backup covers Nextcloud configuration and database, but for comprehensive data protection:

  • VPS snapshots: Enable automatic snapshots in Hetzner Console for your VPS. This protects your encryption keys and local data.
  • Storage Box snapshots: Enable automatic snapshots on your Storage Box. This protects your (encrypted) user files.

Together with borg backup, this gives you:

Data Protected by
Nextcloud config & database Borg backup
Encryption keys VPS snapshots
User files (encrypted) Storage Box snapshots

A note on backup strategy

With 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.

Step 13 - Final considerations

Expanding storage

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.

VPS local disk: If you need more space for the LUKS-encrypted local disk (encryption keys, database, app data), you'll need to:

  1. Shut down the VPS
  2. Resize the disk in Hetzner Console
  3. Boot the VPS normally
  4. Expand the partition and LUKS container:
# Grow the partition (adjust partition number as needed)
growpart /dev/sda 2

# Resize the LUKS container to fill available space
cryptsetup resize luks-<your-uuid>

# Resize the ext4 filesystem (can be done live)
resize2fs /dev/mapper/luks-<your-uuid>

Tip: Take a VPS snapshot before resizing, just in case.

Adding new users workflow

When you create a new user in Nextcloud:

  1. The user's data defaults to local LUKS storage (secure by default)
  2. If you want to offload their files to the Storage Box:
    • Create their folders on the Storage Box (Step 10)
    • Add fstab entries (Step 10)
    • Add a mountpoint check to nextcloud-mounts-check.service (Step 11)
    • Run systemctl daemon-reload && mount -a

Cleaning up leftovers

If 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:

# Check what's there that shouldn't be
ls -la /mnt/myshare/_data/

# Remove leftovers (be careful!)
rm -rf /mnt/myshare/_data/files_encryption
rm -rf /mnt/myshare/_data/appdata_*

Only the per-user folders (admin/, justin/, etc.) should exist on the Storage Box.

Migration from Previous Guide

If 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.

Understanding the risk

In the old setup:

  • Encryption keys: /mnt/myshare/_data/files_encryption/ (on Storage Box)
  • Encrypted files: /mnt/myshare/_data/<user>/files/ (on Storage Box)

Anyone with access to your Storage Box has both the ciphertext AND the keys.

Migration overview

A. Stop Nextcloud
B. Copy encryption keys and config to local disk
C. Copy per-user local data
D. Update NEXTCLOUD_DATADIR to use local Docker volume
E. Set up per-user bind mounts for file storage
F. Start Nextcloud

Step-by-step migration

A. Stop Nextcloud and take backups

cd ~/containers/nextcloud
docker compose down

Take a VPS snapshot and Storage Box snapshot before proceeding.

B. Create the local data directory and copy global files

mkdir -p /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data

Copy encryption keys and essential files to local disk:

# Copy encryption keys (CRITICAL)
cp -a /mnt/myshare/_data/files_encryption /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/

# Copy appdata
cp -a /mnt/myshare/_data/appdata_* /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/

# Copy config files
cp -a /mnt/myshare/_data/.htaccess /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/
cp -a /mnt/myshare/_data/.ncdata /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/
cp -a /mnt/myshare/_data/.noaiobackup /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/
cp -a /mnt/myshare/_data/index.html /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/

C. Copy per-user local data

For each user, copy the folders that should remain local, then create empty directories for the bind mounts:

# Example for user 'admin'

# Copy folders that stay local
cp -a /mnt/myshare/_data/admin/cache /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/
cp -a /mnt/myshare/_data/admin/files_encryption /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/

# Create empty directories for bind mounts
mkdir -p /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin/{files,files_trashbin,files_versions,uploads}

# Set ownership
chown -R www-data:www-data /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/admin

Repeat for each user. For usernames with spaces:

cp -a "/mnt/myshare/_data/Hans Werner/cache" "/var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/Hans Werner/"
cp -a "/mnt/myshare/_data/Hans Werner/files_encryption" "/var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/Hans Werner/"
mkdir -p "/var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/Hans Werner"/{files,files_trashbin,files_versions,uploads}
chown -R www-data:www-data "/var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/Hans Werner"

D. Update compose.yml

Edit ~/containers/nextcloud/compose.yml and change:

NEXTCLOUD_DATADIR: /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/

E. Set up bind mounts and verification service

Follow Step 10 to add fstab entries for each user, binding their Storage Box folders into the local data directory.

Follow Step 11 to create nextcloud-mounts-check.service and update Docker dependencies.

F. Start Nextcloud

systemctl daemon-reload
mount -a
cd ~/containers/nextcloud
docker compose up -d

G. Verify everything works

  • Log into Nextcloud and check that files are accessible
  • Verify mounts are active: mount | grep nextcloud_aio_nextcloud_data
  • Check encryption keys are local: ls -la /var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/files_encryption/

H. Clean up old data on Storage Box (optional)

Once you've confirmed everything works, you can remove the now-redundant files from the Storage Box:

# Remove global files that are now local
rm -rf /mnt/myshare/_data/files_encryption
rm -rf /mnt/myshare/_data/appdata_*
rm -f /mnt/myshare/_data/.htaccess
rm -f /mnt/myshare/_data/.ncdata

# For each user, remove the folders that are now local
rm -rf /mnt/myshare/_data/admin/cache
rm -rf /mnt/myshare/_data/admin/files_encryption
# Repeat for other users

Keep the user files/, files_trashbin/, files_versions/, and uploads/ folders — they contain your actual file data and are now bind-mounted.

After migration

Your 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.

Conclusion

Congratulations! You now have your own private Nextcloud with:

  • Proper encryption key separation — keys on LUKS-encrypted local disk, encrypted files on expandable Storage Box
  • 1TB+ of expandable storage — increase Storage Box size anytime without rebuilding
  • Full control — your data, your server, your rules

Have 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.

The 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.

Enjoy your private cloud!

License: MIT
Want to contribute?

Get Rewarded: Get up to €50 in credit! Be a part of the community and contribute. Do it for the money. Do it for the bragging rights. And do it to teach others!

Report Issue
Try Hetzner Cloud

Get €20/$20 free credit!

Valid until: 31 December 2026 Valid for: 3 months and only for new customers
Get started
Want to contribute?

Get Rewarded: Get up to €50 credit on your account for every tutorial you write and we publish!

Find out more