Deploying Nextcloud with Docker and External Storage

Deploying Nextcloud with Docker and External Storage

We earn commissions when you shop through the links on this page, at no additional cost to you.

Nextcloud is my go-to self-hosted file sync solution, but the real power kicks in when you connect external storage. In this guide, I'll walk you through deploying Nextcloud in Docker and wiring up NAS shares, local mounts, and even object storage. By the end, you'll have a bulletproof personal cloud that scales far beyond your container's local filesystem.

Why Docker for Nextcloud?

I've run Nextcloud on bare metal, in systemd services, and in containers. Docker wins every time because it isolates dependencies, makes upgrades trivial, and lets you spin up a test instance in seconds. When you need to test a major upgrade or plugin, you just clone your compose file, change the port, and run it on the side. Plus, Docker lets you version your entire setup in git—configuration as code.

The tricky part? External storage. Nextcloud itself runs in a container with its own filesystem, but your actual files probably live on a NAS, external drive, or object store. Getting that wiring right means properly mounting volumes and configuring Nextcloud's External Storage app. Get it wrong, and you'll have permission hell or your storage won't mount at all.

Infrastructure Setup: VPS or Homelab?

For a personal Nextcloud instance, I run it on a modest VPS. A $40/year server from RackNerd (around 1 vCore, 2GB RAM, 30GB SSD) handles 1-2 users comfortably. You can grab deals like that at racknerd.com/NewYear/ and use the storage for Nextcloud's MariaDB and app data, while mounting your files from an external NAS via NFS or SMB.

If you're in a homelab, run Nextcloud on your existing Docker host and mount your NAS storage directly. If you're remote-only, a cheap VPS with external storage (Backblaze B2, Wasabi, or Minio) is your best bet.

Docker Compose Setup

Here's a production-ready docker-compose.yml for Nextcloud with MariaDB, Redis for caching, and volume mounts for external storage:

version: '3.8'

services:
  nextcloud-db:
    image: mariadb:11
    container_name: nextcloud-db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: nextcloud
      MYSQL_USER: nextcloud
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - nextcloud-db:/var/lib/mysql
    networks:
      - nextcloud-net
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--su-mysql"]
      interval: 10s
      timeout: 5s
      retries: 5

  nextcloud-cache:
    image: redis:7-alpine
    container_name: nextcloud-cache
    restart: always
    command: redis-server --appendonly yes
    volumes:
      - nextcloud-cache:/data
    networks:
      - nextcloud-net
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

  nextcloud:
    image: nextcloud:28
    container_name: nextcloud
    restart: always
    depends_on:
      nextcloud-db:
        condition: service_healthy
      nextcloud-cache:
        condition: service_healthy
    environment:
      NEXTCLOUD_ADMIN_USER: admin
      NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_PASSWORD}
      MYSQL_DATABASE: nextcloud
      MYSQL_USER: nextcloud
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_HOST: nextcloud-db
      REDIS_HOST: nextcloud-cache
      REDIS_PORT: 6379
      TRUSTED_PROXIES: 127.0.0.1
      OVERWRITE_PROTO: https
    ports:
      - "8080:80"
    volumes:
      - nextcloud-app:/var/www/html
      - ./config:/var/www/html/config
      - /mnt/nas-storage:/mnt/external-nas
      - /mnt/local-media:/mnt/local-media
    networks:
      - nextcloud-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/status.php"]
      interval: 30s
      timeout: 10s
      retries: 3

volumes:
  nextcloud-app:
  nextcloud-db:
  nextcloud-cache:

networks:
  nextcloud-net:
    driver: bridge

Before you start, create a .env file with strong passwords:

DB_ROOT_PASSWORD=your_very_strong_root_password_here
DB_PASSWORD=your_strong_nextcloud_db_password
NEXTCLOUD_PASSWORD=your_strong_admin_password

Then spin it up:

docker-compose up -d

Give it a minute for the database to initialize, then check status with docker-compose logs nextcloud.

Watch out: Never commit .env to git. Add it to .gitignore immediately. If you're managing this in source control, use Docker secrets or a proper secrets manager like Vault or 1Password.

