Nextcloud Docker Setup: Performance Tuning, External Storage, LDAP, and Scaling for Teams
A basic Nextcloud deployment works. A tuned one works for your whole team at speed. The difference is PHP-FPM and OPcache configured properly, Redis caching that eliminates database lock contention, external S3 storage for cost-effective scaling, LDAP integration so your team uses their existing credentials, and Nextcloud Office turning it from a file sync tool into a real collaboration platform. This guide covers advanced Nextcloud Docker setup for teams moving beyond personal use.
If you haven't deployed Nextcloud yet, start with our Nextcloud deployment guide which covers Docker Compose setup, PostgreSQL, Redis basics, and HTTPS configuration. This guide picks up from a running instance and focuses on performance, scalability, and team features.
Prerequisites
- A running Nextcloud instance with PostgreSQL and Redis — see our deployment guide
- Nextcloud 28+ recommended — this guide uses current configuration patterns
- At least 4GB RAM for comfortable multi-user operation
- Docker and Docker Compose access for configuration changes
- Admin access to the Nextcloud web interface and shell access via
occ
Verify your starting point before making changes:
# Check Nextcloud version and server info
docker exec -u www-data nextcloud php occ status
docker exec -u www-data nextcloud php occ config:system:get version
# Check current PHP settings
docker exec nextcloud php -i | grep -E 'memory_limit|upload_max|opcache'
# Check Redis is working
docker exec -u www-data nextcloud php occ config:system:get memcache.local
# Should return: \OC\Memcache\Redis
# Check for admin warnings
docker exec -u www-data nextcloud php occ check
docker exec -u www-data nextcloud php occ setupchecks
PHP-FPM and OPcache: The Foundation of Nextcloud Performance
The default Nextcloud Docker image uses Apache with mod_php. For production multi-user deployments, switching to the FPM (FastCGI Process Manager) variant with Nginx gives you significantly better concurrency, lower memory per request, and fine-grained worker process control.
Switching to the FPM Image
Update your Docker Compose to use the FPM image with a separate Nginx container as the web server:
# Updated docker-compose.yml for FPM setup
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: nextcloud_db
restart: unless-stopped
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- nextcloud_net
redis:
image: redis:7-alpine
container_name: nextcloud_redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
networks:
- nextcloud_net
nextcloud:
image: nextcloud:28-fpm-alpine # FPM variant — lighter, faster
container_name: nextcloud
restart: unless-stopped
environment:
POSTGRES_HOST: postgres
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
NEXTCLOUD_ADMIN_USER: ${NEXTCLOUD_ADMIN_USER}
NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASSWORD}
NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_DOMAIN}
REDIS_HOST: redis
REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
PHP_MEMORY_LIMIT: 1G
PHP_UPLOAD_LIMIT: 16G
OPCACHE_INTERNED_STRINGS_BUFFER: 32
volumes:
- nextcloud_data:/var/www/html
- ./data:/var/www/html/data
- ./php/custom.ini:/usr/local/etc/php/conf.d/custom.ini:ro
depends_on:
- postgres
- redis
networks:
- nextcloud_net
nginx:
image: nginx:alpine
container_name: nextcloud_nginx
restart: unless-stopped
ports:
- "8080:80"
volumes:
- nextcloud_data:/var/www/html:ro
- ./nginx/nextcloud.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- nextcloud
networks:
- nextcloud_net
# Cron job for background tasks
cron:
image: nextcloud:28-fpm-alpine
container_name: nextcloud_cron
restart: unless-stopped
volumes:
- nextcloud_data:/var/www/html
- ./data:/var/www/html/data
entrypoint: /cron.sh
depends_on:
- postgres
- redis
networks:
- nextcloud_net
volumes:
postgres_data:
redis_data:
nextcloud_data:
networks:
nextcloud_net:
Nginx Configuration for FPM
# nginx/nextcloud.conf
upstream php-handler {
server nextcloud:9000;
}
server {
listen 80;
server_name _;
root /var/www/html;
index index.php index.html;
client_max_body_size 16G;
client_body_timeout 300s;
fastcgi_buffers 64 4K;
# WebDAV
location = /.well-known/carddav { return 301 /remote.php/dav/; }
location = /.well-known/caldav { return 301 /remote.php/dav/; }
location ^~ /.well-known { return 301 /index.php$request_uri; }
# Security headers
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Permitted-Cross-Domain-Policies none;
add_header Referrer-Policy no-referrer;
add_header X-XSS-Protection "1; mode=block";
location / {
rewrite ^ /index.php;
}
location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) {
return 404;
}
location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) {
return 404;
}
location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+|.+\/richdocumentscode\/proxy)\.php(?:$|\/) {
fastcgi_split_path_info ^(.+?\.php)(\/.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass php-handler;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTPS on;
fastcgi_read_timeout 300;
fastcgi_send_timeout 300;
}
location ~ \.(?:css|js|svg|gif|png|jpg|ico|wasm|tflite|map)$ {
try_files $uri /index.php$request_uri;
expires 6M;
access_log off;
}
location ~ \.woff2?$ {
try_files $uri /index.php$request_uri;
expires 7d;
access_log off;
}
location /remote {
return 301 /remote.php$request_uri;
}
location / {
try_files $uri $uri/ /index.php$request_uri;
}
}
OPcache and PHP Tuning
Create a custom PHP ini file to tune OPcache — the biggest single performance improvement for PHP applications:
mkdir -p php
# php/custom.ini
cat > php/custom.ini << 'EOF'
[opcache]
opcache.enable=1
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=10000
opcache.memory_consumption=256
opcache.save_comments=1
opcache.revalidate_freq=60
opcache.jit=1255
opcache.jit_buffer_size=128M
[php]
memory_limit=1G
upload_max_filesize=16G
post_max_size=16G
max_execution_time=3600
max_input_time=3600
output_buffering=0
EOF
# Apply changes by recreating the Nextcloud container
docker compose up -d --force-recreate nextcloud
# Verify OPcache is active
docker exec nextcloud php -r "print_r(opcache_get_status()['opcache_enabled']);"
External Storage: S3, SFTP, and Network Shares
Nextcloud's External Storage app lets you mount remote storage sources as folders in users' files — S3 buckets, SFTP servers, WebDAV shares, and network drives all appear as native Nextcloud folders. For large deployments, this also lets you keep Nextcloud's primary storage on cheap block storage while mounting high-capacity S3 for media and archives.
Mounting S3 or MinIO as Primary Storage
For new deployments at scale, configure S3-compatible storage as Nextcloud's primary object store. This moves all file data to S3 while Nextcloud manages metadata in PostgreSQL:
# Add to Nextcloud's config.php for S3 primary storage:
# IMPORTANT: Only configure this on a FRESH installation
# Changing primary storage on existing data will cause data loss
# docker exec -it nextcloud bash
# nano /var/www/html/config/config.php
# Add inside the $CONFIG array:
'objectstore' => array(
'class' => '\\OC\\Files\\ObjectStore\\S3',
'arguments' => array(
'bucket' => 'nextcloud-primary',
'autocreate' => true,
'key' => 'YOUR_S3_ACCESS_KEY',
'secret' => 'YOUR_S3_SECRET_KEY',
'region' => 'us-east-1',
// For MinIO self-hosted:
'hostname' => 's3.yourdomain.com',
'port' => 443,
'use_ssl' => true,
'use_path_style' => true, // Required for MinIO
),
),
Mounting External Storage for Users
For adding external storage to existing users without changing primary storage, use the External Storage app. Enable it first:
# Enable the External Storage app
docker exec -u www-data nextcloud php occ app:enable files_external
# Mount an S3 bucket as admin-defined external storage (visible to all users)
docker exec -u www-data nextcloud php occ files_external:create \
"Company Archives" \
amazons3 \
amazons3::accesskey \
--config bucket=company-archives \
--config key=YOUR_ACCESS_KEY \
--config secret=YOUR_SECRET_KEY \
--config region=us-east-1 \
--config use_path_style=1
# Mount an SFTP server
docker exec -u www-data nextcloud php occ files_external:create \
"Backup Server" \
sftp \
password::password \
--config host=backup.yourdomain.com \
--config root=/backups \
--config user=sftpuser \
--config password=sftppassword
# List all configured external storages
docker exec -u www-data nextcloud php occ files_external:list
LDAP Integration for Team Authentication
For teams using Active Directory or an LDAP server (including self-hosted Authentik, Keycloak with LDAP, or OpenLDAP), Nextcloud's LDAP app syncs users and groups automatically. Team members log in with their existing credentials and group memberships translate to Nextcloud access controls.
Enabling and Configuring LDAP
# Enable LDAP app
docker exec -u www-data nextcloud php occ app:enable user_ldap
# Create a new LDAP configuration
docker exec -u www-data nextcloud php occ ldap:create-empty-config
# Returns a configuration ID like: s01
# Configure the LDAP connection (replace values for your LDAP server)
# Basic connection
docker exec -u www-data nextcloud php occ ldap:set-config s01 ldapHost "ldap.yourdomain.com"
docker exec -u www-data nextcloud php occ ldap:set-config s01 ldapPort "389"
docker exec -u www-data nextcloud php occ ldap:set-config s01 ldapAgentName "cn=nextcloud,ou=service-accounts,dc=yourdomain,dc=com"
docker exec -u www-data nextcloud php occ ldap:set-config s01 ldapAgentPassword "ldap-service-account-password"
docker exec -u www-data nextcloud php occ ldap:set-config s01 ldapBase "ou=users,dc=yourdomain,dc=com"
# User filter — who can log in
docker exec -u www-data nextcloud php occ ldap:set-config s01 ldapUserFilter "(&(objectClass=person)(memberOf=cn=nextcloud-users,ou=groups,dc=yourdomain,dc=com))"
# Attribute mappings
docker exec -u www-data nextcloud php occ ldap:set-config s01 ldapUserDisplayName "cn"
docker exec -u www-data nextcloud php occ ldap:set-config s01 ldapEmailAttribute "mail"
docker exec -u www-data nextcloud php occ ldap:set-config s01 ldapGroupFilter "(objectClass=groupOfNames)"
docker exec -u www-data nextcloud php occ ldap:set-config s01 ldapGroupMemberAssocAttr "member"
# Enable the configuration
docker exec -u www-data nextcloud php occ ldap:set-config s01 ldapConfigurationActive "1"
# Test the connection
docker exec -u www-data nextcloud php occ ldap:test-config s01
# Check users can be found
docker exec -u www-data nextcloud php occ ldap:search --all
Forcing LDAP User Sync
# Manually sync LDAP users (normally done by background job)
docker exec -u www-data nextcloud php occ user:sync "OCA\\User_LDAP\\User_Proxy" -m disable
# -m disable: disable users who no longer exist in LDAP
# Check which users are synced
docker exec -u www-data nextcloud php occ user:list | head -20
# For Keycloak users: Keycloak supports LDAP federation
# Point Nextcloud at Keycloak's LDAP port (usually 389)
# Or use Keycloak's SAML/OIDC integration instead for SSO
# Add LDAP sync to crontab for regular syncing:
# */15 * * * * docker exec -u www-data nextcloud php occ user:sync \
# "OCA\User_LDAP\User_Proxy" -m disable -q
Nextcloud Office: Real-Time Document Collaboration
Nextcloud Office (powered by Collabora Online) adds real-time collaborative editing for Word documents, spreadsheets, and presentations directly in the browser — the same feature set as Google Docs, running on your own infrastructure.
Deploying Collabora Online
Add Collabora as a service in your Docker Compose stack:
# Add to docker-compose.yml services:
collabora:
image: collabora/code:latest
container_name: collabora
restart: unless-stopped
ports:
- "9980:9980"
environment:
- domain=cloud\.yourdomain\.com # Nextcloud domain (escaped for regex)
- username=admin
- password=${COLLABORA_ADMIN_PASSWORD}
- DONT_GEN_SSL_CERT=YES # Nginx handles SSL
- extra_params=--o:ssl.enable=false --o:ssl.termination=true
cap_add:
- MKNOD
networks:
- nextcloud_net
# Add to .env:
# COLLABORA_ADMIN_PASSWORD=a-strong-collabora-password
Add Nginx routing for Collabora — it needs its own location block with WebSocket support:
# Add a separate Nginx server block for Collabora
# /etc/nginx/sites-available/collabora
server {
listen 443 ssl http2;
server_name office.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/office.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/office.yourdomain.com/privkey.pem;
# Static files
location ^~ /browser {
proxy_pass http://localhost:9980;
proxy_set_header Host $http_host;
}
# WOPI discovery URL
location ^~ /hosting/discovery {
proxy_pass http://localhost:9980;
proxy_set_header Host $http_host;
}
# Capabilities endpoint
location ^~ /hosting/capabilities {
proxy_pass http://localhost:9980;
proxy_set_header Host $http_host;
}
# WebSocket connections for live collaboration
location ~ ^/cool/(.*)/ws$ {
proxy_pass http://localhost:9980;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $http_host;
proxy_read_timeout 36000s;
}
location ~ ^/(c|l)ool {
proxy_pass http://localhost:9980;
proxy_set_header Host $http_host;
}
location ^~ /cool/adminws {
proxy_pass http://localhost:9980;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $http_host;
proxy_read_timeout 36000s;
}
}
# Then configure Nextcloud Office to use this URL:
docker exec -u www-data nextcloud php occ app:enable richdocuments
docker exec -u www-data nextcloud php occ config:app:set richdocuments wopi_url \
--value="https://office.yourdomain.com"
Performance Monitoring and Maintenance
Database Optimization
# Add missing database indices (run after every Nextcloud update)
docker exec -u www-data nextcloud php occ db:add-missing-indices
docker exec -u www-data nextcloud php occ db:add-missing-columns
docker exec -u www-data nextcloud php occ db:add-missing-primary-keys
# Convert legacy column types (run once after major upgrades)
docker exec -u www-data nextcloud php occ db:convert-filecache-bigint
# Run full maintenance repair
docker exec -u www-data nextcloud php occ maintenance:repair
# Clean up old deleted files (trashbin) - keep 30 days
docker exec -u www-data nextcloud php occ trashbin:cleanup --all-users
# Clean up old versions - keep versions younger than 30 days and within quota
docker exec -u www-data nextcloud php occ versions:cleanup
# Check filecache integrity
docker exec -u www-data nextcloud php occ files:cleanup
# Schedule these in crontab on the host:
# 0 3 * * 0 docker exec -u www-data nextcloud php occ db:add-missing-indices -q
# 0 2 * * * docker exec -u www-data nextcloud php occ trashbin:cleanup --all-users -q
Monitoring Nextcloud Performance
# Check system metrics via occ
docker exec -u www-data nextcloud php occ system:cron
docker exec -u www-data nextcloud php occ background:cron
# Check active jobs and their timing
docker exec -u www-data nextcloud php occ background:list
# View slow queries and performance hints:
docker exec -u www-data nextcloud php occ log:manage --level=1
# Enable Nextcloud's built-in monitoring for Prometheus:
docker exec -u www-data nextcloud php occ app:enable serverinfo
# Metrics endpoint (requires admin token):
curl https://cloud.yourdomain.com/ocs/v2.php/apps/serverinfo/api/v1/info \
-H 'OCS-APIRequest: true' \
-u 'admin:password' | jq .ocs.data
# Check database performance:
docker exec nextcloud_db psql -U nextcloud -c \
"SELECT query, mean_exec_time, calls FROM pg_stat_statements \
ORDER BY mean_exec_time DESC LIMIT 10;"
Tips, Gotchas, and Troubleshooting
Performance Still Slow Despite Redis and FPM
# Confirm Redis is actually being used for locking and caching:
docker exec -u www-data nextcloud php occ config:system:get memcache.local
# Should: \OC\Memcache\Redis
docker exec -u www-data nextcloud php occ config:system:get memcache.locking
# Should: \OC\Memcache\Redis
# Check Redis connection stats:
docker exec nextcloud_redis redis-cli -a "$REDIS_PASSWORD" info stats | grep -E 'keyspace_hits|keyspace_misses'
# Hit ratio should be >90% after warmup
# Check OPcache is working:
docker exec nextcloud php -r "
\$s = opcache_get_status();
echo 'Cached: ' . \$s['opcache_statistics']['num_cached_scripts'] . PHP_EOL;
echo 'Hit rate: ' . round(\$s['opcache_statistics']['opcache_hit_rate'], 2) . '%' . PHP_EOL;
"
# Enable APCu as local cache alongside Redis:
docker exec -u www-data nextcloud php occ config:system:set memcache.local --value='\\OC\\Memcache\\APCu'
# Note: APCu is per-process; Redis is shared across workers — use both
Collabora Connection Errors
# Test Collabora is accessible from Nextcloud:
docker exec nextcloud curl -s https://office.yourdomain.com/hosting/discovery | head -5
# Should return XML — if empty/error, Collabora is unreachable
# Check Collabora logs:
docker logs collabora --tail 50 | grep -i error
# Verify the domain pattern in Collabora env matches your Nextcloud domain EXACTLY:
# domain=cloud\.yourdomain\.com (dots escaped with backslash)
# If domain doesn't match, Collabora refuses WOPI connections
# Check WOPI URL is set correctly in Nextcloud:
docker exec -u www-data nextcloud php occ config:app:get richdocuments wopi_url
# Should match your Collabora URL
Large File Uploads Failing
# Verify all three limits are aligned:
# 1. PHP upload limit (in php/custom.ini):
docker exec nextcloud php -i | grep upload_max_filesize
# 2. Nginx client_max_body_size (in nginx config):
grep client_max_body_size nginx/nextcloud.conf
# 3. Nextcloud's own chunk upload setting (in config.php):
docker exec -u www-data nextcloud php occ config:system:get max_chunk_size
# All three must be at least as large as your largest expected file
# Set consistently — the smallest one wins
# For very large files (>10GB), enable chunked upload:
docker exec -u www-data nextcloud php occ config:system:set max_chunk_size --value=104857600
# 104857600 = 100MB chunks
Updating Nextcloud Safely
# 1. Back up database FIRST
docker exec nextcloud_db pg_dump -U nextcloud nextcloud | \
gzip > ~/backups/nextcloud-db-$(date +%Y-%m-%d).sql.gz
# 2. Back up config.php
docker cp nextcloud:/var/www/html/config/config.php \
~/backups/config-$(date +%Y-%m-%d).php
# 3. Enable maintenance mode
docker exec -u www-data nextcloud php occ maintenance:mode --on
# 4. Update image version (increment one major version at a time)
# Change: nextcloud:28-fpm-alpine → nextcloud:29-fpm-alpine
docker compose pull
docker compose up -d
# 5. Run upgrade (may take several minutes)
docker exec -u www-data nextcloud php occ upgrade
# 6. Fix any missing DB indices
docker exec -u www-data nextcloud php occ db:add-missing-indices
# 7. Disable maintenance mode
docker exec -u www-data nextcloud php occ maintenance:mode --off
# 8. Verify
docker exec -u www-data nextcloud php occ status
Pro Tips
- Use Preview Generator for mobile photo performance — install the Preview Generator app and run
occ preview:generate-all -vvvon a schedule. Pre-generated thumbnails make the mobile app photo view fast instead of agonizingly slow as thumbnails generate on demand. - Enable full-text search with Elasticsearch — the default file search only finds filenames. Adding Elastic Search with Nextcloud's Full Text Search app lets users search inside documents. Add an Elasticsearch container to your Compose stack and install the
fulltextsearchandfulltextsearch_elasticsearchapps. - Tune PostgreSQL for Nextcloud's query patterns — add
shared_preload_libraries = 'pg_stat_statements'to your PostgreSQL config and increaseshared_buffersto 25% of available RAM. Nextcloud's file listing queries benefit significantly from proper buffer sizing. - Separate the cron container from the application — the Compose file above runs cron as a dedicated container using Nextcloud's built-in
/cron.sh. This is more reliable than a host crontab and ensures background jobs run even after container restarts. - Monitor quota usage per user — set individual and group quotas under Users → [User] → Quota to prevent any single user from filling the disk. Run
occ user:reportweekly for a summary of usage per user.
Wrapping Up
Advanced Nextcloud Docker setup turns a basic file sync server into a full collaboration platform: PHP-FPM and OPcache that handle concurrent users without performance degradation, S3 external storage that scales with your data rather than your disk size, LDAP integration that eliminates a separate user management system, and Nextcloud Office that lets your team edit documents together without sending anything to Google or Microsoft.
Start with the performance tuning — FPM, OPcache, and proper Redis caching configuration make an immediate, noticeable difference for any multi-user deployment. Then layer in LDAP integration for user management, external storage for scale, and Nextcloud Office when your team is ready for collaborative editing. Each addition builds on a solid, well-performing foundation from our initial deployment guide.
Need Nextcloud Deployed and Optimized for Your Organization?
Running Nextcloud for a team of 20, 50, or 200 users requires careful tuning, proper backup automation, LDAP integration with your existing directory, and a maintenance plan that keeps it running without constant attention. The sysbrix team deploys and manages production Nextcloud instances for organizations that need reliability, not just a working demo.
Talk to Us →