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_domainsand 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
rootor a user withsudoprivileges. - One of these systems: Ubuntu 22.04/24.04, Debian 11/12, AlmaLinux/Rocky Linux 8/9.
Step 1. Prepare PHP: version and extensions
Check the installed version:
php -vCurrent 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).
If you're on AlmaLinux/Rocky Linux
Check which PHP streams are available:
sudo dnf module list phpReset 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:
sudo dnf module reset php
sudo dnf module enable php:8.3
sudo dnf distro-sync 'php*'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:
sudo apt install php-gd php-curl php-mbstring php-intl php-gmp php-xml php-zipRequired — AlmaLinux/Rocky:
sudo dnf install php-gd php-curl php-mbstring php-intl php-xml php-zip php-jsonAlso recommended — caching and file previews:
Ubuntu/Debian:
sudo apt install php-apcu php-redis php-imagickAlmaLinux/Rocky:
sudo dnf install php-pecl-apcu php-redis php-imagickAPCu 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):
sudo systemctl restart php8.3-fpmUbuntu/Debian, LAMP:
sudo systemctl restart apache2AlmaLinux/Rocky, LEMP:
sudo systemctl restart php-fpmAlmaLinux/Rocky, LAMP:
sudo systemctl restart httpdStep 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:
sudo mysqlCREATE 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;utf8mb4is the character set with full support for 4-byte characters (including emoji) — exactly what Nextcloud requires.- Replace the password in
IDENTIFIED BYwith your own — you'll need it in step 5.
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:
sudo nano /etc/nginx/sites-available/nextcloud.confAlmaLinux/Rocky:
sudo nano /etc/nginx/conf.d/nextcloud.confThe file's contents are the same on every system (replace cloud.example.com with your own domain):
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:
sudo ln -s /etc/nginx/sites-available/nextcloud.conf /etc/nginx/sites-enabled/Test the configuration and reload nginx:
sudo nginx -t && sudo systemctl reload nginxIf 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:
sudo nano /etc/apache2/sites-available/nextcloud.confAlmaLinux/Rocky:
sudo nano /etc/httpd/conf.d/nextcloud.confContents (replace cloud.example.com with your own domain):
<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:
sudo a2ensite nextcloud.conf
sudo a2enmod rewrite headers env dir mime
sudo systemctl reload apache2AlmaLinux/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:
sudo httpd -M | grep -E 'rewrite|headers_module|env_module|dir_module|mime_module'
sudo systemctl reload httpdStep 4. Download and unpack Nextcloud
Download the archive and check its checksum:
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.sha256The 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):
sudo mkdir -p /var/www
sudo tar -xjf latest.tar.bz2 -C /var/www/
sudo chown -R www-data:www-data /var/www/nextcloud/cd /var/www
sudo wget https://download.nextcloud.com/server/installer/setup-nextcloud.php
sudo chown www-data:www-data setup-nextcloud.phpOpen 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.
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):
sudo mkdir -p /var/nextcloud-data
sudo chown www-data:www-data /var/nextcloud-datasudo 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:
- Administrator account name and password.
- Expand the "Storage & database" section.
- In the "Data folder" field, enter
/var/nextcloud-data. - 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. - Click "Finish setup" and wait for it to complete.
Option B. Command line (occ)
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.
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:
sudo -u www-data php /var/www/nextcloud/occ config:system:get trusted_domainsAdd your domain at the next free index (if 0 is already taken, use 1 — don't skip numbers):
sudo -u www-data php /var/www/nextcloud/occ config:system:set trusted_domains 1 --value=cloud.example.comThe same result, as a direct edit of config/config.php:
'trusted_domains' =>
array (
0 => 'localhost',
1 => 'cloud.example.com',
),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:
sudo certbot --nginx -d cloud.example.comApache:
sudo certbot --apache -d cloud.example.comStep 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:
grep "'debug'" /var/www/nextcloud/config/config.phpThe 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:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;Apache — the same idea, inside <VirtualHost *:443>:
Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains"Restart the web server after editing.
Core integrity check and admin warnings. Verify the core files against their reference signatures (replace www-data if you're on AlmaLinux/Rocky):
sudo -u www-data php /var/www/nextcloud/occ integrity:check-coreEmpty 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:
sudo crontab -u www-data -e*/5 * * * * php -f /var/www/nextcloud/cron.phpAfter 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:
sudo nano /etc/fail2ban/filter.d/nextcloud.conf[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)?"sudo nano /etc/fail2ban/jail.d/nextcloud.local[nextcloud]
backend = auto
enabled = true
port = 80,443
protocol = tcp
filter = nextcloud
maxretry = 3
bantime = 86400
findtime = 43200
logpath = /var/nextcloud-data/nextcloud.logsudo systemctl restart fail2banWhat'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.