Installing Nextcloud on your own server

Published on 14 min read

From an empty server to an overview page without warnings: web server, PHP modules, database, data directory permissions, trusted_domains, upload limits and background jobs via cron.

Unpacking Nextcloud takes seconds. The part that costs time comes afterwards: on a fresh installation, the overview page under Administration settings almost always shows a list of yellow and red warnings, uploads stop at 2 MB, and opening the site by IP address ends with "You are accessing the site from an untrusted domain.". This article walks the whole way through and slows down at exactly the points where most guides stop.

Decide up front: PHP version, database, storage location

Nextcloud depends on the PHP version more than most other server software. The current series 33 and 34 require at least PHP 8.2, series 32 still runs from PHP 8.1 upwards. That means your distribution decides whether the stock packages are enough:

SystemPHP from the distributionDatabase from the distributionVerdict
Debian 13 (trixie)8.4MariaDB 11.8fine without a third-party repository
Debian 12 (bookworm)8.2MariaDB 10.11fine, but right at the lower edge
Ubuntu 24.04 LTS8.3MariaDB 10.11, MySQL 8.0fine without a third-party repository
Ubuntu 22.04 LTS8.1MariaDB 10.6, MySQL 8.0too old for Nextcloud 33 and 34
Debian 11 (bullseye)7.4MariaDB 10.5not an option

Debian 11 is the harshest case, and it still tends to surface late, because the package installation runs through without a single error. The php metapackage pulls in PHP 7.4 there, and the current Nextcloud version dies on the first browser request with HTTP 500 and the message "This version of Nextcloud requires at least PHP 8.2". Debian 11 has left regular support anyway, so it is the wrong basis for a new installation. If it really has to be Debian 11, add the Sury repository first and install explicitly versioned packages, so php8.2-fpm, php8.2-cli, php8.2-mysql and so on, instead of the unversioned metapackages.

On Ubuntu 22.04 you run into the same wall the moment you install the current Nextcloud version. Either you deliberately stay on series 32, or you pull PHP from the well known PPA:

sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:ondrej/php
sudo apt-get update
sudo apt-get install -y php8.3-fpm php8.3-cli php8.3-mysql

Two more points that you decide at the beginning and can only change with pain later: Debian ships no mysql-server package at all, MariaDB is the given choice there. And the data directory does not belong under /var/www/nextcloud/data, it belongs outside the web server root, for example in /var/nextcloud-data. The default path is dangerous for one reason only: a broken web server configuration will otherwise hand out every user file. Nextcloud does warn about that with "Your data directory and files are probably accessible from the internet", but only once the mistake already exists.

Setting up the web server, PHP and the database

We use nginx with PHP-FPM. If you prefer Apache with mod_php, that route is described in Apache, PHP and MySQL on Debian, and the PHP topics further down apply unchanged. A basic nginx installation is covered in installing nginx.

sudo apt-get update
sudo apt-get install -y nginx mariadb-server
sudo apt-get install -y php-fpm php-cli php-mysql php-gd php-curl php-mbstring php-intl php-gmp php-bcmath php-xml php-zip php-imagick php-apcu

This list is deliberately longer than the bare minimum. Nextcloud needs bcmath and gmp for passwordless login, intl for correct sorting of accented and special characters, imagick for preview images, apcu for the local cache. If one of the mandatory modules is missing, the setup wizard will not let you continue at all, and the page then lists the missing modules by name.

Afterwards, check what is actually loaded:

php -v
php -m

The second common stumbling block: there are two separate PHP configurations, one for the command line and one for FPM. php --ini shows you the command line one, the web server one lives in /etc/php/<version>/fpm/php.ini. Changes to the wrong file have no effect, and in practice that costs more time than anything else.

Now the database. Harden MariaDB first, see securing MariaDB and MySQL, then:

sudo mariadb -e "CREATE DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
sudo mariadb -e "CREATE USER 'nextcloud'@'localhost' IDENTIFIED BY 'HierEinLangesPasswort';"
sudo mariadb -e "GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'localhost';"
sudo mariadb -e "FLUSH PRIVILEGES;"

The utf8mb4 in the first command is not a detail. Create the database with utf8 and Nextcloud will later report "MySQL is used as database but does not support 4-byte characters", and converting a running instance is considerably more unpleasant than getting the CREATE DATABASE right at the start. If the database user cannot log in, fixing Access denied for user helps.

Unpacking and setting permissions

