LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Nextcloud: Your Own Google Drive (That You Actually Control)

nextcloudhomelabself-hostedstorageprivacydockerredis

Google Drive, iCloud, and Dropbox are convenient until they aren’t — a price hike, a privacy concern, a terms-of-service change, or simply the realization that your family photos and financial documents live on someone else’s server. Nextcloud is the self-hosted alternative: a full-featured file sync and collaboration platform that runs on your hardware, under your control.

This guide covers the full deployment lifecycle: Docker Compose with PostgreSQL and Redis, performance tuning that keeps Nextcloud responsive even with thousands of files, external storage backends for NAS integration, Nextcloud Office for in-browser document editing, mobile sync configuration, and the maintenance tasks that keep everything running smoothly.

What Nextcloud Actually Is

Nextcloud is not just file sync. The core is a file server with desktop and mobile clients, but the app ecosystem extends it considerably:

  • Files: sync, share, versioning, trash, external storage mounts
  • Nextcloud Office (Collabora Online): in-browser editing of .docx, .xlsx, .pptx
  • Talk: video calls, chat, screen sharing (WebRTC)
  • Calendar: CalDAV server, syncs with iOS/Android/Thunderbird
  • Contacts: CardDAV server
  • Photos: gallery with face recognition and automatic albums
  • Notes: Markdown notes synced across devices
  • Tasks: to-do lists with CalDAV sync

For most people, Files + Calendar + Contacts covers the core Google Workspace replacement. This guide focuses on getting that foundation solid before adding extras.

Architecture

A production-grade Nextcloud deployment has four components:

Client (browser/desktop/mobile)
         │
         │ HTTPS
         ▼
    [Traefik/Nginx]      ← TLS termination, reverse proxy
         │
         ▼
    [Nextcloud FPM]      ← PHP-FPM application server
         │         │
         ▼         ▼
   [PostgreSQL]  [Redis]  ← Database + session/cache/locking
         │
         ▼
   [File storage]         ← Local disk, NFS, S3

The critical detail: Nextcloud is a PHP application. Its performance characteristics are PHP-like — each request forks a PHP-FPM worker, does file I/O, queries the database, and returns. Redis is essential: it handles file locking (preventing corruption when multiple clients sync simultaneously), session storage, and APCu-style opcode/data caching.

Docker Compose Setup

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
# docker-compose.yml
services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U nextcloud"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: >
      redis-server
      --requirepass ${REDIS_PASSWORD}
      --maxmemory 256mb
      --maxmemory-policy allkeys-lru
      --save ""
      --appendonly no
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  nextcloud:
    image: nextcloud:28-fpm-alpine
    restart: unless-stopped
    environment:
      POSTGRES_HOST: db
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      REDIS_HOST: redis
      REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
      NEXTCLOUD_ADMIN_USER: ${ADMIN_USER}
      NEXTCLOUD_ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_DOMAIN}
      OVERWRITEPROTOCOL: https
      OVERWRITEHOST: ${NEXTCLOUD_DOMAIN}
      # PHP memory limit
      PHP_MEMORY_LIMIT: 1G
      PHP_UPLOAD_LIMIT: 16G
    volumes:
      - nextcloud_data:/var/www/html
      - ./config/php-custom.ini:/usr/local/etc/php/conf.d/custom.ini:ro
      - /mnt/nas/nextcloud:/var/www/html/data  # External data directory
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    volumes:
      - nextcloud_data:/var/www/html:ro
      - ./config/nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "8080:80"
    depends_on:
      - nextcloud
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nextcloud.rule=Host(`${NEXTCLOUD_DOMAIN}`)"
      - "traefik.http.routers.nextcloud.entrypoints=websecure"
      - "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
      - "traefik.http.routers.nextcloud.middlewares=nextcloud-headers,nextcloud-redirect"
      # Security headers
      - "traefik.http.middlewares.nextcloud-headers.headers.stsSeconds=31536000"
      - "traefik.http.middlewares.nextcloud-headers.headers.stsIncludeSubdomains=true"
      - "traefik.http.middlewares.nextcloud-headers.headers.stsPreload=true"
      - "traefik.http.middlewares.nextcloud-headers.headers.forceSTSHeader=true"
      # CalDAV/CardDAV redirect
      - "traefik.http.middlewares.nextcloud-redirect.redirectregex.regex=/.well-known/(card|cal)dav"
      - "traefik.http.middlewares.nextcloud-redirect.redirectregex.replacement=/remote.php/dav/"
      - "traefik.http.middlewares.nextcloud-redirect.redirectregex.permanent=true"

  # Cron for background tasks
  cron:
    image: nextcloud:28-fpm-alpine
    restart: unless-stopped
    entrypoint: /cron.sh
    volumes:
      - nextcloud_data:/var/www/html
      - /mnt/nas/nextcloud:/var/www/html/data
    environment:
      POSTGRES_HOST: db
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      REDIS_HOST: redis
      REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
    depends_on:
      - nextcloud

