Self-hosted • Privacy-first • No tracking
Home / Homelab / Nextcloud 34 Docker Compose Guide: Secure Self-Hosted Cloud in 2026
Homelab #self-hosted#homelab#docker-compose#nextcloud#cloud-storage ⏱ 7 min • 👁 3 • Sep 02, 2026

Nextcloud 34 Docker Compose Guide: Secure Self-Hosted Cloud in 2026

Step-by-step Nextcloud 34 deployment with Docker Compose: architecture, hardened security, backup strategy, and troubleshooting for your homelab.

AdSense — Top (970x90) • Responsive
Nextcloud 34 Docker Compose Guide: Secure Self-Hosted Cloud in 2026

Introduction

Self-hosting your cloud storage is the definitive step toward digital sovereignty. Nextcloud provides file sync, calendar, contacts, and collaborative editing—all under your control. Running it via Docker Compose simplifies upgrades, isolates dependencies, and makes your entire stack declarative and reproducible.

This guide walks you through a production-ready deployment of Nextcloud v34.0.3 using Docker Compose. You will learn the recommended architecture separating the web server, application, database, and cache into distinct containers. We will configure Redis for locking and caching, MariaDB for metadata, and a dedicated volume structure for clean backups.

Beyond basic installation, we will cover critical operational concerns: reverse proxy integration with SSL termination, automated database backups, and security hardening options like read-only root filesystems and dropped kernel capabilities. Finally, we will troubleshoot common pitfalls such as permission errors, reverse proxy IP issues, and database connection resets.

By the end, you will have a resilient, private cloud service that you fully own. The process takes about 30 minutes of active work, but the result is a foundation you can extend with Collabora, Talk, or Office integrations later.

Prerequisites

Before you begin, ensure your host meets the following requirements. These are typical ranges for a small-to-medium instance (1-5 users).

Component Minimum Recommended Notes
CPU 1 core 2 cores Encryption and preview generation are CPU-bound.
RAM 2 GB 4 GB MariaDB and PHP-FPM benefit significantly from more memory.
Storage 50 GB 200 GB+ Use SSD/NVMe for the database volume. Data volume can be HDD.
Docker Engine v24+ Latest stable Install from official Docker repository.
Docker Compose Plugin v2.20+ Latest Used as docker compose (v2).
OS Ubuntu 24.04 LTS Debian 12 / Ubuntu 24.04 Any modern Linux distribution works.
Domain Name Optional Required for production Needed for valid SSL certificates.

Software versions verified:

Service Image Version
Nextcloud nextcloud:34.0.3
MariaDB mariadb:11.4
Redis redis:7.4-alpine

Check the official GitHub releases page before pinning a version — the version above may be outdated by now.

Step-by-Step Installation Guide

Step 1: Create Project Directory and .env File

Create a dedicated directory for your Nextcloud stack. We will use ~/nextcloud as our base.

mkdir -p ~/nextcloud && cd ~/nextcloud

Now, create a .env file in this directory. This file will hold all secrets and configurable variables. Never commit this file to Git.

cat > .env <<'EOF'
# Database credentials
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud
# Generate strong passwords: openssl rand -base64 32
MYSQL_PASSWORD=change_this_strong_db_password
MYSQL_ROOT_PASSWORD=change_this_strong_root_password

# Nextcloud admin account (initial setup only)
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=change_this_strong_admin_password

# Host paths (relative to the directory containing this file)
NEXTCLOUD_DATA_DIR=./data
NEXTCLOUD_CONFIG_DIR=./config
NEXTCLOUD_APPS_DIR=./apps
NEXTCLOUD_DB_DIR=./db

# Redis password
REDIS_HOST_PASSWORD=change_this_strong_redis_password

# PHP memory limit (default is fine for most cases)
PHP_MEMORY_LIMIT=512M
EOF

Step 2: Create Persistent Volume Directories

Create the directories on the host that will be mounted into the containers. This ensures data persists across container recreation.

mkdir -p data config apps db && chmod 750 data config apps db

Step 3: Create the Docker Compose File

Create a file named docker-compose.yml in the same directory. This file defines four services: db, redis, app, and cron. We will explain each part after the code block.

