MHOSTMHOST

Nextcloud: your own cloud

Nextcloud is a web application for storing, syncing, and sharing files — a full replacement for Dropbox or Google Drive that runs on your own VPS, under your own control. By the end of this article you'll have a working private cloud on your own domain — with a database, HTTPS, and basic hardening — on top of an already configured web stack.

What you'll need

  • A working LEMP or LAMP web stack — nginx or Apache, MariaDB, and PHP. If you don't have one yet, start with the "The LEMP and LAMP web stacks" article (it also covers installing one via a ready-made VMmanager 6 recipe — see "Ready-made VMmanager 6 recipes").
  • A domain whose A record points at this VPS's public IP — you'll need it for trusted_domains and HTTPS. You can start with a bare IP, but a cloud you actually rely on should run on a domain from the start.
  • SSH access as root or a user with sudo privileges.
  • One of these systems: Ubuntu 22.04/24.04, Debian 11/12, AlmaLinux/Rocky Linux 8/9.
Important: open ports 80 and 443 in the firewall — without them neither Nextcloud itself nor certificate issuance will work. On Ubuntu/Debian with ufw, see the "UFW firewall" article; on AlmaLinux/Rocky with firewalld: sudo firewall-cmd --permanent --add-service=http --add-service=https && sudo firewall-cmd --reload.
Important: current Nextcloud requires PHP 8.2 or newer — more than what some of the systems above install by default. We'll deal with that in step 1.

Step 1. Prepare PHP: version and extensions

Check the installed version:

bash
php -v

Current Nextcloud (branch 34 as of this writing) requires PHP 8.2 or newer, with 8.3 or 8.4 recommended (8.2 is supported but marked as deprecated — Nextcloud will nag you to upgrade on every admin login). After setting up LEMP/LAMP from the corresponding article, the PHP version you end up with depends on the distro:

  • Ubuntu 24.04 (PHP 8.3) and Debian 12 (PHP 8.2) — fine as they are.
  • Ubuntu 22.04 (PHP 8.1) and Debian 11 (PHP 7.4) — the stock apt version is too old for current Nextcloud.
  • AlmaLinux/Rocky Linux 8/9 — the default version is old too, but AppStream has newer streams available (see below).
Important: if you're on Ubuntu 22.04 or Debian 11, the most reliable path is to reinstall the server on Ubuntu 24.04 or Debian 12 — in VMmanager 6 this is a single pass with no compatibility surprises; see the "Reinstalling the OS and choosing a template" article. Chasing a newer PHP on an old LTS system via third-party repositories is technically possible, but that's a separate and less reliable topic not covered in this article.

If you're on AlmaLinux/Rocky Linux

Check which PHP streams are available:

bash
sudo dnf module list php

Reset the current stream and enable the right one — 8.2 for the 8-series distro release, 8.3 (or newer, if available) for the 9-series:

bash
sudo dnf module reset php
sudo dnf module enable php:8.3
sudo dnf distro-sync 'php*'
Verify: the exact streams available (8.2, 8.3, and so on) depend on your distro's version and how recently its mirrors synced. Go by the actual output of dnf module list php on your system, not just the example above; on the 8-series release use php:8.2 instead of php:8.3.

Install the PHP extensions Nextcloud needs

The web stack already gave you base PHP and the database driver (php-mysql/php-mysqlnd) — Nextcloud needs a few more modules.

Required — Ubuntu/Debian:

bash
sudo apt install php-gd php-curl php-mbstring php-intl php-gmp php-xml php-zip

Required — AlmaLinux/Rocky:

bash
sudo dnf install php-gd php-curl php-mbstring php-intl php-xml php-zip php-json
Tip: if dnf can't find one of these packages on your distro version, drop it from the command and install the rest separately — on some builds, for example, php-json is already part of the base php package.

Also recommended — caching and file previews:

Ubuntu/Debian:

bash
sudo apt install php-apcu php-redis php-imagick

AlmaLinux/Rocky:

bash
sudo dnf install php-pecl-apcu php-redis php-imagick

APCu and Redis speed up Nextcloud and take some load off the database; Imagick can generate previews for PDFs and image formats the built-in GD doesn't understand.

Restart PHP so the new modules load — the command depends on what you set up in the LEMP/LAMP article:

Ubuntu/Debian, LEMP (substitute your PHP version):

bash
sudo systemctl restart php8.3-fpm

Ubuntu/Debian, LAMP:

bash
sudo systemctl restart apache2

AlmaLinux/Rocky, LEMP:

bash
sudo systemctl restart php-fpm