volumes:
  db_data:
  redis_data:
  nextcloud_data:
1
2
3
4
5
6
# .env
POSTGRES_PASSWORD=change-this-strong-password
REDIS_PASSWORD=change-this-too
ADMIN_USER=admin
ADMIN_PASSWORD=initial-admin-password
NEXTCLOUD_DOMAIN=cloud.yourdomain.com

Nginx Configuration

Nextcloud requires specific Nginx config for .well-known redirects, large file uploads, and PHP-FPM proxying:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# config/nginx.conf
worker_processes auto;
error_log /var/log/nginx/error.log warn;

events {
    worker_connections 1024;
    use epoll;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;

    # Nextcloud: disable access log for static assets
    log_format main '$remote_addr - $request_time "$request" $status $body_bytes_sent';
    access_log /var/log/nginx/access.log main;

    upstream php-handler {
        server nextcloud:9000;
    }

    server {
        listen 80;
        server_name _;

        root /var/www/html;
        index index.php index.html;

        # Max upload size — must match PHP_UPLOAD_LIMIT
        client_max_body_size 16G;
        client_body_timeout 300s;

        # Security headers (set here or in Traefik middleware)
        add_header X-Content-Type-Options nosniff always;
        add_header X-Robots-Tag "noindex, nofollow" always;
        add_header X-Frame-Options SAMEORIGIN always;

        # CalDAV/CardDAV discovery
        location = /.well-known/carddav {
            return 301 /remote.php/dav/;
        }
        location = /.well-known/caldav {
            return 301 /remote.php/dav/;
        }
        location = /.well-known/webfinger {
            return 301 /index.php/.well-known/webfinger;
        }
        location = /.well-known/nodeinfo {
            return 301 /index.php/.well-known/nodeinfo;
        }

        # Static files — serve directly from nginx
        location ~ \.(?:css|js|svg|gif|png|jpg|ico|wasm|tflite|map|ogg|flac)$ {
            try_files $uri /index.php$request_uri;
            expires 6M;
            access_log off;
        }

        # PHP
        location ~ \.php(?:$|/) {
            fastcgi_split_path_info ^(.+?\.php)(/.*)$;
            set $path_info $fastcgi_path_info;

            try_files $fastcgi_script_name =404;

            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param PATH_INFO $path_info;
            fastcgi_param modHeadersAvailable true;
            fastcgi_param front_controller_active true;

            fastcgi_pass php-handler;
            fastcgi_intercept_errors on;
            fastcgi_request_buffering off;

            # Timeout for large file uploads
            fastcgi_read_timeout 3600;
            fastcgi_send_timeout 3600;

            include fastcgi_params;
        }

        location / {
            try_files $uri $uri/ /index.php$request_uri;
        }
    }
}

PHP Configuration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# config/php-custom.ini
; Memory
memory_limit = 1G
post_max_size = 16G
upload_max_filesize = 16G

