How to Self-Host Nextcloud Hub with Docker Compose, PostgreSQL, Redis, and Caddy: Production-Grade Private Cloud

IT system architect deploying Nextcloud Hub with PostgreSQL, Redis, and Caddy using Docker Compose
How to Self-Host Nextcloud Hub with Docker Compose, PostgreSQL, Redis, and Caddy: Production-Grade Private Cloud 3

Public cloud storage platforms like Google Drive, Dropbox, and Microsoft OneDrive offer convenient file synchronization and document collaboration. However, for organizations, privacy-conscious engineering teams, and homelab administrators, storing sensitive proprietary data on third-party commercial clouds introduces significant operational, compliance, and sovereignty risks. Vendor price hikes, changing terms of service, unexpected account lockouts, and compliance frameworks such as GDPR or HIPAA frequently mandate complete control over data at rest and in transit.

Nextcloud Hub is the undisputed open-source industry standard for self-hosted productivity and cloud collaboration. Beyond standard WebDAV file sync, Nextcloud Hub provides full document editing via Nextcloud Office (Collabora or OnlyOffice), groupware (Calendars, Contacts, Mail), project boards (Deck), and encrypted audio/video calling (Talk). However, a naive Nextcloud deployment using SQLite and built-in web serving quickly collapses under concurrent access, resulting in slow page rendering, desktop sync lockups, and database corruption.

In this technical walkthrough, you will deploy a high-performance, production-grade Nextcloud Hub instance using Docker Compose. The architecture pairs the official Nextcloud Apache image with PostgreSQL 16 for robust relational storage, Redis 7 for distributed in-memory caching and transactional file locking, an isolated background cron container, and Caddy v2 as the edge reverse proxy handling automatic Let’s Encrypt TLS certificates and RFC-compliant WebDAV redirects.

Architecture Overview & Traffic Flow

A resilient Nextcloud deployment separates stateful database transactions, cache lookups, cron maintenance tasks, and public ingress into dedicated container processes. The following diagram illustrates the interaction between external clients, reverse proxy routing, and internal backend services:

+-----------------------------------------------------------------------+
|                         Public Internet / LAN                         |
|             (Desktop Sync Client, Mobile App, Web Browser)            |
+-----------------------------------------------------------------------+
                                   |
                                   | HTTPS (Port 443 / TLS 1.3)
                                   v
+-----------------------------------------------------------------------+
|                    Edge Reverse Proxy: Caddy v2                       |
|   - Automatic Let's Encrypt / ZeroSSL TLS Certificate Management     |
|   - Strict HSTS, HTTP/2, and HTTP/3 QUIC Protocol Support             |
|   - /.well-known/carddav & caldav RFC-compliant URL Rewrites          |
+-----------------------------------------------------------------------+
                                   |
                                   | HTTP (Internal Bridge Network: nextcloud_net)
                                   v
+-----------------------------------------------------------------------+
|                     Nextcloud Application Tier                        |
|   - Nextcloud Hub (Apache / PHP 8.3 / OPcache tuned)                  |
|   - Mounted Volumes: ./html (App code), ./data (User Files)          |
+-----------------------------------------------------------------------+
         |                                           |
         | SQL Queries                               | Memory Cache &
         | (Port 5432)                               | File Locks (Port 6379)
         v                                           v
+----------------------------+             +----------------------------+
|  PostgreSQL 16 Database    |             |       Redis 7 Cache        |
|  - Relational metadata     |             |  - Distributed Memcache    |
|  - Persistent DB volume    |             |  - Transactional Locking   |
+----------------------------+             +----------------------------+
         ^
         | Scheduled CLI Maintenance (Every 5 minutes)
+-----------------------------------------------------------------------+
|                      Dedicated Cron Container                         |
|   - Executes: php -f /var/www/html/cron.php                           |
|   - Independent process preventing web worker thread starvation       |
+-----------------------------------------------------------------------+

Key highlights of this decoupled architecture:

  • Transactional File Locking via Redis: Nextcloud locks files during active uploads and sync operations. Without Redis, file locking relies on SQL tables, leading to severe PostgreSQL table bloat and sync freezes.
  • Offloaded Background Processing: Running cron.php in a separate container ensures that heavy background jobs (search indexing, notification dispatch, preview generation) never block user web requests.
  • Hardened Edge Ingress: Caddy handles modern TLS handshakes, transparent compression, and the specific redirects required by iOS, macOS, and Android WebDAV clients.

Prerequisites & Environment Setup

Before launching the stack, ensure your host environment meets the following baseline requirements:

  • Host Operating System: Ubuntu 24.04 LTS, Debian 12, or AlmaLinux 9 with kernel 6.x.
  • Hardware Sizing: Minimum 2 vCPUs, 4 GB RAM (8 GB recommended for 10+ users or active Nextcloud Office), and fast SSD/NVMe storage for database and cache directories.
  • Software: Docker Engine 26.x or newer and Docker Compose v2.26+ installed.
  • DNS Configuration: A fully qualified domain name (FQDN), such as cloud.yourdomain.com, pointing via an A/AAAA record to your server’s public IP address.

