Nextcloud 34 Reverse Proxy Setup: Docker Compose Guide for 2026
Step-by-step guide to deploy Nextcloud 34.0.3 behind a reverse proxy using Docker Compose. Includes full configs, security hardening, common errors, and solutions.
Introduction
Running a self-hosted Nextcloud instance in 2026 is no longer just about mounting a volume and exposing port 8080. Modern deployments demand a reverse proxy for TLS termination, HTTP/2, and centralized access control. Without it, you're exposing your data to plaintext traffic and struggling with mixed-content warnings on every page load. This guide walks you through deploying Nextcloud v34.0.3 — the latest official release as of August 2026 — using Docker Compose, with all traffic routed through a single Caddy reverse proxy.
You'll learn how to structure your project directories, set up environment variables securely, configure the necessary PHP and database containers, and wire them behind a reverse proxy with automatic HTTPS. We'll also cover trusted domain configuration, cron job setup, and the most common pitfalls people hit when they move from a bare-metal install to a containerized one.
The focus here is on reproducibility. Every command is copy-paste ready, and every configuration block is explained line by line. By the end, you'll have a production-ready Nextcloud stack that survives container restarts, proxy reconnects, and database backups — without any of the guesswork.
What you will not find in this guide: performance benchmark numbers fabricated from thin air, vague "it should work" advice, or deprecated configuration patterns. All versions referenced are verified against official sources at the time of writing.
Prerequisites / Requirements
Before you begin, ensure your host meets the following baseline specifications. These are typical values; actual usage depends on the number of active users and file sizes. Memory usage is estimated and can vary significantly with PHP workers, database cache, and preview generation.
| Component | Minimum | Recommended | Notes |
|---|---|---|---|
| CPU | 2 cores | 4+ cores | Encryption and preview generation are CPU-bound. |
| RAM | 4 GB | 8 GB | Estimated for 5-10 concurrent users. PHP-FPM and PostgreSQL consume most of it. |
| Storage | 50 GB free | 1 TB+ | Actual requirement depends on your data. Use a separate volume for /var/www/html/data. |
| OS | Debian 12 / Ubuntu 24.04 LTS | Same | Docker Engine 24+ and Docker Compose v2 required. |
| Software | Docker Engine, Docker Compose plugin | Portainer (optional) | Check official Docker docs for installation. |
| Network | Static IP or dynamic DNS | Reverse proxy port 80/443 open | Caddy needs inbound access to issue Let's Encrypt certificates. |
Directory structure
Create a dedicated directory for the whole stack. This keeps related configuration files together and simplifies backups.
mkdir -p ~/nextcloud-stack && cd ~/nextcloud-stack
Step-by-Step Installation
Step 1: Create the .env file
All secrets and version pins belong in .env. This file is excluded from version control and consumed by Docker Compose automatically. Use ${VAR} syntax in docker-compose.yml to reference these values.
cat > ~/nextcloud-stack/.env <<'EOF'
# Nextcloud version pin
NEXTCLOUD_VERSION=34.0.3
# Database credentials
POSTGRES_DB=nextcloud
POSTGRES_USER=nextcloud
POSTGRES_PASSWORD=change_this_strong_password_2026
# Nextcloud admin account (created on first web setup)
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=change_this_admin_password_2026
# Host ports (adjust if conflicts)
CADDY_HTTP_PORT=80
CADDY_HTTPS_PORT=443
# Your domain or subdomain
NEXTCLOUD_DOMAIN=cloud.example.com
EOF
Warning: Never commit .env to Git. Add it to .gitignore immediately. The password above is a placeholder — generate real ones using openssl rand -base64 48.
Step 2: Create the Docker Compose stack
The following docker-compose.yml defines four services: a Caddy reverse proxy, Nextcloud itself, a PostgreSQL database, and a Redis cache for locks and file locking. The setup uses named volumes for persistent data.
cat > ~/nextcloud-stack/docker-compose.yml <<'EOF'
services:
caddy:
image: caddy:2.9-alpine
container_name: nextcloud-caddy
restart: unless-stopped
ports:
- "${CADDY_HTTP_PORT}:80"
- "${CADDY_HTTPS_PORT}:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- nextcloud_network
nextcloud:
image: nextcloud:${NEXTCLOUD_VERSION:-latest}
container_name: nextcloud-app
restart: unless-stopped
depends_on:
- db
- redis
volumes:
- nextcloud_data:/var/www/html
environment:
- POSTGRES_HOST=db
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- REDIS_HOST=redis
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
- NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_DOMAIN}
- PHP_MEMORY_LIMIT=1G
- PHP_UPLOAD_LIMIT=10G
networks:
- nextcloud_network
db:
image: postgres:16-alpine
container_name: nextcloud-db
restart: unless-stopped
volumes:
- db_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
networks:
- nextcloud_network
redis:
image: redis:7.4-alpine
container_name: nextcloud-redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD:-default_redis_pass}
volumes:
- redis_data:/data
networks:
- nextcloud_network
volumes:
nextcloud_data:
db_data:
redis_data:
caddy_data:
caddy_config:
networks:
nextcloud_network:
driver: bridge
EOF
Important: Add REDIS_PASSWORD to your .env file. The command above uses a fallback for safety, but you should define it explicitly.
Step 3: Configure the Caddy reverse proxy
Caddy automatically obtains and renews Let's Encrypt certificates. The Caddyfile routes traffic to the Nextcloud container and sets appropriate headers.
cat > ~/nextcloud-stack/Caddyfile <<'EOF'
{$NEXTCLOUD_DOMAIN} {
reverse_proxy nextcloud:80
request_body {
max_size 10GB
}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
}
}
EOF
Step 4: Set up environment variables for Caddy
Caddy reads the domain from the environment. Add this to your .env file and export it before starting the stack.
echo "NEXTCLOUD_DOMAIN=cloud.example.com" >> ~/nextcloud-stack/.env
Step 5: Launch the stack
Now start all containers in detached mode. This command also creates the named volumes if they don't exist.
cd ~/nextcloud-stack && docker compose up -d
Step 6: Verify container health
Check that all four containers are running and that the Nextcloud container reports a healthy status. The first startup can take a few minutes as it initializes the database schema.
cd ~/nextcloud-stack && docker compose ps
Step 7: Complete the web-based installation
Open https://cloud.example.com in your browser. You'll see the Nextcloud setup page. Use the admin credentials from your .env file to log in. The database connection settings are pre-filled because you passed them as environment variables — do not change them on this screen.
Step 8: Set up cron jobs
Nextcloud relies on background jobs for file scanning, preview generation, and expiration tasks. Replace the default AJAX cron with a system cron that hits the container's cron endpoint every 5 minutes.
crontab -e
Add the following line, adjusting the path if your stack lives elsewhere:
*/5 * * * * docker exec -u www-data nextcloud-app php /var/www/html/cron.php
Step 9: Configure trusted domains
If you access Nextcloud via multiple hostnames (e.g., local IP and domain), add them to the config/config.php file inside the container. This prevents CSRF and host header poisoning.
docker exec -u www-data nextcloud-app php occ config:system:set trusted_domains 1 --value="192.168.1.100"
Step 10: Enable maintenance mode for safe restarts
Before any manual container restart, enable maintenance mode to avoid database corruption from concurrent writes.
docker exec -u www-data nextcloud-app php occ maintenance:mode --on
After the restart, turn it off:
docker exec -u www-data nextcloud-app php occ maintenance:mode --off
Step 11: Install additional apps (optional)
You can install apps from the Nextcloud app store using the occ command. For example, to add the Deck app:
docker exec -u www-data nextcloud-app php occ app:install deck
Step 12: Verify reverse proxy headers
Confirm that Caddy is passing the correct protocol headers to Nextcloud. This ensures that the instance recognizes HTTPS and doesn't generate insecure redirects.
docker exec -u www-data nextcloud-app php occ config:system:set overwriteprotocol --value="https"
Advanced Configuration / Optimization
Backup strategy
A robust backup includes the database dump and the entire nextcloud_data volume. Use pg_dump for the database and tar for the files. Store backups off-site.
cd ~/nextcloud-stack && docker exec nextcloud-db pg_dump -U ${POSTGRES_USER} ${POSTGRES_DB} > nextcloud_backup_$(date +%Y%m%d).sql
cd ~/nextcloud-stack && docker run --rm -v nextcloud_data:/data -v $(pwd):/backup alpine tar czf /backup/nextcloud_files_$(date +%Y%m%d).tar.gz -C /data .
Object storage integration
For large libraries, offload file storage to S3-compatible object storage. This requires installing the External Storage app and configuring it via the web UI. The primary database and metadata stay local.
Performance tuning
- Set
opcache.enable_cli=1in the PHP configuration for faster CLI commands. - Increase the number of PHP-FPM workers if you have spare RAM. Edit
php-fpm.d/www.confinside the container. - Use Redis for distributed locking, which is already configured in this stack.
Optional Hardening
The following security measures are not enabled by default because they require careful adaptation to your specific workload. Copying them blindly may break container functionality.
- Read-only root filesystem: Add
read_only: trueto the Nextcloud service. You must then mount/tmpastmpfsand ensure thenextcloud_datavolume is writable. - Drop all capabilities: Add
cap_drop: [ALL]to the Nextcloud and Redis services. Nextcloud needsCHOWN,FOWNER,DAC_OVERRIDE, andSETGIDto function correctly; Redis needsSETGIDandSETUID. - Run as non-root user: The Nextcloud image runs as
www-data(UID 33). The PostgreSQL image runs as UID 999. Verify withdocker exec <container> idbefore changing user mappings.
Troubleshooting / Common Errors
| Error | Cause | Solution |
|---|---|---|
502 Bad Gateway from Caddy |
Nextcloud container is not ready or crashed | Check docker compose ps and docker logs nextcloud-app. Wait for the database migration to finish. |
Trusted domain error after login |
The overwritehost setting is missing or wrong |
Run docker exec -u www-data nextcloud-app php occ config:system:set trusted_domains 0 --value="cloud.example.com" |
Database connection refused |
PostgreSQL container restarted or credentials mismatch | Verify .env values match. Use docker compose logs db to see authentication errors. |
413 Request Entity Too Large |
Caddy's max_size is too small |
Increase max_size in the Caddyfile and restart Caddy: docker compose restart caddy |
Redis connection error in logs |
Redis password mismatch | Ensure REDIS_PASSWORD in .env matches the --requirepass argument in the Compose file. |
File locking not enabled warning |
Redis not configured for locking | Add 'memcache.locking' => '\OC\Memcache\Redis' to config/config.php |
Caddy certificate issuance failure |
Port 80/443 not reachable from the internet | Open the ports in your firewall and ensure your DNS A record points to your public IP. |
Conclusion & FAQ
Deploying Nextcloud 34.0.3 behind a reverse proxy is a repeatable process once you understand the moving parts: persistent volumes, environment variable injection, and proxy header forwarding. The stack you built in this guide uses Caddy for automatic TLS, PostgreSQL for relational data, and Redis for caching — all standard components in a 2026 self-hosted setup. Regular backups and occasional occ maintenance commands will keep it running for years.
FAQ
Q1: Why use Caddy instead of Nginx or Traefik?
Caddy simplifies TLS management by automatically obtaining and renewing certificates from Let's Encrypt without extra configuration. Its Caddyfile syntax is more readable than Nginx's for basic reverse proxying. Traefik is equally capable but has a steeper learning curve if you're not already using Docker labels extensively.
Q2: Can I use the built-in Apache server without a reverse proxy?
Yes, but you lose automatic HTTPS, HTTP/2, and centralized logging. The built-in server is intended for direct access on a trusted network. Exposing it to the internet without a proxy requires manual certificate management and often results in mixed-content issues if you don't configure the overwriteprotocol setting.
Q3: How do I update Nextcloud to a new minor version?
Change NEXTCLOUD_VERSION in your .env file to the new version tag (e.g., 34.0.4), then run docker compose pull nextcloud followed by docker compose up -d. Always back up the database and data directory before upgrading. Check the official changelog for any manual migration steps.
Q4: What is the recommended way to handle user uploads larger than 10 GB?
Increase the PHP_UPLOAD_LIMIT environment variable and the max_size in the Caddyfile. Also adjust client_max_body_size if you were using Nginx. For truly massive files (50 GB+), consider chunked upload which Nextcloud supports natively via its web interface.
Q5: Is it safe to expose the PostgreSQL port to the host network?
No. Keep the database container on the internal Docker network only. The only service that should connect to it is the Nextcloud container. Exposing port 5432 to the host increases the attack surface and risks unauthorized access if your firewall rules are misconfigured.