Nextcloud is a self-hosted platform for file sync and sharing, calendars, contacts and collaboration, which keeps your data on a server you control. In this tutorial you will install Nextcloud on Ubuntu 24.04 with Nginx, PHP-FPM 8.3, MariaDB and Redis, run the installation from the command line with occ, secure it with Let's Encrypt, and configure caching and background jobs so the admin overview shows no warnings.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS with at least 2 GB of RAM (4 GB for more than a handful of users), for example a CubePath VPS.
  • Enough disk space for your files. The data directory in this guide is /srv/nextcloud-data; mount a separate volume there if you expect a lot of data.
  • A non-root user with sudo privileges.
  • A subdomain with an A record pointing to the server, referred to as cloud.your_domain.
  • Ports 80 and 443 open to the internet.

Step 1 - Installing Nginx, PHP-FPM and the required modules

Install Nginx, PHP-FPM and the PHP modules Nextcloud requires or recommends, plus Redis for file locking and bzip2 to unpack the release archive:

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

Ubuntu 24.04 installs PHP 8.3, which current Nextcloud releases support. Check the services:

systemctl is-active nginx php8.3-fpm redis-server
active
active
active

Open the firewall:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Step 2 - Tuning PHP for Nextcloud

Nextcloud needs more memory and a larger OPcache string buffer than the defaults. Create an override file for PHP-FPM:

sudo nano /etc/php/8.3/fpm/conf.d/90-nextcloud.ini
memory_limit = 512M
upload_max_filesize = 1G
post_max_size = 1G
max_execution_time = 360
output_buffering = Off
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16

The occ command line tool runs under PHP CLI, which disables APCu by default. Enable it for the CLI too, otherwise occ fails once APCu is configured as the cache in Step 7:

echo 'apc.enable_cli=1' | sudo tee /etc/php/8.3/cli/conf.d/90-nextcloud-apcu.ini

PHP-FPM also clears environment variables by default, and Nextcloud warns that it cannot read PATH. Open the pool configuration:

sudo nano /etc/php/8.3/fpm/pool.d/www.conf

Find the commented env[PATH] line and remove the leading ; so it reads:

env[PATH] = /usr/local/bin:/usr/bin:/bin

Restart PHP-FPM and confirm the memory limit:

sudo systemctl restart php8.3-fpm
sudo php-fpm8.3 -i | grep ^memory_limit
memory_limit => 512M => 512M

Step 3 - Creating the database

Install MariaDB:

sudo apt install mariadb-server

Open the MariaDB shell as root:

sudo mariadb

Create the database and user. Replace your_db_password with a long random password:

CREATE DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER 'nextcloud'@'localhost' IDENTIFIED BY 'your_db_password';
GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Test the new account:

mariadb -u nextcloud -p -e "SELECT CURRENT_USER();"
+---------------------+
| CURRENT_USER()      |
+---------------------+
| nextcloud@localhost |
+---------------------+

Step 4 - Downloading Nextcloud

Download the latest release and its checksum from the official server:

cd /tmp
wget https://download.nextcloud.com/server/releases/latest.tar.bz2
wget https://download.nextcloud.com/server/releases/latest.tar.bz2.sha256

Verify the archive before extracting it:

sha256sum -c latest.tar.bz2.sha256
latest.tar.bz2: OK

Extract it to /var/www, which creates /var/www/nextcloud, and create the data directory outside the web root so user files can never be served directly by Nginx:

sudo tar -xjf latest.tar.bz2 -C /var/www
sudo mkdir -p /srv/nextcloud-data
sudo chown -R www-data:www-data /var/www/nextcloud /srv/nextcloud-data
sudo chmod 750 /srv/nextcloud-data

Step 5 - Installing Nextcloud with occ

occ is Nextcloud's command line administration tool. It must always run as the web server user, www-data. Run the installation, replacing the database password, and choose an admin user name and a strong admin password:

cd /var/www/nextcloud
sudo -u www-data php occ maintenance:install \
  --database mysql \
  --database-name nextcloud \
  --database-user nextcloud \
  --database-pass 'your_db_password' \
  --admin-user 'your_admin' \
  --admin-pass 'your_admin_password' \
  --data-dir /srv/nextcloud-data
Nextcloud was successfully installed

By default Nextcloud only accepts requests for localhost. Add your domain as a trusted domain and set the URL used when occ and background jobs generate links:

sudo -u www-data php occ config:system:set trusted_domains 1 --value=cloud.your_domain
sudo -u www-data php occ config:system:set overwrite.cli.url --value=https://cloud.your_domain

Check the installation status:

sudo -u www-data php occ status
  - installed: true
  - version: 32.0.0.13
  - versionstring: 32.0.0
  - edition: 
  - maintenance: false
  - needsDbUpgrade: false
  - productname: Nextcloud
  - extendedSupport: false

Your version numbers will match the release you downloaded.

Step 6 - Getting a certificate and configuring Nginx

Request the certificate first, so the Nextcloud server block can point to it. The Certbot Nginx plugin answers the validation challenge through the default site that is still active:

sudo apt install certbot python3-certbot-nginx
sudo certbot certonly --nginx -d cloud.your_domain
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/cloud.your_domain/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/cloud.your_domain/privkey.pem