sudo apt-get install -y wget unzip
wget https://download.nextcloud.com/server/releases/latest.zip
sudo unzip -q latest.zip -d /var/www
sudo mkdir -p /var/nextcloud-data
sudo chown -R www-data:www-data /var/www/nextcloud
sudo chown -R www-data:www-data /var/nextcloud-data
sudo chmod 750 /var/nextcloud-data

Permissions are where corners get cut most often. Three error messages and what causes them:

  • "Cannot write into config directory": /var/www/nextcloud/config is not owned by the web server user. On Debian and Ubuntu that is www-data, on AlmaLinux and Rocky it is apache or nginx instead. A blindly copied chown www-data achieves nothing on the Red Hat family.
  • "Can't create or write into the data directory": the path does not exist, it is not an absolute path, or a parent directory cannot be traversed by www-data.
  • "Your data directory is readable by other users": permissions are too wide. chmod 750 on the data directory is enough.

Resist the temptation to solve the problem with chmod -R 777. Nextcloud answers that with exactly the warning you wanted to get rid of, and along the way you have made every file readable for every local user.

The nginx configuration

Nextcloud needs more than a standard PHP block, among other things rewrites for service discovery under /.well-known/ and blocks on internal directories. The following is the shortened version of the official template and works as it is:

upstream php-handler {
    server unix:/run/php/php8.3-fpm.sock;
}

server {
    listen 80;
    server_name cloud.example.com;
    root /var/www/nextcloud;

    client_max_body_size 10G;
    client_body_timeout 300s;
    fastcgi_buffers 64 4K;

    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;

    index index.php index.html /index.php$request_uri;

    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; }
        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\/.+) /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 front_controller_active true;
        fastcgi_pass php-handler;
        fastcgi_request_buffering off;
        fastcgi_max_temp_file_size 0;
    }

    location ~ \.(?:css|js|mjs|svg|gif|ico|jpg|png|webp|wasm|map|woff2)$ {
        try_files $uri /index.php$request_uri;
        expires 6M;
        access_log off;
    }

    location / {
        try_files $uri $uri/ /index.php$request_uri;
    }
}

You have to adapt the path to the FPM socket to your PHP version. On Debian 13 it is called php8.4-fpm.sock, on Debian 12 php8.2-fpm.sock, on Ubuntu 24.04 php8.3-fpm.sock and on Ubuntu 22.04 php8.1-fpm.sock. If the name does not match, you get a 502 Bad Gateway. The actual name is shown by:

systemctl enable --now php8.4-fpm
ls /run/php/

The first line belongs there, because the socket file only appears once the service starts. Before that, /run/php/ is either empty or does not exist at all, and ls answers with No such file or directory. On a normal server the package starts the service during installation, but after a rebuild or inside a container that is not guaranteed. Adapt the version number in the service name to your installation. After that, run nginx -t and reload.

Setup, HTTPS and trusted_domains

You can click through the setup in the browser or do it on the command line right away. The second option is reproducible and can go straight into a script:

cd /var/www/nextcloud
sudo -u www-data php occ maintenance:install --database "mysql" --database-name "nextcloud" --database-user "nextcloud" --database-pass "HierEinLangesPasswort" --admin-user "admin" --admin-pass "EinAnderesLangesPasswort" --data-dir "/var/nextcloud-data"

Two traps here. First, the command has to run from inside the Nextcloud directory, otherwise PHP aborts with a fatal error. Second, occ must never run as root, otherwise you get "Console has to be executed with the user that owns the file config/config.php", and in the worst case freshly created files end up owned by the wrong user afterwards.

Now HTTPS. Without a certificate the mobile apps refuse to connect and Nextcloud warns you in the overview:

sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d cloud.example.com

For several subdomains a wildcard certificate is worth it. Afterwards, add the header Strict-Transport-Security "max-age=15552000; includeSubDomains" always; to the TLS server block, otherwise the notice "The Strict-Transport-Security HTTP header is not configured to at least 15552000 seconds" stays where it is.

The classic one at the end: you open the site under a different name than the one used during installation and see nothing but "You are accessing the site from an untrusted domain." Nextcloud accepts host names only if they are listed in trusted_domains. To add one without editing the file by hand:

sudo -u www-data php occ config:system:set trusted_domains 1 --value=cloud.example.com
sudo -u www-data php occ config:system:get trusted_domains

The index starts counting at 0, and 0 is usually taken already. Use the same index twice and you overwrite the existing entry, which may lock you out of your own instance. If that happens: config/config.php is a perfectly ordinary PHP file, so you can correct the entry there in an editor. Also set overwrite.cli.url to the final HTTPS address, otherwise the background jobs generate links with the wrong host name.

Upload size and memory limit

