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.
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.