Now create the Nextcloud site. This configuration is based on the one in the Nextcloud administration manual, adapted to Nginx 1.24 and PHP-FPM 8.3 on Ubuntu 24.04:

sudo nano /etc/nginx/sites-available/nextcloud.conf
upstream php-handler {
    server unix:/run/php/php8.3-fpm.sock;
}

# Mark versioned static assets as immutable
map $arg_v $asset_immutable {
    "" "";
    default ", immutable";
}

server {
    listen 80;
    listen [::]:80;
    server_name cloud.your_domain;
    server_tokens off;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name cloud.your_domain;

    root /var/www/nextcloud;

    ssl_certificate     /etc/letsencrypt/live/cloud.your_domain/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cloud.your_domain/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    server_tokens off;

    client_max_body_size 1G;
    client_body_timeout 300s;
    fastcgi_buffers 64 4K;
    client_body_buffer_size 512k;

    add_header Strict-Transport-Security "max-age=15768000" 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;
    fastcgi_hide_header X-Powered-By;

    include mime.types;
    types {
        text/javascript mjs;
    }

    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;
    }

    # Hide internal directories and files
    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 HTTPS on;
        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;
        fastcgi_read_timeout 360s;
    }

    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";
        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;
        access_log off;
    }

    location ~ \.(otf|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;
    }
}

The security headers are repeated in the static assets block because Nginx does not inherit add_header directives into a location that defines its own. client_max_body_size matches the PHP upload limit from Step 2.

Enable the site, disable the default one and test the configuration:

sudo ln -s /etc/nginx/sites-available/nextcloud.conf /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Reload Nginx and check that Nextcloud answers:

sudo systemctl reload nginx
curl -s https://cloud.your_domain/status.php
{"installed":true,"maintenance":false,"needsDbUpgrade":false,"version":"32.0.0.13","versionstring":"32.0.0","edition":"","productname":"Nextcloud","extendedSupport":false}

Certbot renews the certificate automatically with its systemd timer and reloads Nginx through the plugin. Test it with sudo certbot renew --dry-run.

Step 7 - Configuring caching and file locking

Nextcloud uses APCu as a fast local cache and Redis for transactional file locking, which prevents corruption when several clients sync the same files. Configure both with occ:

cd /var/www/nextcloud
sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu'
sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis'
sudo -u www-data php occ config:system:set redis host --value=localhost
sudo -u www-data php occ config:system:set redis port --value=6379 --type=integer

Verify that the settings were stored:

sudo -u www-data php occ config:system:get memcache.locking
\OC\Memcache\Redis

Step 8 - Running background jobs with cron

Nextcloud runs maintenance tasks (cleanup, previews, notifications) as background jobs. The default AJAX mode only runs them when someone uses the web interface. Switch to system cron:

sudo -u www-data php occ background:cron

Open the crontab of the www-data user:

sudo crontab -u www-data -e

Add this line to run the job every five minutes:

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

Also set a maintenance window, so heavy jobs run at night (the value is the hour in UTC), and the default phone region used to validate phone numbers in user profiles:

sudo -u www-data php occ config:system:set maintenance_window_start --type=integer --value=1
sudo -u www-data php occ config:system:set default_phone_region --value=ES

Replace ES with your ISO 3166-1 country code. After a few minutes, Administration settings > Basic settings shows that the last background job ran seconds ago.

Step 9 - Checking the setup warnings

Log in at https://cloud.your_domain with the admin account from Step 5 and open Administration settings > Overview. Nextcloud runs a series of security and setup checks there. Two common database items can be fixed immediately:

cd /var/www/nextcloud
sudo -u www-data php occ db:add-missing-indices
sudo -u www-data php occ maintenance:repair --include-expensive

Reload the overview page. The remaining warnings, if any, include a link to the relevant section of the documentation. Configure an email server under Administration settings > Basic settings so users receive share notifications and password resets.

Troubleshooting

"Access through untrusted domain". The host name in the browser is not in trusted_domains. List the current values with sudo -u www-data php occ config:system:get trusted_domains and add the missing one with a new index.

occ prints "Memcache \OC\Memcache\APCu not available". APCu is disabled for the CLI. Check that /etc/php/8.3/cli/conf.d/90-nextcloud-apcu.ini contains apc.enable_cli=1.

Large uploads fail with 413 or time out. Raise client_max_body_size in Nginx and upload_max_filesize and post_max_size in the PHP override together, then reload both services. The desktop and mobile clients upload in chunks and are not affected by these limits.

502 Bad Gateway. PHP-FPM is not running or the socket path is wrong. Check sudo systemctl status php8.3-fpm and that /run/php/php8.3-fpm.sock exists.

Conclusion

Nextcloud is now running on Ubuntu 24.04 with Nginx, PHP-FPM 8.3, MariaDB and Redis, with HTTPS, file locking, local caching and system cron. As next steps, install the desktop and mobile clients to sync files, enable apps such as Calendar and Contacts from the Apps page, and set up regular backups of /var/www/nextcloud/config, /srv/nextcloud-data and the database (with sudo -u www-data php occ maintenance:mode --on while the backup runs).