; Timeouts (for large uploads)
max_execution_time = 3600
max_input_time = 3600

; OPcache — critical for Nextcloud performance
opcache.enable = 1
opcache.enable_cli = 1
opcache.interned_strings_buffer = 64
opcache.max_accelerated_files = 10000
opcache.memory_consumption = 256
opcache.save_comments = 1
opcache.revalidate_freq = 60

; APCu — in-memory cache for Nextcloud's internal data
apm.enabled = 1
apc.shm_size = 128M
apc.ttl = 7200

Nextcloud Configuration

After the first startup, fine-tune config/config.php. Nextcloud auto-generates this, but several settings improve performance and security:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php
// /var/www/html/config/config.php (or use environment vars for Docker)
$CONFIG = array(
  // Redis for file locking, session storage, and caching
  'memcache.local' => '\\OC\\Memcache\\APCu',
  'memcache.distributed' => '\\OC\\Memcache\\Redis',
  'memcache.locking' => '\\OC\\Memcache\\Redis',
  'redis' => array(
    'host' => 'redis',
    'port' => 6379,
    'password' => getenv('REDIS_PASSWORD'),
    'timeout' => 1.5,
    'dbindex' => 0,
  ),

  // Database
  'dbtype' => 'pgsql',
  'dbhost' => 'db',
  'dbname' => 'nextcloud',
  'dbuser' => 'nextcloud',

  // Trusted domains and proxy
  'trusted_domains' => array('cloud.yourdomain.com'),
  'overwrite.cli.url' => 'https://cloud.yourdomain.com',
  'overwriteprotocol' => 'https',
  'trusted_proxies' => array('172.16.0.0/12'),  // Docker network
  'forwarded_for_headers' => array('HTTP_X_FORWARDED_FOR'),

  // Default phone region (for phone number validation)
  'default_phone_region' => 'US',

  // Logging
  'loglevel' => 1,  // 0=debug, 1=info, 2=warn, 3=error, 4=fatal
  'logfile' => '/var/www/html/data/nextcloud.log',
  'log_rotate_size' => 104857600,  // 100MB

  // Preview generation (thumbnails)
  'preview_max_x' => 2048,
  'preview_max_y' => 2048,
  'preview_max_scale_factor' => 1,
  'enabledPreviewProviders' => array(
    'OC\\Preview\\PNG',
    'OC\\Preview\\JPEG',
    'OC\\Preview\\GIF',
    'OC\\Preview\\HEIC',
    'OC\\Preview\\BMP',
    'OC\\Preview\\XBitmap',
    'OC\\Preview\\MP3',
    'OC\\Preview\\TXT',
    'OC\\Preview\\MarkDown',
    'OC\\Preview\\OpenDocument',
    'OC\\Preview\\Krita',
  ),

  // Maintenance
  'maintenance_window_start' => 1,  // Start maintenance tasks at 1am UTC

  // Activity app
  'activity_expire_days' => 90,

  // Performance
  'filesystem_check_changes' => 0,  // Don't stat files on every access
  'filelocking.enabled' => true,    // Use Redis file locking
  'filelocking.ttl' => 3600,
);

Performance Tuning

Redis Caching Layers

Nextcloud uses Redis for three distinct purposes — understanding each helps tune it correctly:

File locking (memcache.locking): Prevents two clients from corrupting a file by writing simultaneously. Every file operation acquires a Redis lock. Low-latency Redis is critical here — a slow Redis makes Nextcloud feel sluggish under sync load.

Distributed cache (memcache.distributed): Caches database query results, user preferences, app configuration. Reduces PostgreSQL load significantly.

Local memory cache (memcache.local): APCu caches PHP opcode and Nextcloud’s in-process data (like group memberships). This is per-PHP-FPM worker — it doesn’t cross process boundaries, unlike Redis.