Mounting External Storage in Docker

The volumes section of that compose file mounts two external paths: /mnt/nas-storage (your NAS share) and /mnt/local-media (a local drive or mount point). These paths must exist on your host before Nextcloud starts.

If you're mounting a NAS share via NFS, set it up first on your host:

sudo mkdir -p /mnt/nas-storage
sudo mount -t nfs 192.168.1.100:/share/nextcloud /mnt/nas-storage

Make sure the mount is readable by the www-data user inside the container (UID 33). Check your NAS permissions:

ls -la /mnt/nas-storage

If you see permission denied, you'll need to adjust NAS export settings or use uid=33,gid=33 in your NFS mount options.

For SMB/CIFS mounts (Windows shares), use:

sudo mount -t cifs //192.168.1.100/nextcloud /mnt/nas-storage \
  -o username=nasuser,password=naspass,uid=33,gid=33

To make mounts persistent across reboots, add them to /etc/fstab.

Configuring External Storage in Nextcloud

Once Nextcloud is running, log in as admin and navigate to Settings → Administration → External Storage. You'll need to enable the External Storage app first.

For the paths you mounted in Docker, create a new external storage entry:

If you're using S3-compatible storage (Wasabi, Backblaze B2, or Minio), choose Amazon S3 and enter:

Click Check to verify the connection. If it fails, check Docker logs for permission issues:

docker-compose logs nextcloud | grep -i storage

Performance Tuning

Redis caching is crucial when you're pulling files from a slow NAS. I also recommend enabling the Preview Generator app to pre-generate thumbnails instead of generating them on-the-fly:

docker-compose exec nextcloud php occ app:enable preview
docker-compose exec nextcloud php occ preview:generate-all

Add a cron job to regenerate previews nightly:

docker-compose exec nextcloud php occ preview:generate-all -vv

Also enable trusted_domains in config/config.php if you access Nextcloud from multiple domains:

'trusted_domains' =>
  array (
    0 => 'localhost',
    1 => 'nextcloud.example.com',
    2 => '192.168.1.50',
  ),
Tip: Use Caddy as a reverse proxy in front of Nextcloud. It handles SSL termination, HSTS headers, and proxy protocol automatically. Set OVERWRITE_PROTO: https in your compose file and let Caddy handle the HTTPS.

Backup Strategy

Your Nextcloud data lives in three places: the container volume, the external storage mount, and the database. Back them all up:

#!/bin/bash
# Backup script for Nextcloud
BACKUP_DIR="/backups/nextcloud"
mkdir -p $BACKUP_DIR

# Database backup
docker-compose exec -T nextcloud-db mysqldump \
  -u nextcloud -p${DB_PASSWORD} nextcloud > \
  $BACKUP_DIR/nextcloud-db-$(date +%Y%m%d).sql

# Config and app data
tar -czf $BACKUP_DIR/nextcloud-app-$(date +%Y%m%d).tar.gz \
  ./config nextcloud-app:/var/www/html

# External storage (rsync to avoid copying everything)
rsync -av /mnt/nas-storage/ /backups/nextcloud-storage/

echo "Backup complete"

Schedule this with cron:

0 2 * * * /path/to/backup-nextcloud.sh

Troubleshooting Common Issues

Permission denied on external storage: Check that the mount is readable by UID 33 (www-data). If using NFS, add no_root_squash to your NAS export and mount with uid=33,gid=33.

Slow file sync: Enable Redis (already in the compose file) and increase the number of background workers. Edit config.php and set 'backgroundjobs_mode' => 'cron', then add a Docker exec cron task.

Database won't start: Check that no other MariaDB instance is running on port 3306. If upgrading from a much older Nextcloud version, the database schema might need migration—the container logs will tell you.

Can't reach Nextcloud from outside: Make sure your reverse proxy is set up correctly and trusted_domains includes your external domain. Don't forget to set OVERWRITE_PROTO: https if using HTTPS.

Going Further

Once you have this running, consider adding:

The external storage setup I've shown scales to petabytes—you're not limited by Nextcloud's container size, only by your network and storage backend.

Discussion