The PHP defaults are too tight for Nextcloud. upload_max_filesize is typically set to 2M, memory_limit to 128M. Nextcloud recommends at least 512M of memory and otherwise warns with "The PHP memory limit is below the recommended value of 512MB".

There are three places involved here, and touching just one of them is not enough:

  1. The FPM configuration in /etc/php/<version>/fpm/php.ini: memory_limit = 512M, upload_max_filesize = 10G, post_max_size = 10G, max_execution_time = 3600. Then restart FPM, reloading nginx is not enough.
  2. The file .user.ini in the Nextcloud directory: Nextcloud ships its own values, and because .user.ini applies per directory, it wins against the global php.ini. That is precisely where troubleshooting goes wrong again and again. Adjust the values there as well. PHP caches this file, by default for five minutes, so your change takes effect with a delay.
  3. The nginx directive client_max_body_size: if it is missing or too small, the upload fails with "413 Request Entity Too Large" before PHP is even asked.

To check what actually arrives at the end, use the Administration settings page, which shows the limit that is really in effect. On the command line:

grep -E '^(memory_limit|upload_max_filesize|post_max_size)' /etc/php/8.4/fpm/php.ini

Query the FPM file explicitly, not the command line. A php -r "echo ini_get('memory_limit');" reads the CLI SAPI, and on every distribution we checked that reports -1, meaning unlimited. Anyone who trusts that assumes the value is sufficient and still runs into memory errors later, because the FPM file still says memory_limit = 128M. What really arrives in the browser is shown by php-fpm8.4 -i or by a briefly placed info.php with phpinfo() that you delete again immediately afterwards.

If the kernel kills the process while large files are being uploaded, you are simply out of memory. In that case setting up swap helps as a stopgap, more RAM is the better answer.

Switching background jobs to cron

After the installation Nextcloud runs in AJAX mode: background jobs only run while somebody has the interface open. That is why full text search, cleanup work and notifications seem to never happen on lightly used instances. Switch to real cron, Nextcloud expects a run every five minutes. The basics are covered in setting up a cron job on Linux.

sudo crontab -u www-data -e

Enter this there:

*/5 * * * * php -f /var/www/nextcloud/cron.php

Then tell Nextcloud about the change of mode:

cd /var/www/nextcloud
sudo -u www-data php occ background:cron

If you prefer to work without a cron daemon, use a systemd service with a timer. The unit nextcloudcron.service calls /usr/bin/php -f /var/www/nextcloud/cron.php as user www-data, and the matching timer sets OnBootSec=5min and OnUnitActiveSec=5min.

If the warning "Last background job execution ran X hours ago. Something seems wrong" stays anyway, check in this order: is the job running as the correct user? Does the path really exist? And most importantly: can cron.php run at all with the command line PHP configuration, or is a module missing there that was only installed for FPM? The most honest test is calling it by hand, because that way you see every error message in plain text:

sudo -u www-data php -f /var/www/nextcloud/cron.php

Working through the warnings in the overview

The list under Administration settings and Overview is not decoration, every line has a concrete reason. The most common ones and how to fix them:

  • "No memory cache has been configured": APCu is installed but not registered. occ config:system:set memcache.local --value='\OC\Memcache\APCu'. So that occ and the cron runs benefit from it too, also set apc.enable_cli=1 in /etc/php/<version>/mods-available/apcu.ini.
  • "Transactional file locking is disabled": install Redis (apt-get install -y redis-server php-redis) and set memcache.locking to \OC\Memcache\Redis. Optional on single user instances, no longer optional as soon as several users sync at the same time.
  • "Your web server is not properly set up to resolve /.well-known/caldav": the rewrites in the location ^~ /.well-known block are missing. You can test this directly with curl -I https://cloud.example.com/.well-known/caldav, a 301 to /remote.php/dav/ is expected.
  • "Your installation has no default phone region set": occ config:system:set default_phone_region --value="AT", or DE for Germany. The value is a country code according to ISO 3166-1.
  • "Server has no maintenance window start time configured": occ config:system:set maintenance_window_start --type=integer --value=1. The value is the start hour in UTC, so expensive daily jobs run at night instead of in the middle of business hours.
  • "The database is missing some indexes": occ db:add-missing-indices, together with occ db:add-missing-columns and occ db:add-missing-primary-keys. These commands are slow on large instances, but harmless.
  • "PHP does not seem to be setup properly to query system environment variables": in the FPM pool file /etc/php/<version>/fpm/pool.d/www.conf, uncomment the line env[PATH] = /usr/local/bin:/usr/bin:/bin and restart FPM.
  • "Module php-imagick in this instance has no SVG support": this is not a Nextcloud bug, it is a missing delegate library in ImageMagick. If you do not need SVG previews, you can simply leave the notice standing.

