Self-Host Nextcloud
Install a production Nextcloud server using Nginx, PHP-FPM, MariaDB, and Redis — including systemd-managed cron and performance tuning.
Before you start
- ▸A server with at least 2 GB RAM and a public IP address
- ▸A domain name with an A record pointing at the server
- ▸Root or sudo access on the server
- ▸Certbot installed and working for TLS certificate issuance
Nextcloud gives you a self-hosted alternative to Google Drive, Dropbox, and similar services. Running it yourself means your data stays on your hardware, but it also means you are responsible for the stack. This guide walks through a production-grade install using Nginx, PHP-FPM, MariaDB, and systemd-managed cron — the combination that performs best under real load.
Prerequisites and Architecture
You need a server (VPS or bare metal) running a supported Linux distro with at least 2 GB RAM and 20 GB disk. A domain name pointed at the server's IP is assumed. The stack we build:
- Nginx — reverse proxy and static file server
- PHP-FPM — process manager for PHP (PHP 8.2 or 8.3)
- MariaDB 10.11+ — database
- Redis — APCu for local cache, Redis for file locking
- systemd timer — replaces crontab for background jobs
Step 1: Install System Packages
Install the web server, PHP-FPM, and required PHP extensions. Nextcloud's list changes between releases; always check the official requirements page.
Debian / Ubuntu
sudo apt update && sudo apt install -y \
nginx mariadb-server redis-server \
php8.2-fpm php8.2-cli php8.2-mysql php8.2-xml \
php8.2-mbstring php8.2-curl php8.2-zip php8.2-gd \
php8.2-intl php8.2-bcmath php8.2-gmp \
php8.2-imagick php8.2-redis php8.2-apcu \
unzip wget
Fedora / RHEL 9 / Rocky 9
sudo dnf install -y epel-release
sudo dnf install -y nginx mariadb-server redis \
php php-fpm php-mysqlnd php-xml php-mbstring \
php-curl php-zip php-gd php-intl php-bcmath \
php-gmp php-pecl-imagick php-pecl-redis php-pecl-apcu \
unzip wget
Arch Linux
sudo pacman -Syu nginx mariadb redis \
php php-fpm php-gd php-intl php-apcu \
php-imagick redis unzip wget
# php-fpm includes all core extensions on Arch; install extras from AUR as needed
Step 2: Configure MariaDB
Secure the installation, then create a dedicated database and user. Using utf8mb4 with the unicode_ci collation is mandatory for Nextcloud to store emoji and non-ASCII filenames correctly.
sudo systemctl enable --now mariadb
sudo mariadb-secure-installation
sudo mariadb -u root
CREATE DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER 'ncuser'@'localhost' IDENTIFIED BY 'StrongPassHere';
GRANT ALL PRIVILEGES ON nextcloud.* TO 'ncuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Add the following to /etc/mysql/conf.d/nextcloud.cnf (Debian/Ubuntu) or /etc/my.cnf.d/nextcloud.cnf (RHEL/Arch) to tune MariaDB for Nextcloud:
[mysqld]
transaction_isolation = READ-COMMITTED
binlog_format = ROW
innodb_buffer_pool_size = 512M
sudo systemctl restart mariadb
Step 3: Configure PHP-FPM
Edit /etc/php/8.2/fpm/pool.d/www.conf (paths vary; on RHEL it is /etc/php-fpm.d/www.conf). The key settings for a medium workload:
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.max_requests = 500
; These environment variables are required by Nextcloud
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMPDIR] = /tmp
Then tune php.ini (find it with php --ini):
memory_limit = 512M
upload_max_filesize = 16G
post_max_size = 16G
max_execution_time = 360
date.timezone = UTC
opcache.enable = 1
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 10000
opcache.memory_consumption = 128
opcache.save_comments = 1
opcache.revalidate_freq = 1
apc.enable_cli = 1
sudo systemctl enable --now php8.2-fpm
# On RHEL/Rocky:
# sudo systemctl enable --now php-fpm
Step 4: Download and Install Nextcloud
Always grab the latest stable release from nextcloud.com and verify the checksum. Do not install from distro packages — they lag behind on security releases.
cd /tmp
wget https://download.nextcloud.com/server/releases/latest.tar.bz2
wget https://download.nextcloud.com/server/releases/latest.tar.bz2.sha256
sha256sum -c latest.tar.bz2.sha256
sudo tar -xjf latest.tar.bz2 -C /var/www/
sudo chown -R www-data:www-data /var/www/nextcloud
# On RHEL/Arch the web user is 'nginx' or 'http'; adjust accordingly:
# sudo chown -R nginx:nginx /var/www/nextcloud
Step 5: Configure Nginx
Create /etc/nginx/sites-available/nextcloud (Debian/Ubuntu) or /etc/nginx/conf.d/nextcloud.conf (RHEL/Arch) with the configuration below. This follows Nextcloud's official Nginx template — the rewrite rules are required for DAV clients and the mobile apps.
upstream php-handler {
server unix:/run/php/php8.2-fpm.sock;
# RHEL path: unix:/run/php-fpm/www.sock
}
server {
listen 80;
server_name cloud.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name cloud.example.com;
ssl_certificate /etc/letsencrypt/live/cloud.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cloud.example.com/privkey.pem;
root /var/www/nextcloud;
index index.php index.html;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
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;
client_max_body_size 16G;
fastcgi_buffers 64 4K;
location = /robots.txt { allow all; log_not_found off; access_log off; }
location = /.well-known/carddav { return 301 $scheme://$host/remote.php/dav; }
location = /.well-known/caldav { return 301 $scheme://$host/remote.php/dav; }
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/.+|ocs-provider/.+)\.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_pass php-handler;
fastcgi_intercept_errors on;
fastcgi_read_timeout 360;
}
location ~ \.(?:css|js|woff2?|svg|gif|map)$ {
try_files $uri /index.php$request_uri;
expires 6M;
access_log off;
}
location ~ \.(?:png|html|ttf|ico|jpg|jpeg|bcmap|mp4|webm)$ {
try_files $uri /index.php$request_uri;
access_log off;
}
}
# Debian/Ubuntu only:
sudo ln -s /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Step 6: Run the Web Installer
Before visiting the URL, obtain a TLS certificate with Certbot if you have not already:
sudo certbot --nginx -d cloud.example.com
Navigate to https://cloud.example.com in a browser. Fill in the admin account details and database credentials you created in Step 2. Set the data directory to somewhere outside /var/www — for example /srv/nextcloud-data — to avoid accidental web exposure. Create it first:
sudo mkdir -p /srv/nextcloud-data
sudo chown www-data:www-data /srv/nextcloud-data
After the wizard completes, run the post-install OCC commands to set missing indexes and convert columns to bigint:
sudo -u www-data php /var/www/nextcloud/occ db:add-missing-indices
sudo -u www-data php /var/www/nextcloud/occ db:convert-filecache-bigint
Step 7: Set Up Redis for Caching and File Locking
sudo systemctl enable --now redis-server
# Fedora/RHEL: sudo systemctl enable --now redis
Add to /var/www/nextcloud/config/config.php inside the existing array:
'memcache.local' => '\OC\Memcache\APCu',
'memcache.locking' => '\OC\Memcache\Redis',
'memcache.distributed'=> '\OC\Memcache\Redis',
'redis' => [
'host' => 'localhost',
'port' => 6379,
'timeout' => 1.5,
],
Step 8: Replace Crontab with a systemd Timer
Nextcloud's background jobs must run every 5 minutes. A systemd timer is more reliable than cron and integrates with journalctl for logging.
Create /etc/systemd/system/nextcloud-cron.service:
[Unit]
Description=Nextcloud background job
After=network.target
[Service]
Type=oneshot
User=www-data
ExecStart=/usr/bin/php -f /var/www/nextcloud/cron.php
Create /etc/systemd/system/nextcloud-cron.timer:
[Unit]
Description=Run Nextcloud background jobs every 5 minutes
[Timer]
OnBootSec=5min
OnUnitActiveSec=5min
Unit=nextcloud-cron.service
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now nextcloud-cron.timer
Tell Nextcloud to use system cron rather than AJAX:
sudo -u www-data php /var/www/nextcloud/occ background:cron
Verification
# Check the timer is firing
systemctl list-timers nextcloud-cron.timer
# Run the Nextcloud security scan
sudo -u www-data php /var/www/nextcloud/occ security:check
# Check overall status
sudo -u www-data php /var/www/nextcloud/occ status
The admin panel at https://cloud.example.com/settings/admin/overview will also flag any outstanding configuration warnings — work through these before putting the instance into production.
Troubleshooting
- 502 Bad Gateway: PHP-FPM is not running or the socket path in Nginx does not match. Check
systemctl status php8.2-fpmand confirm the socket path withgrep listen /etc/php/8.2/fpm/pool.d/www.conf. - 413 Request Entity Too Large:
client_max_body_sizein Nginx orupload_max_filesizeinphp.iniis too low. Both must be set; the smaller one wins. - Redis connection refused: Redis binds only to 127.0.0.1 by default, which is correct. Confirm with
redis-cli ping. On SELinux systems (RHEL/Rocky), runsetsebool -P httpd_can_network_connect 1. - Slow file listing after many files: Run
sudo -u www-data php /var/www/nextcloud/occ files:scan --allonce and ensure the bigint conversion from Step 6 completed successfully. - OCC commands fail with permission errors: Always prefix with
sudo -u www-data. Running as root corrupts file ownership inside the data directory.
Frequently asked questions
- Can I use PostgreSQL instead of MariaDB?
- Yes. Nextcloud fully supports PostgreSQL 14+. Create the database with UTF-8 encoding and LC_COLLATE='C' for best compatibility, then install the php-pgsql extension in place of php-mysql.
- Do I need a separate Redis instance, or can I share one?
- You can share a single Redis instance by assigning Nextcloud a unique database index (add 'dbindex' => 1 to the redis array in config.php). Dedicated instances are safer in multi-tenant environments.
- How do I upgrade Nextcloud to a new major version?
- Put the instance in maintenance mode with 'occ maintenance:mode --on', back up the database and data directory, then use the built-in web updater or the occ upgrade command. Never skip major versions.
- Why not use the all-in-one Docker image instead of this manual setup?
- The AIO Docker image is excellent for low-maintenance homelabs. A manual stack gives you precise control over PHP tuning, storage paths, and reverse proxy configuration — important when running alongside other services or on shared infrastructure.
- How do I handle large file uploads reliably?
- Set client_max_body_size in Nginx, upload_max_filesize and post_max_size in php.ini, and fastcgi_read_timeout to a high value (360s or more). For very large files, the desktop or mobile sync client with chunked upload is more reliable than the browser.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.