Create a dedicated directory structure on your server to house your configuration, database, and application data:

sudo mkdir -p /opt/nextcloud/{html,data,db_data,redis_data,caddy_data,caddy_config}
cd /opt/nextcloud
sudo chown -R 33:33 /opt/nextcloud/html /opt/nextcloud/data
sudo chmod 750 /opt/nextcloud/data

Note: UID and GID 33:33 correspond to the standard www-data user inside the official Nextcloud Debian-based container. Ensuring correct ownership prevents permission denial errors during the initial installation wizard.

Step 1: Define Environment Variables (.env)

Store all sensitive passwords, tokens, and domain configuration in a secure .env file. Generate strong, cryptographically random strings using openssl rand -hex 24:

# Public Domain
NEXTCLOUD_FQDN=cloud.yourdomain.com

# Database Credentials
POSTGRES_DB=nextcloud
POSTGRES_USER=nextcloud_admin
POSTGRES_PASSWORD=generate_super_secret_db_pass_here_48chars
POSTGRES_HOST=nextcloud-db

# Redis Configuration
REDIS_HOST=nextcloud-redis
REDIS_PASSWORD=generate_super_secret_redis_pass_here_48chars

# Nextcloud Admin Account (Initial Bootstrap)
NEXTCLOUD_ADMIN_USER=ncadmin
NEXTCLOUD_ADMIN_PASSWORD=generate_strong_admin_pass_here_48chars

# System Tuning
PHP_MEMORY_LIMIT=1024M
PHP_UPLOAD_LIMIT=16G

Step 2: Production Docker Compose Configuration

Create the docker-compose.yml file. This configuration defines the five interconnected services: database, cache, web application, background cron, and reverse proxy.

services:
  # ----------------------------------------------------------------------------
  # PostgreSQL 16 Relational Database
  # ----------------------------------------------------------------------------
  nextcloud-db:
    image: postgres:16-alpine
    container_name: nextcloud-db
    restart: unless-stopped
    volumes:
      - /opt/nextcloud/db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    command:
      - "postgres"
      - "-c"
      - "max_connections=200"
      - "-c"
      - "shared_buffers=512MB"
      - "-c"
      - "effective_cache_size=1536MB"
      - "-c"
      - "maintenance_work_mem=128MB"
      - "-c"
      - "checkpoint_completion_target=0.9"
      - "-c"
      - "wal_buffers=16MB"
      - "-c"
      - "default_statistics_target=100"
    networks:
      - nextcloud_internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

  # ----------------------------------------------------------------------------
  # Redis 7 In-Memory Cache & Transactional File Locking
  # ----------------------------------------------------------------------------
  nextcloud-redis:
    image: redis:7-alpine
    container_name: nextcloud-redis
    restart: unless-stopped
    command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}", "--maxmemory", "512mb", "--maxmemory-policy", "allkeys-lru"]
    volumes:
      - /opt/nextcloud/redis_data:/data
    networks:
      - nextcloud_internal
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # ----------------------------------------------------------------------------
  # Nextcloud Hub Core Application
  # ----------------------------------------------------------------------------
  nextcloud-app:
    image: nextcloud:30-apache
    container_name: nextcloud-app
    restart: unless-stopped
    depends_on:
      nextcloud-db:
        condition: service_healthy
      nextcloud-redis:
        condition: service_healthy
    volumes:
      - /opt/nextcloud/html:/var/www/html
      - /opt/nextcloud/data:/var/www/html/data
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_HOST: ${POSTGRES_HOST}
      REDIS_HOST: ${REDIS_HOST}
      REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
      NEXTCLOUD_ADMIN_USER: ${NEXTCLOUD_ADMIN_USER}
      NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASSWORD}
      NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_FQDN}
      OVERWRITEHOST: ${NEXTCLOUD_FQDN}
      OVERWRITEPROTOCOL: https
      OVERWRITECLIURL: https://${NEXTCLOUD_FQDN}
      PHP_MEMORY_LIMIT: ${PHP_MEMORY_LIMIT}
      PHP_UPLOAD_LIMIT: ${PHP_UPLOAD_LIMIT}
    networks:
      - nextcloud_internal
      - nextcloud_edge

  # ----------------------------------------------------------------------------
  # Dedicated Asynchronous Cron Runner
  # ----------------------------------------------------------------------------
  nextcloud-cron:
    image: nextcloud:30-apache
    container_name: nextcloud-cron
    restart: unless-stopped
    entrypoint: /cron.sh
    depends_on:
      - nextcloud-app
    volumes:
      - /opt/nextcloud/html:/var/www/html
      - /opt/nextcloud/data:/var/www/html/data
    networks:
      - nextcloud_internal

  # ----------------------------------------------------------------------------
  # Caddy v2 Edge Reverse Proxy (Automatic TLS)
  # ----------------------------------------------------------------------------
  caddy:
    image: caddy:2-alpine
    container_name: nextcloud-caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - /opt/nextcloud/Caddyfile:/etc/caddy/Caddyfile:ro
      - /opt/nextcloud/caddy_data:/data
      - /opt/nextcloud/caddy_config:/config
    networks:
      - nextcloud_edge

