Hi everyone,
I wanted to share a "maximum performance" guide for running Nextcloud. Many default tutorials suggest the standard Apache image with MariaDB, which is fine for small setups but can feel sluggish as you grow.
I have put together a stack that uses **Nextcloud FPM + Nginx + Postgres + Redis**. It also fully integrates **OnlyOffice** so you can edit documents right in the browser, something that is often tricky to configure correctly because of mixed content errors or missing headers.
Its based on this (nor defunct and not working) setup provided by Onlyoffice themselves. No idea why they dont maintain it to work properly.
### Why is this faster?
**PHP-FPM vs Apache:** We are using the `nextclo...
Hi everyone,
I wanted to share a "maximum performance" guide for running Nextcloud. Many default tutorials suggest the standard Apache image with MariaDB, which is fine for small setups but can feel sluggish as you grow.
I have put together a stack that uses **Nextcloud FPM + Nginx + Postgres + Redis**. It also fully integrates **OnlyOffice** so you can edit documents right in the browser, something that is often tricky to configure correctly because of mixed content errors or missing headers.
Its based on this (nor defunct and not working) setup provided by Onlyoffice themselves. No idea why they dont maintain it to work properly.
### Why is this faster?
**PHP-FPM vs Apache:** We are using the `nextcloud:stable-fpm` image. This separates the PHP processing from the web server, allowing us to tune the worker processes specifically for high throughput.
**Postgres 17:** We are using PostgreSQL instead of MariaDB/MySQL. In my experience, Postgres handles concurrent read/writes better for Nextcloud's heavy file locking operations.
**Tuned Nginx:** The Nginx config included below enables HTTP/2, gzip compression, and aggressive caching for static assets.
**Redis:** Handles transactional file locking and caching to keep the web interface snappy.
**Custom Workers:** The config includes a tuned `www.conf\` that increases the `pm.max_children` limit, preventing the server from choking during heavy syncs.
---
### The Setup Guide
**Prerequisites:** A Linux server (Debian/Ubuntu recommended) with Docker and Docker Compose installed.
#### Step 1: Directory Structure
Create a folder for your project. Inside it, create the following subfolders for the configuration files:
nextcloud-stack/
โโโ compose.yaml
โโโ nginx.conf
โโโ php-fpm/
โโโ www.conf
โโโ docker.conf
โโโ zz-docker.conf
#### Step 2: The `compose.yaml`
Save this file in the root of your folder.
*Note: I have set the network to be created automatically for simplicity. Replace the `REDACTED` passwords with strong secure strings.*
services:
# Database (Postgres 17)
nc_postgres:
image: postgres:17-alpine
container_name: nc_postgres
restart: unless-stopped
volumes:
- ./z_postgres_data:/var/lib/postgresql/data:Z
environment:
- POSTGRES_PASSWORD=REDACTED_DB_PASSWORD
- POSTGRES_DB=nextcloud
- POSTGRES_USER=nextcloud
# Redis (Cache & Locking)
nc_redis:
image: redis:alpine
container_name: nc_redis
command: redis-server --requirepass REDACTED_REDIS_PASSWORD
restart: unless-stopped
volumes:
- ./z_redis_data:/data
# Nginx (Web Server)
nc_nginx:
container_name: nc_nginx
image: nginx:alpine
restart: unless-stopped
ports:
- 8080:80 # Access via http://your-ip:8080
# - 443:443 # Uncomment if configuring SSL directly here
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./z_nextcloud_data:/var/www/html:ro
depends_on:
- nc_app
- nc_onlyoffice_documentserver
# Nextcloud (PHP-FPM)
nc_app:
container_name: nc_app
image: nextcloud:stable-fpm
restart: unless-stopped
environment:
# - NEXTCLOUD_TRUSTED_DOMAINS=your.domain.com
- POSTGRES_HOST=nc_postgres
- POSTGRES_DB=nextcloud
- POSTGRES_USER=nextcloud
- POSTGRES_PASSWORD=REDACTED_DB_PASSWORD
- REDIS_HOST=nc_redis
- REDIS_HOST_PASSWORD=REDACTED_REDIS_PASSWORD
- PHP_MEMORY_LIMIT=2048M
- PHP_UPLOAD_LIMIT=4096M
volumes:
- ./z_nextcloud_data:/var/www/html
# Map our custom PHP config tweaks
- ./php-fpm:/usr/local/etc/php-fpm.d
depends_on:
- nc_postgres
- nc_redis
# OnlyOffice Document Server
nc_onlyoffice_documentserver:
container_name: nc_onlyoffice_documentserver
image: onlyoffice/documentserver:latest
restart: unless-stopped
environment:
- JWT_ENABLED=true
- JWT_SECRET=REDACTED_JWT_SECRET
- JWT_HEADER=AuthorizationJwt
- JWT_IN_BODY=true
volumes:
- ./z_documentserver/logs:/var/log/onlyoffice
- ./z_documentserver/data:/var/www/onlyoffice/Data
- ./z_documentserver/lib:/var/lib/onlyoffice
- ./z_documentserver/rabbitmq:/var/lib/rabbitmq
- ./z_documentserver/redis:/var/lib/redis
- ./z_documentserver/db:/var/lib/postgresql
depends_on:
- nc_app
networks:
default:
driver: bridge
#### Step 3: The `nginx.conf`
This file connects Nginx to the PHP-FPM container. Save this as `nginx.conf` in the root folder.
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
types {
text/javascript mjs;
application/wasm wasm;
}
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
server_tokens off;
keepalive_timeout 65;
map $arg_v $asset_immutable {
"" "";
default ", immutable";
}
upstream php-handler {
server nc_app:9000;
}
server {
listen 80;
# Upload size limit - adjust as needed
client_max_body_size 512M;
client_body_timeout 300s;
fastcgi_buffers 64 4K;
client_body_buffer_size 512k;
# Gzip settings
gzip on;
gzip_vary on;
gzip_comp_level 4;
gzip_min_length 256;
gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
gzip_types application/atom+xml text/javascript application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
# Headers
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Robots-Tag "noindex, nofollow" always;
add_header X-XSS-Protection "1; mode=block" always;
fastcgi_hide_header X-Powered-By;
root /var/www/html;
index index.php index.html /index.php$request_uri;
location = / {
if ( $http_user_agent ~ ^DavClnt ) {
return 302 /remote.php/webdav/$is_args$args;
}
}
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/; }
location /.well-known/acme-challenge { try_files $uri $uri/ =404; }
location /.well-known/pki-validation { try_files $uri $uri/ =404; }
return 301 /index.php$request_uri;
}
location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { return 404; }
location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; }
location ~ \.php(?:$|/) {
rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri;
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
set $path_info $fastcgi_path_info;
try_files $fastcgi_script_name =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $path_info;
fastcgi_param modHeadersAvailable true;
fastcgi_param front_controller_active true;
fastcgi_pass php-handler;
fastcgi_intercept_errors on;
fastcgi_request_buffering off;
fastcgi_max_temp_file_size 0;
}
location ~ \.(?:css|js|mjs|svg|gif|ico|jpg|png|webp|wasm|tflite|map|ogg|flac)$ {
try_files $uri /index.php$request_uri;
add_header Cache-Control "public, max-age=15778463$asset_immutable";
access_log off;
location ~ \.wasm$ { default_type application/wasm; }
}
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;
}
}
}
#### Step 4: The PHP Config (`php-fpm` folder)
We need to tune the workers. The default config is often too conservative.
**File 1: `php-fpm/www.conf\`\*\*
This is where the performance magic happens. We switch to `dynamic` process management and increase the child limits.
[www]
user = www-data
group = www-data
listen = 127.0.0.1:9000
pm = dynamic
pm.max_children = 500
pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 30
**File 2: `php-fpm/docker.conf`**
Ensures logs go to the docker output.
[global]
error_log = /proc/self/fd/2
log_limit = 8192
[www]
access.log = /proc/self/fd/2
clear_env = no
catch_workers_output = yes
decorate_workers_output = no
**File 3: `php-fpm/zz-docker.conf`**
Prevents the container from daemonizing.
[global]
daemonize = no
[www]
listen = 9000
#### Step 5: Launch & Connect OnlyOffice
Run `docker compose up -d`.
Open your browser to `http://YOUR_SERVER_IP:8080`.
**Nextcloud Setup Wizard:**
* **Database:** Select PostgreSQL.
* **Host:** `nc_postgres` (This matches the container name in compose).
* **User/Pass:** Use the values you set in `compose.yaml`.
- **Connect OnlyOffice:**
* Install the "ONLYOFFICE" app from the Nextcloud Apps menu.
* Go to Settings -> Administration -> ONLYOFFICE.
* **ONLYOFFICE Docs address:** `http://nc_onlyoffice_documentserver` (This uses the internal docker network, which is super fast and avoids loopback issues).
* **Secret Key:** Enter the `JWT_SECRET` you defined in `compose.yaml`.
* Save.
You should now be able to open docx/xlsx files instantly.
Enjoy!