The combination of APCu (local) + Redis (distributed/locking) is the recommended setup for single-server and multi-server deployments.

PHP-FPM Pool Tuning

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# /usr/local/etc/php-fpm.d/www.conf (mount into container)
[www]
pm = dynamic
pm.max_children = 20        ; Max concurrent PHP workers
pm.start_servers = 5        ; Workers at startup
pm.min_spare_servers = 2    ; Keep at least 2 idle
pm.max_spare_servers = 8    ; Keep at most 8 idle
pm.max_requests = 500       ; Restart workers after 500 requests (prevent leaks)

; Timeouts
request_terminate_timeout = 3600

pm.max_children is the key value. Each PHP-FPM worker uses ~50-150MB RAM. On a server with 4GB RAM (leaving 2GB for PostgreSQL, Redis, OS), you have room for roughly 15-20 workers. More workers = more concurrent users but more memory pressure.

Database Tuning for Nextcloud

Nextcloud is read-heavy but has many small queries per page load. Tune PostgreSQL accordingly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- Nextcloud-specific indexes that dramatically speed up file listing
-- These are normally created by Nextcloud's migrations, but verify they exist:

-- Index on filecache for path lookups
SELECT indexname FROM pg_indexes WHERE tablename = 'oc_filecache';

-- If missing, add:
CREATE INDEX IF NOT EXISTS oc_filecache_parent
  ON oc_filecache (parent);
CREATE INDEX IF NOT EXISTS oc_filecache_storage_path_prefix
  ON oc_filecache (storage, path_hash);
1
2
3
4
5
6
7
8
9
# Run Nextcloud's built-in database optimization
docker compose exec --user www-data nextcloud \
  php occ db:add-missing-indices

docker compose exec --user www-data nextcloud \
  php occ db:add-missing-primary-keys

docker compose exec --user www-data nextcloud \
  php occ db:convert-filecache-bigint

Preview Generation

Nextcloud generates thumbnail previews on-demand by default — the first time you open a folder with 500 photos, it blocks generating all previews. Pre-generate them with the Previewgenerator app:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Install the Preview Generator app
docker compose exec --user www-data nextcloud \
  php occ app:install previewgenerator

# Generate all missing previews (run once after initial setup)
docker compose exec --user www-data nextcloud \
  php occ preview:generate-all --path=/

# Set up incremental generation in cron (add to crontab)
# */10 * * * * docker compose exec --user www-data nextcloud php occ preview:pre-generate

For video thumbnails and HEIC support, add ffmpeg and libheif to the Nextcloud image:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Dockerfile.nextcloud
FROM nextcloud:28-fpm-alpine

RUN apk add --no-cache \
    ffmpeg \
    imagemagick \
    libheif-tools \
    ghostscript

# Enable HEIC in ImageMagick policy
RUN sed -i 's|<policy domain="coder" rights="none" pattern="HEIC" />||' \
    /etc/ImageMagick-7/policy.xml
1
2
3
4
5
6
// config.php additions for video and HEIC previews
'enabledPreviewProviders' => array(
  // ... existing providers ...
  'OC\\Preview\\Movie',
  'OC\\Preview\\HEIC',
),

External Storage

Nextcloud’s external storage app (files_external) mounts remote storage as folders in the Files interface. Users see them like any other folder but data lives elsewhere.

NFS Mount (Home NAS)

1
2
3
4
5
6
7
8
# Mount NAS via NFS on the host
apt install nfs-common
mkdir -p /mnt/nas/media

# Add to /etc/fstab:
# 192.168.1.50:/volume1/media  /mnt/nas/media  nfs  defaults,_netdev,soft,timeo=30  0  0

mount -a
1
2
3
4
5
# docker-compose.yml — bind-mount the NFS path into Nextcloud
services:
  nextcloud:
    volumes:
      - /mnt/nas/media:/mnt/media:ro  # Read-only media library