services:
  db:
    image: mariadb:11.4
    container_name: nextcloud-db
    restart: unless-stopped
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW --innodb-file-per-table=1 --skip-innodb-read-only-compressed
    volumes:
      - ${NEXTCLOUD_DB_DIR}:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - nextcloud_network

  redis:
    image: redis:7.4-alpine
    container_name: nextcloud-redis
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_HOST_PASSWORD} --maxmemory 128mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - nextcloud_network

  app:
    image: nextcloud:34.0.3
    container_name: nextcloud-app
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    ports:
      - "8080:80"
    volumes:
      - ${NEXTCLOUD_DATA_DIR}:/var/www/html/data
      - ${NEXTCLOUD_CONFIG_DIR}:/var/www/html/config
      - ${NEXTCLOUD_APPS_DIR}:/var/www/html/custom_apps
    environment:
      - MYSQL_HOST=db
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - REDIS_HOST=redis
      - REDIS_HOST_PASSWORD=${REDIS_HOST_PASSWORD}
      - PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT}
      - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
      - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
      - TRUSTED_PROXIES=172.20.0.0/16
      - OVERWRITEPROTOCOL=https
      - OVERWRITECLIURL=https://your-domain.com
      - OVERWRITEHOST=your-domain.com
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/status.php"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s
    networks:
      - nextcloud_network

  cron:
    image: nextcloud:34.0.3
    container_name: nextcloud-cron
    restart: unless-stopped
    depends_on:
      app:
        condition: service_healthy
    volumes:
      - ${NEXTCLOUD_DATA_DIR}:/var/www/html/data
      - ${NEXTCLOUD_CONFIG_DIR}:/var/www/html/config
      - ${NEXTCLOUD_APPS_DIR}:/var/www/html/custom_apps
    entrypoint: /cron.sh
    networks:
      - nextcloud_network

volumes:
  redis_data:

networks:
  nextcloud_network:
    driver: bridge

Step 4: Explanation of Key Configuration Points

The db service uses MariaDB 11.4 with specific command-line flags required by Nextcloud for proper transaction handling and binary logging. The healthcheck ensures the database is ready before the app container starts.

The redis service acts as a distributed cache and file locking manager. Without it, Nextcloud falls back to file-based locking, which is unreliable and slow. The --maxmemory-policy allkeys-lru prevents Redis from exhausting host RAM.

The app service maps port 8080 on the host to port 80 in the container. We mount three host directories: data for actual user files, config for the config.php, and custom_apps for manually installed applications. The environment variables set up database and Redis connectivity along with the initial admin account.

The cron service runs the same image but overrides the entrypoint to run cron.sh. This executes background jobs (e.g., file scanning, preview generation) on schedule. This is critical for large libraries because the web cron alternative is unreliable.

Step 5: Configure Reverse Proxy and SSL (Recommended)

For production, expose Nextcloud through a reverse proxy like Caddy or Nginx Proxy Manager to handle HTTPS. Here is a minimal Caddy configuration.

Create a file named Caddyfile on your host:

your-domain.com {
    reverse_proxy nextcloud-app:80
}

Then run Caddy in a separate container on the same Docker network. Add this to a separate docker-compose.proxy.yml file:

services:
  caddy:
    image: caddy:2.8-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - nextcloud_network

volumes:
  caddy_data:
  caddy_config:

networks:
  nextcloud_network:
    external: true

Ensure the nextcloud_network is external by creating it first:

docker network create nextcloud_network

Update your .env file with your actual domain and run docker compose --env-file .env -f docker-compose.yml -f docker-compose.proxy.yml up -d.

Step 6: Start the Stack and Perform Initial Setup

Start all services in detached mode.

docker compose up -d

Wait for the app container to become healthy. Check the logs to monitor progress.

docker compose logs -f app

Once healthy, access http://your-server-ip:8080. You will see the initial setup wizard. Since we provided NEXTCLOUD_ADMIN_USER and NEXTCLOUD_ADMIN_PASSWORD, the admin account is pre-created. The wizard will ask you to select the database. Choose MariaDB and enter the credentials from your .env file. The database host is db.

Step 7: Post-Installation Web UI Configuration

After logging in, navigate to Settings > Overview. You will likely see warnings about missing indexes or database inconsistencies. Run the following occ commands to fix them.

sudo docker exec -u www-data nextcloud-app php occ db:add-missing-indices
sudo docker exec -u www-data nextcloud-app php occ db:convert-filecache-bigint

Step 8: Set Up Automated Backups

Create a backup script that dumps the database and copies the data directory. Save this as backup.sh on your host.

#!/bin/bash
# Backup script for Nextcloud
export BACKUP_DIR=~/backups/nextcloud
mkdir -p ${BACKUP_DIR}

# Backup database
docker exec nextcloud-db sh -c 'exec mysqldump --all-databases --single-transaction --quick --lock-tables=false -uroot -p"$MYSQL_ROOT_PASSWORD"' > ${BACKUP_DIR}/nextcloud-db-$(date +%Y%m%d-%H%M%S).sql

# Backup data and config directories
rsync -avh ~/nextcloud/data ${BACKUP_DIR}/
rsync -avh ~/nextcloud/config ${BACKUP_DIR}/
rsync -avh ~/nextcloud/apps ${BACKUP_DIR}/

echo "Backup completed at $(date)"

Make it executable and run it manually to test:

chmod +x backup.sh && ./backup.sh

Add a cron job to run this daily:

crontab -e

Add this line:

0 2 * * * /home/your-username/nextcloud/backup.sh >> /home/your-username/nextcloud/backup.log 2>&1

