Self-hosted • Privacy-first • No tracking
Home / Homelab / Nextcloud 34 on Docker: The Complete Homelab Installation Guide (2026)
Homelab #homelab#docker-compose#nextcloud#postgresql#self-hosted-cloud ⏱ 8 min • 👁 1 • Sep 03, 2026

Nextcloud 34 on Docker: The Complete Homelab Installation Guide (2026)

Step-by-step guide to deploy Nextcloud v34.0.3 with Docker Compose, including database setup, reverse proxy, backups, and troubleshooting.

AdSense — Top (970x90) • Responsive
Nextcloud 34 on Docker: The Complete Homelab Installation Guide (2026)

Introduction

Self-hosting your cloud storage is the definitive step toward digital sovereignty. Nextcloud gives you file sync, calendar, contacts, and collaborative editing without sending your data through third-party servers. In your homelab, it becomes the central hub that ties your media server, document management, and personal productivity together.

This guide walks you through deploying Nextcloud v34.0.3 (the latest official release as of 2026-08-13) using Docker Compose. We cover the production-ready stack: Nginx web server, PHP-FPM, PostgreSQL, and Redis for caching. You will learn the exact directory structure, environment variables, and security hardening steps that separate a hobbyist setup from a robust homelab service.

By the end, you will have a fully functional Nextcloud instance behind your own reverse proxy with SSL, automated database backups, and a clear understanding of common failure points. The instructions are copy-paste ready, but we explain every configuration block so you can adapt it to your existing infrastructure.

Prerequisites

Before you start, ensure your homelab server meets the following requirements. These are typical ranges based on community reports — actual usage varies with the number of users, files, and enabled apps.

Component Minimum Recommended Notes
CPU 2 cores 4+ cores Heavily used during file preview generation and encryption operations.
RAM 4 GB 8 GB PHP-FPM and PostgreSQL are memory-hungry. Redis adds ~50 MB overhead.
Storage 20 GB free 1 TB+ The disk holds your data files, database, and backups. Use HDD for archives, SSD for the DB.
OS Ubuntu 22.04+ / Debian 12 Same Any modern Linux distribution with kernel 5.15+ works.
Software Docker Engine 24+ Docker Compose v2.20+ Install from the official Docker repository, not distro packages.
Network Static IP or dynamic DNS Same Needed for stable access via reverse proxy.

Step-by-Step Installation

1. Prepare the Directory Structure and Environment File

Create the base directory for all Nextcloud components. We use a single folder to simplify backups and permission management.

mkdir -p ~/nextcloud && cd ~/nextcloud && mkdir -p data config apps db && touch .env

The .env file holds all secrets and configurable parameters. Never commit this file to Git — add .env to your .gitignore immediately. Generate strong passwords using openssl rand -base64 32.

cat > .env << 'EOF'
NEXTCLOUD_VERSION=34.0.3
POSTGRES_DB=nextcloud
POSTGRES_USER=nextcloud
POSTGRES_PASSWORD=change_this_strong_password_1
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=change_this_strong_admin_password_2
REDIS_PASSWORD=change_this_strong_redis_password_3
EOF

Verify the file was created correctly:

cat .env && echo "---ENV READY---"

2. Create the Docker Compose File

The compose file defines five services: db (PostgreSQL), redis, app (Nextcloud + PHP-FPM), cron, and web (Nginx). We pin the exact version from the verified facts. Check the official Nextcloud Docker Hub page before pinning a future version — the one above may be outdated by then.

# docker-compose.yml
services:
  db:
    image: postgres:16-alpine
    container_name: nextcloud-db
    restart: unless-stopped
    volumes:
      - ./db:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=${POSTGRES_DB}
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: nextcloud-redis
    restart: unless-stopped
    command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
    volumes:
      - ./redis:/data

  app:
    image: nextcloud:${NEXTCLOUD_VERSION}
    container_name: nextcloud-app
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - ./data:/var/www/html
      - ./config:/var/www/html/config
      - ./apps:/var/www/html/custom_apps
    environment:
      - POSTGRES_HOST=db
      - POSTGRES_DB=${POSTGRES_DB}
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - REDIS_HOST=redis
      - REDIS_HOST_PASSWORD=${REDIS_PASSWORD}
      - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
      - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
      - NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com
      - PHP_MEMORY_LIMIT=512M
      - PHP_UPLOAD_LIMIT=10G

  cron:
    image: nextcloud:${NEXTCLOUD_VERSION}
    container_name: nextcloud-cron
    restart: unless-stopped
    depends_on:
      - app
    volumes:
      - ./data:/var/www/html
      - ./config:/var/www/html/config
      - ./apps:/var/www/html/custom_apps
    entrypoint: /cron.sh
    environment:
      - POSTGRES_HOST=db
      - POSTGRES_DB=${POSTGRES_DB}
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - REDIS_HOST=redis
      - REDIS_HOST_PASSWORD=${REDIS_PASSWORD}

  web:
    image: nginx:1.27-alpine
    container_name: nextcloud-web
    restart: unless-stopped
    depends_on:
      - app
    ports:
      - "8080:80"
    volumes:
      - ./data:/var/www/html:ro
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro

3. Create the Nginx Configuration

The Nginx container proxies requests to the app service. Save this as nginx.conf in the ~/nextcloud directory.

upstream php-handler {
    server app:9000;
}

server {
    listen 80;
    server_name cloud.example.com;
    root /var/www/html;

    client_max_body_size 10G;

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    location ^~ /.well-known {
        location = /.well-known/carddav { return 301 /remote.php/dav/; }
        location = /.well-known/caldav  { return 301 /remote.php/dav/; }
        try_files $uri $uri/ =404;
    }

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

    location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/ {
        deny all;
    }

    location ~ \.php(?:$|/) {
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param HTTPS on;
        fastcgi_pass php-handler;
        fastcgi_read_timeout 3600;
    }

    location ~* \.(?:svg|gif|png|html|ttf|woff|ico|jpg|jpeg)$ {
        try_files $uri /index.php$request_uri;
        access_log off;
        expires 30d;
    }
}

4. Launch the Stack

Start all containers in detached mode. The first pull may take several minutes depending on your internet connection.

cd ~/nextcloud && docker compose up -d

Check the status of all services:

docker compose ps

Wait for the database to become healthy, then monitor the app logs for the initial setup completion:

docker compose logs -f app

You should see a message like "Nextcloud is ready" or an Apache startup log. Navigate to http://your-server-ip:8080 to verify the web interface loads.

5. Configure Trusted Domains

If you access Nextcloud via an IP address or a domain that is not in NEXTCLOUD_TRUSTED_DOMAINS, you will see an error. Edit config/config.php inside the container and add your domain to the trusted_domains array.

docker compose exec -u www-data app php occ config:system:set trusted_domains 1 --value=cloud.example.com

For local IP access, add another entry:

docker compose exec -u www-data app php occ config:system:set trusted_domains 2 --value=192.168.1.50

6. Set Up Background Jobs (Cron)

Nextcloud needs periodic tasks for file scanning, expiry of trash, and updates. The cron container runs every 5 minutes by default. Verify it works:

docker compose logs cron

You should see output from the cron script. If not, check the container is running:

docker compose ps cron

7. Configure Redis for Caching and File Locking

Redis is already wired via environment variables. Verify Nextcloud sees it by running:

docker compose exec -u www-data app php occ status

Then check the config file:

docker compose exec -u www-data app cat config/config.php

You should see 'memcache.local' => '\OC\Memcache\Redis' and 'memcache.locking' => '\OC\Memcache\Redis'.

8. Set File Permissions

The app container runs as user www-data (UID 33) inside the container. On your host, the data, config, and apps directories must be writable by that UID. Run id -u and id -g on your host and verify against the image's documentation — the default user in the official Nextcloud image is www-data with UID 33. If your host user has a different UID, you must adjust permissions.

# Check your host UID/GID
id -u && id -g
# If your UID is not 33, change ownership of the directories
sudo chown -R 33:33 data config apps

9. Enable the Nextcloud Skeleton Apps

After first login, enable essential apps from the command line:

docker compose exec -u www-data app php occ app:enable calendar contactspdf_viewer

10. Verify the Installation

Run a full system check from within the container:

docker compose exec -u www-data app php occ maintenance:mode --off && docker compose exec -u www-data app php occ status

Then visit http://your-server-ip:8080 and log in with the admin credentials you set in .env. Complete the initial setup wizard — it should detect PostgreSQL and Redis as already configured.

Advanced Setup & Optimization

Reverse Proxy with Caddy and Automatic SSL

Expose Nextcloud only on the internal network and let Caddy handle TLS termination. Add this to your main docker-compose.yml:

  caddy:
    image: caddy:2
    container_name: nextcloud-caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy_data:/data
      - ./caddy_config:/config
    depends_on:
      - web

Create a Caddyfile:

cloud.example.com {
    reverse_proxy web:80
}

Then remove the ports section from the web service in your compose file to prevent direct access on port 8080.

Backup Strategy

Automate nightly backups of the database and data files. Create a script backup.sh:

#!/bin/bash
set -euo pipefail
cd ~/nextcloud
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
docker compose exec -T db pg_dump -U ${POSTGRES_USER} ${POSTGRES_DB} | gzip > backups/db_${TIMESTAMP}.sql.gz
tar -czf backups/data_${TIMESTAMP}.tar.gz data config apps
find backups -name "*.gz" -mtime +7 -delete

Add it to your crontab:

crontab -e
# Add this line for daily backup at 2 AM
0 2 * * * /home/youruser/nextcloud/backup.sh >/dev/null 2>&1

Optional Hardening

Warning: The following settings restrict container capabilities and may break Nextcloud or its apps if not adapted to your specific environment. Test in a staging environment first.

Add these to your app service to reduce the attack surface:

    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETUID
      - SETGID
      - DAC_OVERRIDE
    read_only: true
    tmpfs:
      - /tmp

If you enable read_only, you must mount additional writable volumes for sessions and logs:

    volumes:
      - ./data:/var/www/html
      - ./config:/var/www/html/config
      - ./apps:/var/www/html/custom_apps
      - ./sessions:/var/www/html/sessions
      - ./tmp:/tmp

Troubleshooting

Common Error Cause Solution
502 Bad Gateway from Nginx PHP-FPM container is down or not ready Run docker compose ps app and docker compose logs app — restart with docker compose restart app
Trusted domain error Your access URL is not in trusted_domains Add it via occ command as shown in Step 5
Database connection refused PostgreSQL is still initializing or credentials mismatch Check docker compose logs db and verify the .env passwords match those in the compose file
Permission denied when writing to data Host UID does not match container UID 33 Run id -u on host and sudo chown -R 33:33 data config apps
Redis connection failed Wrong password or hostname in environment Verify REDIS_HOST_PASSWORD matches the redis service command argument
Upgrade failed during docker compose pull You did not run occ upgrade after pulling a new image Run docker compose exec -u www-data app php occ upgrade
Preview generation broken Missing PHP imagemagick extension Install the nextcloud-imaginary app or configure an external preview service

Conclusion and FAQ

You now have a production-grade Nextcloud v34.0.3 running on Docker with PostgreSQL, Redis, and proper background processing. The setup is reproducible — you can destroy the stack and recreate it from the same files in minutes. The key to long-term success is monitoring your logs weekly and keeping backups tested.

FAQ

Q1: How do I update Nextcloud to a new version?

Pull the new image and recreate the containers: docker compose pull app && docker compose up -d app. Then run the upgrade routine: docker compose exec -u www-data app php occ upgrade. Always back up your database and data files before upgrading. The official docs recommend upgrading one major version at a time, so if you are on v32, go to v33 first, then v34.

Q2: Can I use SQLite instead of PostgreSQL?

Yes, for testing or single-user setups, SQLite works. However, it will lock the database during writes, causing performance degradation with multiple users or when the sync client is active. PostgreSQL is the recommended production choice because it handles concurrent reads and writes better. The official Nextcloud documentation states that SQLite is only suitable for development and small personal instances.

Q3: How do I move my Nextcloud data to a different disk?

Stop the stack with docker compose down. Move the data directory to the new location, e.g., mv data /mnt/large-disk/nextcloud-data. Update the bind mount in docker-compose.yml to point to the new path. Restart with docker compose up -d. Run docker compose exec -u www-data app php occ files:scan --all to update the file cache.

Q4: Why is my upload limit still 10GB despite setting PHP_UPLOAD_LIMIT?

The environment variable sets the PHP and Nginx limits, but the Nextcloud client also has its own chunk size setting. In the desktop client, go to Settings > Advanced and increase the chunk size. Additionally, the reverse proxy in front (if any) must have client_max_body_size set to at least the same value. We included this directive in our Nginx configuration above.

Q5: Is it safe to expose Nextcloud directly to the internet without a reverse proxy?

No. The Nginx container we provide serves HTTP on port 8080. Exposing that port publicly sends your login credentials in plaintext. Always put a reverse proxy with TLS in front. Caddy, as shown in the advanced section, automatically manages Let's Encrypt certificates. If you must use the built-in Nginx, at minimum add TLS certificates and redirect port 80 to 443.

AdSense — In-article (responsive)

Related Guides