In Nextcloud admin panel → External Storage:

  • Folder name: Media Library
  • External storage type: Local
  • Configuration: /mnt/media
  • Applicable users/groups: Everyone

S3-Compatible Storage (Backblaze B2, MinIO)

For large primary storage, use S3 as Nextcloud’s primary data directory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// config.php — S3 as primary object store
'objectstore' => array(
  'class' => '\\OC\\Files\\ObjectStore\\S3',
  'arguments' => array(
    'bucket' => 'my-nextcloud-bucket',
    'autocreate' => false,
    'key' => getenv('S3_ACCESS_KEY'),
    'secret' => getenv('S3_SECRET_KEY'),
    'hostname' => 's3.us-west-002.backblazeb2.com',  // B2 endpoint
    'port' => 443,
    'use_ssl' => true,
    'region' => 'us-west-002',
    'use_path_style' => true,
  ),
),

With S3 as the object store, Nextcloud’s database stores metadata (filenames, permissions, shares) while file contents live in S3. This allows virtually unlimited storage without expanding local disk.

Warning: Once you set objectstore in config.php, you cannot easily switch back to local storage. Do this on a fresh installation or plan a migration carefully.

SFTP/WebDAV External Mounts

Mount a remote server as a Nextcloud folder:

1
2
3
4
5
6
# In Nextcloud admin: External Storage
# Type: SFTP
# Host: remote.example.com
# Username: backup_user
# SSH key: (paste private key)
# Root: /backups
1
2
3
# Enable external storage app
docker compose exec --user www-data nextcloud \
  php occ app:enable files_external

Nextcloud Office (Collabora Online)

Nextcloud Office enables in-browser editing of Office documents via an embedded Collabora Online server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# docker-compose.yml addition
services:
  collabora:
    image: collabora/code:latest
    restart: unless-stopped
    environment:
      aliasgroup1: https://cloud.yourdomain.com:443
      DONT_GEN_SSL_CERT: "YES"
      extra_params: >
        --o:ssl.enable=false
        --o:ssl.termination=true
        --o:logging.level=warning
        --o:net.proto=IPv4
    cap_add:
      - MKNOD
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.collabora.rule=Host(`office.yourdomain.com`)"
      - "traefik.http.routers.collabora.entrypoints=websecure"
      - "traefik.http.routers.collabora.tls.certresolver=letsencrypt"
      - "traefik.http.services.collabora.loadbalancer.server.port=9980"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Configure Nextcloud to use Collabora
docker compose exec --user www-data nextcloud \
  php occ app:install richdocuments

docker compose exec --user www-data nextcloud \
  php occ config:app:set richdocuments wopi_url \
    --value="https://office.yourdomain.com"

docker compose exec --user www-data nextcloud \
  php occ richdocuments:activate-config

After configuration, clicking any .docx, .xlsx, or .pptx file in Nextcloud opens it in Collabora Online — a LibreOffice-based editor running in the browser. Multiple users can edit the same document simultaneously with conflict resolution.

For the built-in “Text” editor (Markdown/plain text files), no extra configuration is needed — it works out of the box.

Mobile Sync

iOS

  1. Install Nextcloud from the App Store
  2. Log in with your Nextcloud URL and credentials
  3. Files tab → browse and download files on demand (or mark for offline)
  4. Auto Upload (Settings → Auto Upload): automatically uploads Camera Roll photos to a Nextcloud folder

For Calendar and Contacts sync on iOS:

  • Settings → Mail → Accounts → Add Account → Other
  • Add CalDAV account: server https://cloud.yourdomain.com, username, password
  • Add CardDAV account: same server

Android

  1. Install Nextcloud from F-Droid or Play Store
  2. The official app handles file sync and auto upload
  3. For calendar/contacts: install DAVx⁵ (F-Droid/Play Store)
    • Add account → Nextcloud URL
    • DAVx⁵ syncs CalDAV/CardDAV with the Android system calendar and contacts apps

Desktop Sync Clients