Step 9: Enable Maintenance Mode for Safe Upgrades

Before any major upgrade, put Nextcloud into maintenance mode to prevent data corruption.

sudo docker exec -u www-data nextcloud-app php occ maintenance:mode --on

Then update your images:

docker compose pull && docker compose up -d

Finally, turn off maintenance mode:

sudo docker exec -u www-data nextcloud-app php occ maintenance:mode --off

Advanced Configuration & Optimization

Reverse Proxy Trusted Domain and IP

When using a reverse proxy, Nextcloud must trust the proxy's IP address to log real client IPs. The TRUSTED_PROXIES environment variable is set to 172.20.0.0/16 in our compose file, which covers the default Docker bridge network. If you use a different subnet, adjust it. You can find your subnet by running docker network inspect nextcloud_network | grep Subnet.

File Locking and Caching

We already configured Redis. To verify it is being used, check config/config.php for these entries:

'memcache.local' => '\OC\Memcache\Redis',
'memcache.distributed' => '\OC\Memcache\Redis',
'memcache.locking' => '\OC\Memcache\Redis',
'redis' => array(
  'host' => 'redis',
  'port' => 6379,
  'password' => 'your_redis_password',
),

Optional Hardening

The following are advanced security measures. They require adaptation per application and may break containers if copied verbatim. Test in a staging environment first.

Measure Implementation Warning
Read-only root filesystem Add read_only: true to the app service. Nextcloud needs to write to /var/www/html for updates. You must mount a volume for /var/www/html and /tmp.
Drop all kernel capabilities Add cap_drop: [ALL] to all services. The app service may need CHOWN and FOWNER to manage files. Test carefully.
Run as non-root Ensure the container user matches the host directory owner. Run id -u && id -g on your host and verify against the image's documentation (default user in this image is www-data with UID 33).

Troubleshooting Common Errors

Error Cause Solution
SQLSTATE[HY000] [2002] Connection refused Database not ready when app starts. Increase start_period in db healthcheck. Ensure depends_on uses condition: service_healthy.
Your data directory is not writable Host directory permissions mismatch. Run id -u and id -g on your host. Then sudo chown -R <uid>:<gid> data config apps. Do not use fixed 1000:1000.
Your IP address is in the trusted proxies list but your reverse proxy is not Misconfigured TRUSTED_PROXIES. Set TRUSTED_PROXIES to the exact subnet of your proxy container. Inspect with docker network inspect.
The database is missing some indexes Fresh install or upgrade. Run sudo docker exec -u www-data nextcloud-app php occ db:add-missing-indices.
Redis connection refused Redis password mismatch or container not on same network. Check REDIS_HOST_PASSWORD in .env and the redis service command. Ensure both containers are on nextcloud_network.
413 Request Entity Too Large Nginx proxy limit. In your proxy config, set client_max_body_size 10G; (or your desired limit).

Conclusion

You now have a fully operational Nextcloud 34 instance using Docker Compose. We isolated the database, cache, and application into separate containers with proper health checks. We configured automated backups and covered the essential post-installation maintenance commands. Your data is under your control, accessible from any device, and ready for expansion.

Remember that self-hosting is a continuous process. Subscribe to Nextcloud release announcements, monitor your logs, and test restores from your backups regularly.

FAQ

1. How do I update Nextcloud to a new minor version?

Put Nextcloud into maintenance mode using docker exec -u www-data nextcloud-app php occ maintenance:mode --on. Then pull the new image and recreate containers: docker compose pull && docker compose up -d. Finally, turn off maintenance mode. Run php occ upgrade if the container does not do it automatically.

2. Can I use PostgreSQL instead of MariaDB?

Yes. Replace the db service image with postgres:16-alpine, set the appropriate environment variables (POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD), and change MYSQL_HOST to POSTGRES_HOST in the app service. Update the db service volume path to /var/lib/postgresql/data. Nextcloud fully supports both databases.

3. Where are my user files stored physically?

User files are stored in the data directory you mounted from the host. Specifically, they reside in data/<username>/files/. The database stores file metadata, versions, and shares. The data directory is the most critical asset for backups—the database can be rebuilt, but file content cannot.

4. Why is the cron service necessary?

Nextcloud performs background jobs like file scans, preview generation, and expiring shares. These jobs are triggered by a cron job. Without a dedicated cron service, these tasks rely on user visits to the web interface, which causes delays and poor performance. The dedicated cron container ensures these jobs run every 5 minutes by default.

5. How do I change the domain name after installation?

Edit the OVERWRITEHOST and OVERWRITECLIURL environment variables in your .env file. Then recreate the app container: docker compose up -d app. Finally, update the trusted_domains array in config/config.php by running sudo docker exec -u www-data nextcloud-app php occ config:system:set trusted_domains 1 --value=new-domain.com.

AdSense — In-article (responsive)

Related Guides