networks:
  nextcloud_internal:
    internal: true
  nextcloud_edge:
    internal: false

Step 3: Configuring Caddy Reverse Proxy & WebDAV Rewrites

Nextcloud relies heavily on CalDAV (Calendar) and CardDAV (Contacts) protocols. Mobile operating systems (especially Apple iOS and macOS) probe /.well-known/caldav and /.well-known/carddav immediately upon account creation. If your reverse proxy does not return an HTTP 301 redirect to /remote.php/dav, account syncing will fail silently, and Nextcloud’s admin security audit will flag warnings.

Create the /opt/nextcloud/Caddyfile with optimized security headers and explicit redirects:

{$NEXTCLOUD_FQDN:cloud.yourdomain.com} {
    encode zstd gzip

    # CalDAV and CardDAV RFC-compliant redirects for mobile sync
    redir /.well-known/carddav /remote.php/dav 301
    redir /.well-known/caldav /remote.php/dav 301
    redir /.well-known/webfinger /index.php/.well-known/webfinger 301
    redir /.well-known/nodeinfo /index.php/.well-known/nodeinfo 301

    # Security Headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        X-Permitted-Cross-Domain-Policies "none"
        X-Robots-Tag "noindex, nofollow"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "no-referrer"
    }

    # Reverse Proxy to Nextcloud Apache container
    reverse_proxy nextcloud-app:80 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto https
        header_up X-Forwarded-Host {host}

        # Maximum upload chunk streaming
        transport http {
            read_buffer 8192
            response_header_timeout 600s
        }
    }
}

Step 4: Launching and Bootstrapping the Stack

Validate your Compose syntax and launch the containers in detached mode:

cd /opt/nextcloud
docker compose up -d

Monitor the startup sequence to verify that PostgreSQL initializes the database tables and Caddy obtains an SSL certificate from Let’s Encrypt:

docker compose logs -f --tail=50

Once all services report healthy, navigate to https://cloud.yourdomain.com in your browser. Because we supplied administrative and database credentials in the environment variables, Nextcloud will automatically execute its initial database migrations without prompting for manual database parameters.

Step 5: Post-Installation Hardening with OCC CLI

Even with environmental configuration, Nextcloud requires several PHP, Redis, and phone region parameters to be finalized in its config/config.php file. The occ (Open Collaboration Services) command-line tool provides a scriptable interface to adjust these settings without risking PHP syntax errors.

Execute the following commands from your host to apply essential performance and security enhancements:

# Set default phone region for phone number validation (e.g., US, DE, GB)
docker compose exec -u www-data nextcloud-app php occ config:system:set default_phone_region --value="US"

# Configure Redis as the distributed memcache and transactional locking cache
docker compose exec -u www-data nextcloud-app php occ config:system:set memcache.local --value="\OC\Memcache\APCu"
docker compose exec -u www-data nextcloud-app php occ config:system:set memcache.distributed --value="\OC\Memcache\Redis"
docker compose exec -u www-data nextcloud-app php occ config:system:set memcache.locking --value="\OC\Memcache\Redis"

docker compose exec -u www-data nextcloud-app php occ config:system:set redis host --value="nextcloud-redis"
docker compose exec -u www-data nextcloud-app php occ config:system:set redis port --value=6379 --type=integer
docker compose exec -u www-data nextcloud-app php occ config:system:set redis password --value="$(grep REDIS_PASSWORD .env | cut -d= -f2)"

# Enforce maintenance window for heavy background migrations (01:00 UTC)
docker compose exec -u www-data nextcloud-app php occ config:system:set maintenance_window_start --value=1 --type=integer

# Enable memory-efficient preview generation
docker compose exec -u www-data nextcloud-app php occ config:system:set enable_previews --value=true --type=boolean
docker compose exec -u www-data nextcloud-app php occ config:system:set preview_max_x --value=1024 --type=integer
docker compose exec -u www-data nextcloud-app php occ config:system:set preview_max_y --value=1024 --type=integer

# Add missing database indices if flagged by schema updates
docker compose exec -u www-data nextcloud-app php occ db:add-missing-indices

Step 6: Configuring System Background Jobs (Cron)