The Nextcloud desktop client (Windows/macOS/Linux) syncs a local folder bidirectionally:

1
2
3
4
5
6
7
8
# Linux (AppImage)
wget https://github.com/nextcloud-releases/desktop/releases/latest/download/Nextcloud-*.AppImage
chmod +x Nextcloud-*.AppImage
./Nextcloud-*.AppImage

# Or via package manager (Ubuntu)
add-apt-repository ppa:nextcloud-devs/client
apt install nextcloud-desktop

Configure selective sync to avoid syncing everything — choose which folders to sync locally. The “Virtual Files” feature (Windows/macOS) shows all files in Explorer/Finder as placeholders without downloading them until accessed.

Background Jobs and Maintenance

Nextcloud relies on background jobs for:

  • Sending notifications and activity emails
  • Cleaning up old file versions and trash
  • Generating previews (with Previewgenerator)
  • Propagating shares
  • Updating the search index

The cron container handles this via /cron.sh which runs php occ system:cron every 5 minutes. Verify it’s working:

1
2
3
4
5
6
# Check when cron last ran
docker compose exec --user www-data nextcloud \
  php occ system:cron

# In Admin → Basic settings: Background jobs should show "Last job ran X minutes ago"
# If it shows hours or "never", the cron container has a problem.

Essential Maintenance Commands

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Run after config changes — clears all caches
docker compose exec --user www-data nextcloud \
  php occ maintenance:repair

# Scan for files added outside Nextcloud (e.g., rsync directly to data dir)
docker compose exec --user www-data nextcloud \
  php occ files:scan --all

# Scan a specific user
docker compose exec --user www-data nextcloud \
  php occ files:scan username

# Clean up deleted files older than 30 days
docker compose exec --user www-data nextcloud \
  php occ trashbin:cleanup --all-users

# Clean up old file versions (keep last 30 days)
docker compose exec --user www-data nextcloud \
  php occ versions:cleanup

# Check overall system status
docker compose exec --user www-data nextcloud \
  php occ status

# List all warnings from the admin overview
docker compose exec --user www-data nextcloud \
  php occ setupchecks

Updating Nextcloud

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 1. Put Nextcloud in maintenance mode
docker compose exec --user www-data nextcloud \
  php occ maintenance:mode --on

# 2. Back up the database
docker compose exec db pg_dump -U nextcloud nextcloud \
  | gzip > nextcloud_backup_$(date +%Y%m%d).sql.gz

# 3. Update the image in docker-compose.yml
# Change: nextcloud:28-fpm-alpine → nextcloud:29-fpm-alpine

# 4. Pull new images
docker compose pull

# 5. Restart — Nextcloud runs migrations automatically on startup
docker compose up -d

# 6. Watch logs for migration completion
docker compose logs -f nextcloud | grep -E "Nextcloud|migration|upgrade"

# 7. Run upgrade command
docker compose exec --user www-data nextcloud \
  php occ upgrade

# 8. Disable maintenance mode
docker compose exec --user www-data nextcloud \
  php occ maintenance:mode --off

# 9. Add any missing indices (new ones are sometimes added between versions)
docker compose exec --user www-data nextcloud \
  php occ db:add-missing-indices

Always update one major version at a time — don’t jump from v26 to v29 directly. Intermediate versions run migrations that the final version depends on.

Backup Strategy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env bash
# backup-nextcloud.sh
set -euo pipefail

BACKUP_DIR="/backup/nextcloud"
DATE=$(date +%Y-%m-%d)
NEXTCLOUD_DATA="/mnt/nas/nextcloud"

mkdir -p "${BACKUP_DIR}/${DATE}"

# 1. Enable maintenance mode (prevents writes during backup)
docker compose exec --user www-data nextcloud \
  php occ maintenance:mode --on

# 2. Database dump
docker compose exec -T db \
  pg_dump -U nextcloud nextcloud \
  | gzip > "${BACKUP_DIR}/${DATE}/db.sql.gz"