AlmaLinux/Rocky, LAMP:

bash
sudo systemctl restart httpd

Step 2. Create the database and a database user

MariaDB is already installed and configured by the steps in the LEMP/LAMP article. Connect as root (via sudo, passwordless, same as in that article) and create a database plus a dedicated user for Nextcloud:

bash
sudo mysql
sql
CREATE DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER 'nextcloud'@'localhost' IDENTIFIED BY 'change_me';
GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'localhost';
FLUSH PRIVILEGES;
EXIT;
  • utf8mb4 is the character set with full support for 4-byte characters (including emoji) — exactly what Nextcloud requires.
  • Replace the password in IDENTIFIED BY with your own — you'll need it in step 5.
Tip: Nextcloud also supports PostgreSQL — if you already have it set up instead of MariaDB, use --database pgsql further down in this article instead of mysql, along with your own connection details; PostgreSQL itself isn't covered separately here.

Step 3. Configure the web server for Nextcloud

The rest of this article uses the term HTTP user — the system user your web server and PHP run as, and the one that must own the Nextcloud files:

  • Ubuntu/Debian (both LEMP and LAMP) — www-data;
  • AlmaLinux/Rocky, LEMP (nginx + PHP-FPM) — nginx, if you changed the PHP-FPM pool user as instructed in the LEMP article (that's a required step there);
  • AlmaLinux/Rocky, LAMP (Apache + mod_php) — apache.

Set up a dedicated virtual host for Nextcloud — ideally on its own subdomain (for example, cloud.example.com), as Nextcloud's own documentation recommends, rather than on your main site's domain.

If you're using nginx (LEMP)

Ubuntu/Debian:

bash
sudo nano /etc/nginx/sites-available/nextcloud.conf

AlmaLinux/Rocky:

bash
sudo nano /etc/nginx/conf.d/nextcloud.conf

The file's contents are the same on every system (replace cloud.example.com with your own domain):

nginx
upstream php-handler {
    # Ubuntu/Debian — substitute your PHP version:
    server unix:/run/php/php8.3-fpm.sock;
    # AlmaLinux/Rocky — use this instead of the line above:
    # server unix:/run/php-fpm/www.sock;
}

server {
    listen 80;
    listen [::]:80;
    server_name cloud.example.com;

    root /var/www/nextcloud;

    client_max_body_size 512M;
    client_body_timeout 300s;

    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;

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

    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 ~ ^/(?:composer\.(?:json|lock)|package(?:-lock)?\.json)$      { return 404; }

    location ~ \.php(?:$|/) {
        rewrite ^/(?!index|remote|public|cron|status|ocs\/v[12]|ocs-provider\/.+|core\/ajax\/update|updater\/.+|.+\/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_pass php-handler;

        fastcgi_param SCRIPT_FILENAME         $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO               $path_info;
        fastcgi_param HTTPS                   $https if_not_empty;
        fastcgi_param modHeadersAvailable      true;
        fastcgi_param front_controller_active  true;
        fastcgi_max_temp_file_size 0;
    }

    location ~ \.(?:css|js|mjs|svg|gif|ico|jpg|jpeg|png|webp|wasm|tflite|map|ogg|flac|mp4|webm)$ {
        try_files $uri /index.php$request_uri;
        expires 7d;
        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;
    }
}

What the main blocks do: location ~ \.php passes PHP files to PHP-FPM and supports "pretty" links without index.php in the URL; the return 404 blocks close off direct access to internal directories (config, data, 3rdparty, and others) that hold passwords and user data; /.well-known is needed by CalDAV/CardDAV clients and by the Let's Encrypt certificate you'll add later.

On Ubuntu/Debian, enable the site with a symlink:

bash
sudo ln -s /etc/nginx/sites-available/nextcloud.conf /etc/nginx/sites-enabled/

Test the configuration and reload nginx:

bash
sudo nginx -t && sudo systemctl reload nginx

If you're using Apache (LAMP)

Nextcloud ships its own .htaccess file with all the rules it needs — the virtual host just has to point at the directory and allow .htaccess.

Ubuntu/Debian:

bash
sudo nano /etc/apache2/sites-available/nextcloud.conf

AlmaLinux/Rocky:

bash
sudo nano /etc/httpd/conf.d/nextcloud.conf

Contents (replace cloud.example.com with your own domain):

apache
<VirtualHost *:80>
    ServerName cloud.example.com
    DocumentRoot /var/www/nextcloud/

    <Directory /var/www/nextcloud/>
        Require all granted
        AllowOverride All
        Options FollowSymLinks MultiViews

        <IfModule mod_dav.c>
            Dav off
        </IfModule>
    </Directory>
</VirtualHost>

Ubuntu/Debian — enable the site and the required mod_rewrite module (without it, Nextcloud's .htaccess won't work), plus the recommended modules for headers and MIME types:

bash
sudo a2ensite nextcloud.conf
sudo a2enmod rewrite headers env dir mime
sudo systemctl reload apache2

AlmaLinux/Rocky — the file in conf.d/ is picked up automatically, and the rewrite/headers/env/dir/mime modules are usually already enabled by default in httpd; verify and reload:

bash
sudo httpd -M | grep -E 'rewrite|headers_module|env_module|dir_module|mime_module'
sudo systemctl reload httpd
Tip: if any module is missing from the output, uncomment the matching LoadModule line in /etc/httpd/conf.modules.d/00-base.conf and restart httpd.

Step 4. Download and unpack Nextcloud

Download the archive and check its checksum:

bash
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

The output should include latest.tar.bz2: OK. Unpack the archive into the web root and hand it over to the HTTP user (replace www-data with your HTTP user from step 3 if you're on AlmaLinux/Rocky):

bash
sudo mkdir -p /var/www
sudo tar -xjf latest.tar.bz2 -C /var/www/
sudo chown -R www-data:www-data /var/www/nextcloud/
Tip: instead of downloading it manually, you can use Nextcloud's web installer — a small script that downloads the current release itself, unpacks it with the right permissions, and hands off to the same installation wizard described in step 5. Download it straight into the web root:
bash
cd /var/www
sudo wget https://download.nextcloud.com/server/installer/setup-nextcloud.php
sudo chown www-data:www-data setup-nextcloud.php

Open http://cloud.example.com/setup-nextcloud.php in your browser and follow the instructions; once setup finishes, delete the setup-nextcloud.php file itself — it's no longer needed.

AlmaLinux/Rocky only: if SELinux is running in Enforcing mode (check with getenforce), Nextcloud's files need the right security contexts, or the web server won't be able to write to them. If getenforce reports Disabled or Permissive, skip these commands.
bash
sudo dnf install -y policycoreutils-python-utils
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/nextcloud/config(/.*)?'
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/nextcloud/apps(/.*)?'
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/nextcloud/.htaccess'
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/nextcloud/.user.ini'
sudo restorecon -Rv /var/www/nextcloud/

Step 5. Finish the install: browser wizard or occ

Create the data directory up front — outside the web root, which is both safer and Nextcloud's own recommendation, and easiest to do right now on a fresh install (replace www-data if you're on AlmaLinux/Rocky):

bash
sudo mkdir -p /var/nextcloud-data
sudo chown www-data:www-data /var/nextcloud-data
AlmaLinux/Rocky with SELinux in Enforcing mode only: the data directory lives outside /var/www/nextcloud, so it needs its own security context — the commands from step 4 don't cover it:
bash
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/nextcloud-data(/.*)?'
sudo restorecon -Rv /var/nextcloud-data/

Pick either of the two options below — both lead to the same result.

Option A. Browser installation wizard

Open http://cloud.example.com/ (or the server's IP if the domain isn't pointed there yet) and fill in the form:

  1. Administrator account name and password.
  2. Expand the "Storage & database" section.
  3. In the "Data folder" field, enter /var/nextcloud-data.
  4. Choose MySQL/MariaDB (not SQLite — that's the default option, fine for testing only) and enter the user, password, and database name from step 2, with host localhost.
  5. Click "Finish setup" and wait for it to complete.

Option B. Command line (occ)

bash
cd /var/www/nextcloud
sudo -E -u www-data php occ maintenance:install \
  --database "mysql" \
  --database-name "nextcloud" \
  --database-user "nextcloud" \
  --database-pass "change_me" \
  --admin-user "admin" \
  --admin-pass "change_me_admin" \
  --data-dir "/var/nextcloud-data"

Replace www-data with your HTTP user from step 3 if you're on AlmaLinux/Rocky, and both passwords with your own (the database password must match what you set in step 2). On success, the command prints Nextcloud was successfully installed.

Important: you must run this command from the /var/www/nextcloud directory (the cd on the line above) — otherwise the install fails with a PHP error.
Done: open the server's address in a browser and log in with the admin account — Nextcloud is installed and ready to use.

Step 6. Check trusted_domains

Nextcloud only responds to requests for domains listed in trusted_domains in config/config.php — this protects against Host header poisoning. After installing via occ, that list usually contains only localhost:

bash
sudo -u www-data php /var/www/nextcloud/occ config:system:get trusted_domains

Add your domain at the next free index (if 0 is already taken, use 1 — don't skip numbers):

bash
sudo -u www-data php /var/www/nextcloud/occ config:system:set trusted_domains 1 --value=cloud.example.com

The same result, as a direct edit of config/config.php:

php
'trusted_domains' =>
array (
  0 => 'localhost',
  1 => 'cloud.example.com',
),
Tip: if you installed via the browser wizard opened at your domain's address, it most likely already added that domain to the list. It's still worth checking with the command above.

Step 7. Enable HTTPS

Certbot will get a Let's Encrypt certificate, add it to the web server's config, and turn on the HTTP-to-HTTPS redirect for you — for the details and the prerequisite steps (installing certbot, opening ports 80/443), see the "Free SSL with Let's Encrypt (certbot)" article. The short version for Nextcloud:

nginx:

bash
sudo certbot --nginx -d cloud.example.com

Apache:

bash
sudo certbot --apache -d cloud.example.com
Done: https://cloud.example.com opens in a browser with no certificate warnings.
Tip: if Nextcloud still generates some links with http:// after enabling HTTPS (for example, in emails or "Share" links), force the protocol in config/config.php: 'overwriteprotocol' => 'https',.

Step 8. Set up basic hardening

A handful of simple but genuinely useful steps after installation.

Debug mode is off. That's already the case on a fresh install by default, but it's worth confirming:

bash
grep "'debug'" /var/www/nextcloud/config/config.php

The line should either be missing entirely or read 'debug' => false,. Debug mode left on in production means detailed PHP errors and stack traces visible to every visitor.

HSTS header stops the browser from ever contacting the site over plain HTTP, even if someone types a link without https://.

nginx — add this line inside the server { listen 443 ssl; ... } block that certbot created in the previous step:

nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Apache — the same idea, inside <VirtualHost *:443>:

apache
Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains"

Restart the web server after editing.

Important: don't add the preload option to HSTS until you've read about its consequences at hstspreload.org — once a domain is on browsers' preload list, getting it removed again is extremely difficult.

Core integrity check and admin warnings. Verify the core files against their reference signatures (replace www-data if you're on AlmaLinux/Rocky):

bash
sudo -u www-data php /var/www/nextcloud/occ integrity:check-core

Empty output means everything checks out. Next, check the admin "Overview" page (your profile menu → "Administration settings" → "Overview"): it collects Nextcloud's own warnings — from missing database indices to insecure settings and available updates. Check it right after installing and periodically afterward — updates often close real vulnerabilities.

Background jobs via cron instead of AJAX. By default, Nextcloud runs its maintenance tasks whenever a user loads a page in the browser (AJAX mode) — it works, but it's not the most reliable option. Add a job to the HTTP user's crontab:

bash
sudo crontab -u www-data -e
*/5  *  *  *  * php -f /var/www/nextcloud/cron.php

After the first successful scheduled run, Nextcloud switches itself to Cron mode — there's nothing extra to enable.

Brute-force protection. By default, Nextcloud logs failed login attempts to nextcloud.log inside the data directory (the default log level, 2, is already enough for this). Fail2ban can automatically ban such IPs — see the "Fail2ban: protection against brute-force" article for the base install and general approach. Nextcloud itself needs its own filter:

bash
sudo nano /etc/fail2ban/filter.d/nextcloud.conf
ini
[Definition]
_groupsre = (?:(?:,?\s*"\w+":(?:"[^"]+"|\w+))*)
failregex = ^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Login failed:
            ^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Two-factor challenge failed:
            ^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Trusted domain error.
datepattern = ,?\s*"time"\s*:\s*"%%Y-%%m-%%d[T ]%%H:%%M:%%S(%%z)?"
bash
sudo nano /etc/fail2ban/jail.d/nextcloud.local
ini
[nextcloud]
backend = auto
enabled = true
port = 80,443
protocol = tcp
filter = nextcloud
maxretry = 3
bantime = 86400
findtime = 43200
logpath = /var/nextcloud-data/nextcloud.log
bash
sudo systemctl restart fail2ban

What's next

Nextcloud is installed, running over HTTPS on its own domain, and already has basic hardening in place — you can start adding users, installing apps from the built-in catalog, and connecting desktop and mobile sync clients. The data is now yours alone, on your own VPS — don't forget to set up backups for it, for example with the "Backups with rsync" article, and don't put off the fail2ban setup from step 8 if you haven't configured it yet.