How to tell that it really works

Four checks that are meaningful when taken together:

cd /var/www/nextcloud
sudo -u www-data php occ status
sudo -u www-data php occ check
sudo -u www-data php occ config:app:get core lastcron

occ status has to report installed: true and the expected version, occ check must not produce any output. The third command returns a Unix timestamp. Convert it: it must not be older than five minutes, and only then is your cron really working.

From the outside:

curl -s https://cloud.example.com/status.php

The answer is a JSON object with "installed":true, "maintenance":false and the version number. If HTML comes back instead, one of your location rules matches too broadly. If you get a redirect to the login page, everything is fine, but you have hit the wrong URL.

Finally, the practical test that no status page can replace: upload a file of several gigabytes through the web interface and then sync it with the desktop client. Only then does it become clear whether client_max_body_size, post_max_size, timeouts and the free disk space fit together. If everything suddenly grinds to a halt, take a look at full disks, because Nextcloud creates previews and versions that grow noticeably.

When things go wrong

Nextcloud logs to /var/nextcloud-data/nextcloud.log, so into your data directory, not to /var/log. That is the first file you should look at, not the nginx log. For readable output:

sudo -u www-data php occ log:watch

If the instance is stuck in maintenance mode after a failed update, occ maintenance:mode --off brings it back. If the interface is no longer reachable at all, set 'maintenance' => false directly in config/config.php.

Before you start tinkering with the database or the configuration, back up both. A directory backup on its own is not enough, Nextcloud is worthless without the matching database:

sudo -u www-data php occ maintenance:mode --on
sudo mariadb-dump --single-transaction nextcloud > /root/nextcloud-db.sql
sudo -u www-data php occ maintenance:mode --off

And one piece of general advice: finish building the server before you make it publicly reachable. A firewall, a hardened SSH access and the points from the checklist for new root servers belong before the first login, not after it. A Nextcloud instance with a default password is found within hours.

Frequently asked questions

Which PHP version do I need for Nextcloud?
Nextcloud 33 and 34 require at least PHP 8.2 and support up to 8.5, Nextcloud 32 runs from PHP 8.1 upwards. Debian 13 (PHP 8.4), Debian 12 (PHP 8.2) and Ubuntu 24.04 (PHP 8.3) fit without a third-party repository. Ubuntu 22.04 only ships PHP 8.1 and is therefore too old for the current series, so you need the ondrej/php PPA there. Debian 11 ships PHP 7.4: the installation runs through without errors, but Nextcloud dies on the first request with HTTP 500 and "This version of Nextcloud requires at least PHP 8.2".
Why do I get "You are accessing the site from an untrusted domain"?
Nextcloud only accepts host names that are listed in the trusted_domains array in config/config.php. Add the name with sudo -u www-data php occ config:system:set trusted_domains 1 --value=cloud.example.com. The index starts counting at 0, and an index that is already in use gets overwritten.
Why do uploads still fail after raising the php.ini values?
There are three limits. Besides upload_max_filesize and post_max_size in the FPM php.ini, Nextcloud ships its own .user.ini in the installation directory, which wins because it applies per directory, and nginx limits the request separately through client_max_body_size. If the latter is too small, you get 413 Request Entity Too Large before PHP is even asked.
Why are my background jobs not running?
Fresh installations are set to AJAX, where jobs only run while the interface is open. Add */5 * * * * php -f /var/www/nextcloud/cron.php to the crontab of www-data and switch over with occ background:cron. You can verify it with occ config:app:get core lastcron, where the timestamp must not be older than five minutes.
What permissions does the data directory need?
It belongs to the web server user (www-data on Debian and Ubuntu, apache or nginx on the Red Hat family) and should be set to chmod 750. Permissions that are too wide trigger the message "Your data directory is readable by other users". Create the directory outside the web server root, for example under /var/nextcloud-data.
How do I get rid of the missing memory cache warning?
Install php-apcu and set occ config:system:set memcache.local --value='\OC\Memcache\APCu'. So that occ and the cron runs use the cache as well, also set apc.enable_cli=1 in the apcu.ini of the PHP command line.
Can I run Nextcloud with PostgreSQL as well?
Yes, PostgreSQL is supported on equal terms and is even named as the preferred option in the documentation. On Debian that also settles the MySQL question, since MySQL is not available as a package there anyway. During setup you pass --database "pgsql" instead of "mysql".

Nextcloud Selfhosting PHP nginx MariaDB Debian Ubuntu Cloud Linux