# 3. Nextcloud config
cp /opt/nextcloud/config/config.php "${BACKUP_DIR}/${DATE}/config.php"

# 4. Data directory (rsync — only changed files)
rsync -av --delete \
  "${NEXTCLOUD_DATA}/" \
  "${BACKUP_DIR}/${DATE}/data/"

# 5. Disable maintenance mode
docker compose exec --user www-data nextcloud \
  php occ maintenance:mode --off

# 6. Remove backups older than 30 days
find "${BACKUP_DIR}" -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +

echo "Backup complete: ${BACKUP_DIR}/${DATE}"

For offsite, rclone sync the backup directory to Backblaze B2, S3, or another provider.

Security Hardening

Two-Factor Authentication

1
2
3
4
5
6
7
# Enable TOTP 2FA
docker compose exec --user www-data nextcloud \
  php occ app:enable twofactor_totp

# Enforce 2FA for all users (or specific groups)
docker compose exec --user www-data nextcloud \
  php occ twofactorauth:enforce --on

Brute Force Protection

Nextcloud has built-in brute force protection — it throttles repeated failed login attempts. For additional protection, enable the bruteforce app and configure Fail2ban:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# /etc/fail2ban/filter.d/nextcloud.conf
[Definition]
failregex = ^.*Login failed: '.*' \(Remote IP: '<HOST>'\).*$
            ^.*Trusted domain error.*Remote Address: '<HOST>'.*$
ignoreregex =

# /etc/fail2ban/jail.d/nextcloud.conf
[nextcloud]
backend = auto
enabled = true
port = 80,443
protocol = tcp
filter = nextcloud
maxretry = 5
bantime = 3600
findtime = 600
logpath = /mnt/nas/nextcloud/nextcloud.log

App Permissions

Restrict which apps can be installed:

1
2
3
4
5
6
7
# List installed apps
docker compose exec --user www-data nextcloud php occ app:list

# Disable apps you don't need (reduces attack surface)
docker compose exec --user www-data nextcloud php occ app:disable activity
docker compose exec --user www-data nextcloud php occ app:disable weather_status
docker compose exec --user www-data nextcloud php occ app:disable firstrunwizard

Admin Panel Security Checks

The Nextcloud Admin → Overview page runs a comprehensive security scan and flags:

  • Missing HTTPS headers
  • Incorrect file permissions
  • Missing database indices
  • PHP configuration issues
  • App version warnings
1
2
# Run from CLI
docker compose exec --user www-data nextcloud php occ setupchecks

Fix every warning here. A clean admin overview is the baseline for a secure installation.

Troubleshooting Common Issues

Files not syncing after adding them to data directory directly:

1
docker compose exec --user www-data nextcloud php occ files:scan --all

“Too many requests” from clients: Redis file locking is failing or Redis is slow. Check Redis health:

1
docker compose exec redis redis-cli -a "${REDIS_PASSWORD}" info stats | grep -E "connected|rejected|blocked"

Slow file listing: Missing database indices. Run:

1
docker compose exec --user www-data nextcloud php occ db:add-missing-indices

Login redirect loop (HTTPS/proxy issues): Verify overwriteprotocol = https and trusted_proxies includes the Traefik container’s IP range in config.php.

Large file uploads failing: Check three places: PHP_UPLOAD_LIMIT env var, upload_max_filesize in php.ini, client_max_body_size in nginx.conf — all must be ≥ the max file size you want to support.

Nextcloud Office not connecting: The Nextcloud container must be able to reach Collabora at office.yourdomain.com — which means the Docker network must route that hostname. Add Collabora to the same Docker network or use an internal hostname.

The investment in setup pays off quickly. With Nextcloud properly tuned, it’s indistinguishable from Google Drive in daily use — same automatic photo backup, same desktop sync folder, same shared folders with family members — with the added benefit that every byte of your data lives on your hardware.

Comments