By default, Nextcloud uses “AJAX” cron, which executes background tasks only when a user interacts with the web interface. In a production environment, this causes delayed notifications, uncleaned trash bins, and sluggish user response times.

Because our docker-compose.yml includes a dedicated nextcloud-cron container running /cron.sh every 5 minutes, tell Nextcloud to use system cron mode:

docker compose exec -u www-data nextcloud-app php occ background:cron

Verify this setting in the web UI under Administration Settings > Basic settings > Background jobs. It should reflect Cron (Recommended) with a green checkmark indicating execution within the last 5 minutes.

Troubleshooting Common Nextcloud Issues

1. WebDAV / CalDAV Warning: “Your web server is not properly set up to resolve /.well-known/caldav”

Symptom: The Security & setup warnings page in the Nextcloud admin dashboard displays an alert that /.well-known/caldav or /.well-known/carddav is not resolving correctly.

Root Cause: Reverse proxies frequently strip or fail to rewrite well-known discovery endpoints to Nextcloud’s internal WebDAV router (/remote.php/dav).

Resolution: Ensure your Caddyfile contains explicit 301 redirects placed outside any nested location blocks:

redir /.well-known/carddav /remote.php/dav 301
redir /.well-known/caldav /remote.php/dav 301

Reload Caddy configuration without downtime: docker compose exec nextcloud-caddy caddy reload --config /etc/caddy/Caddyfile.

2. “Transactional file locking should be configured with Redis”

Symptom: Severe performance degradation during parallel desktop sync uploads, accompanied by admin dashboard warnings regarding file locking.

Root Cause: Nextcloud falls back to database-driven locking when Redis configuration is incomplete or when authentication against the Redis container fails.

Resolution: Test raw Redis connectivity from inside the application container using nc or PHP CLI:

docker compose exec -u www-data nextcloud-app php -r '
$redis = new Redis();
$redis->connect("nextcloud-redis", 6379);
$redis->auth("YOUR_REDIS_PASSWORD");
echo $redis->ping() ? "Redis OK\n" : "Redis FAIL\n";
'

If the test succeeds, re-verify that 'memcache.locking' => '\OC\Memcache\Redis' is present in /var/www/html/config/config.php.

3. “The reverse proxy header configuration is incorrect” / Trusted Proxies Warning

Symptom: User login attempts are flagged as coming from internal Docker gateway IPs (such as 172.20.0.1), triggering brute-force throttling warnings.

Root Cause: Nextcloud does not inherently trust the Docker internal network range for X-Forwarded-For header forwarding.

Resolution: Configure trusted proxies using the Docker network subnet:

# Determine your docker edge network subnet
docker network inspect nextcloud_nextcloud_edge | grep Subnet

# Register trusted proxies (replace with your actual Docker subnet or 172.16.0.0/12)
docker compose exec -u www-data nextcloud-app php occ config:system:set trusted_proxies 0 --value="172.16.0.0/12"
docker compose exec -u www-data nextcloud-app php occ config:system:set forwardfor_class --value="HTTP_X_FORWARDED_FOR"

Automating Backup and Recovery

A complete Nextcloud backup requires three components: the PostgreSQL database dump, the configuration file, and user data files. Create a daily backup cron script on your host:

#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="/backup/nextcloud/$(date +%F)"
mkdir -p "$BACKUP_DIR"

# 1. Put Nextcloud into maintenance mode to freeze writes
docker compose -f /opt/nextcloud/docker-compose.yml exec -T -u www-data nextcloud-app php occ maintenance:mode --on

# 2. Backup PostgreSQL database
docker compose -f /opt/nextcloud/docker-compose.yml exec -T nextcloud-db \
    pg_dump -U nextcloud_admin nextcloud | gzip > "$BACKUP_DIR/nextcloud_db.sql.gz"

# 3. Backup config and application code
tar -czf "$BACKUP_DIR/nextcloud_config.tar.gz" -C /opt/nextcloud html/config

# 4. Disable maintenance mode
docker compose -f /opt/nextcloud/docker-compose.yml exec -T -u www-data nextcloud-app php occ maintenance:mode --off

# 5. Snapshot data directory (or sync via restic/borgbackup)
echo "Nextcloud database and config backup complete: $BACKUP_DIR"

Conclusion & Next Steps

You now have a fully containerized, production-hardened Nextcloud Hub deployment. By combining PostgreSQL 16, Redis 7 caching, a dedicated cron runner, and Caddy reverse proxy with automated TLS, this setup delivers rapid file synchronization, stable document collaboration, and complete data ownership.

To further extend your private cloud, consider connecting a local AI assistant via Nextcloud Assistant and Ollama, integrating Collabora Online or OnlyOffice document servers for real-time multi-user editing, and pairing the instance with offsite S3-compatible object storage via Restic or BorgBackup for comprehensive disaster recovery.