Self-host Nextcloud on a VPS
Replace Dropbox, Google Drive, and Office 365 with one self-hosted Nextcloud instance on a small VPS. Full setup with PostgreSQL, Redis, and proper backups.

Nextcloud is the closest thing to a self-hosted Google Workspace that actually works. File sync, calendar, contacts, photos, and a hundred apps. Setup has gotten genuinely good in the last few years. Here's the install that holds up under daily use.
One prerequisite before anything else: a domain, with an A record (and an AAAA record if your VPS has IPv6) pointing at the server. Without it Certbot can't issue a certificate and the browser setup step can't reach your instance. This guide uses cloud.example.com throughout; if you need a refresher on records and propagation, see the DNS records guide.
Sizing
- 1-3 users, light file sync: 2 vCPU, 4 GB RAM, 80 GB disk
- Family / small team, photos: 4 vCPU, 8 GB RAM, 200+ GB disk
- Remember: photos and videos eat disk. Plan storage by use, not by users.
Stack
- Ubuntu 24.04 LTS
- Nginx
- PHP 8.3 FPM
- PostgreSQL 16
- Redis (for caching and file locking)
Skip Apache. Nginx is faster, lighter, and easier to debug.
Install dependencies
sudo apt update
sudo apt install -y nginx postgresql redis-server unzip \
php8.3-fpm php8.3-cli php8.3-pgsql php8.3-curl php8.3-gd php8.3-intl \
php8.3-mbstring php8.3-xml php8.3-zip php8.3-bcmath php8.3-gmp \
php8.3-imagick php8.3-redis ffmpeg
Don't drop php8.3-cli from that list. On Ubuntu, php8.3-fpm does not pull it in, and without the command-line binary the occ commands and the cron job later in this guide have nothing to run.
Database
sudo -u postgres psql <<'SQL'
CREATE USER nextcloud WITH PASSWORD 'use-a-real-password';
CREATE DATABASE nextcloud TEMPLATE template0 ENCODING 'UTF8' OWNER nextcloud;
SQL
Download Nextcloud
cd /tmp
curl -O https://download.nextcloud.com/server/releases/latest.zip
sudo unzip latest.zip -d /var/www/
sudo chown -R www-data:www-data /var/www/nextcloud
PHP-FPM tuning
Edit /etc/php/8.3/fpm/pool.d/www.conf and bump:
pm = dynamic
pm.max_children = 10
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
Scale pm.max_children to the box, not to a number from a forum post. PHP-FPM's worst-case memory use is roughly max_children times the typical worker size, and a busy Nextcloud worker settles around 150 to 250 MB. On the 4 GB tier from the sizing table, 8 to 12 children leaves headroom for PostgreSQL and Redis; save 20 to 30 for 8 GB and up, or the kernel will start OOM-killing your database under load.
Edit /etc/php/8.3/fpm/php.ini:
memory_limit = 512M
upload_max_filesize = 16G
post_max_size = 16G
max_execution_time = 3600
output_buffering = 0
sudo systemctl restart php8.3-fpm
TLS certificate
Get the certificate before you write the Nextcloud server block. The upstream Nginx config is HTTPS-only and points ssl_certificate at files under /etc/letsencrypt/live/, so if you enable it before those files exist, nginx -t fails and Nginx refuses to reload. At this point Nginx is still answering on port 80 with Ubuntu's stock default site, which is all Certbot's nginx plugin needs to pass validation:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot certonly --nginx -d cloud.example.com
The certificate and key land in /etc/letsencrypt/live/cloud.example.com/. Certbot sets up automatic renewal via a systemd timer, and the nginx plugin keeps renewals working after the Nextcloud site goes live. The Let's Encrypt guide covers the details and troubleshooting.
Nginx server block
Use the official Nextcloud Nginx config from their docs (it's long and they update it, so grab the current version from that page). Save it to /etc/nginx/sites-available/nextcloud, but don't enable it verbatim: the upstream file ships with placeholder values. Make five edits first:
- Set
server_nameto your domain,cloud.example.comin this guide. It appears in both the port-80 redirect block and the HTTPS block. - Set
rootto/var/www/nextcloud. - Point
fastcgi_passatunix:/run/php/php8.3-fpm.sock, which is where Ubuntu's default PHP-FPM pool listens. - Point
ssl_certificateandssl_certificate_keyat the files Certbot just issued:/etc/letsencrypt/live/cloud.example.com/fullchain.pemand/etc/letsencrypt/live/cloud.example.com/privkey.pem. - Raise
client_max_body_sizefrom the shipped512Mto16Gto match thepost_max_sizeyou set in php.ini. Leave it at the default and Nginx rejects any upload over 512 MB with an HTTP 413 before PHP ever sees it.
Then enable and reload:
sudo ln -s /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Finish setup in the browser
Open https://cloud.example.com. Pick PostgreSQL, enter the credentials, click through. The web installer does the schema work.
Post-install must-dos
In Settings -> Administration -> Overview, fix every warning. Most are one config change in /var/www/nextcloud/config/config.php.
Before you touch config.php, wire up Redis. Ubuntu's redis-server package only listens on TCP 127.0.0.1:6379 out of the box; the unix socket the config below points at doesn't exist until you enable it. Edit /etc/redis/redis.conf and uncomment or set these two lines:
unixsocket /run/redis/redis-server.sock
unixsocketperm 770
The socket is owned by the redis group, so add www-data to it, then restart Redis to create the socket and PHP-FPM so its workers pick up the new group membership:
sudo usermod -aG redis www-data
sudo systemctl restart redis-server php8.3-fpm
Confirm Nextcloud's user can actually reach it:
sudo -u www-data redis-cli -s /run/redis/redis-server.sock ping
# PONG
Now the config.php changes:
'memcache.local' => '\OC\Memcache\Redis',
'memcache.locking' => '\OC\Memcache\Redis',
'redis' => [
'host' => '/run/redis/redis-server.sock',
'port' => 0,
],
'default_phone_region' => 'GB',
'maintenance_window_start' => 1,
Run the cron job as www-data:
sudo -u www-data crontab -e
# */5 * * * * php8.3 -f /var/www/nextcloud/cron.php
In Settings, switch from "AJAX" to "Cron" for background jobs.
Backups
Three pieces: the database, the data directory, and the config directory. That last one is the piece people forget. config/config.php holds instanceid, passwordsalt, and secret, and without those a restored instance can't decrypt tokens or encrypted files; a backup that skips it is not a backup you can restore from.
Two things to know before you run the script below. First, it puts Nextcloud into maintenance mode for the duration of the dump, so sync clients and the web UI go offline for a few minutes each night. Skipping that step is tempting, but dumping the database and tarring the data directory while the instance is live can produce a snapshot where the two disagree, and you only find out when a restore fails. The trap line turns maintenance mode back off even if the dump errors out. Second, the find line at the end permanently deletes every .gz file in $DEST older than 14 days. Keep that directory dedicated to these backups and store nothing else in it.
Save this as /usr/local/bin/nc-backup.sh:
#!/bin/bash
set -euo pipefail
DEST=/srv/nc-backups
DATE=$(date +%F)
OCC="sudo -u www-data php8.3 /var/www/nextcloud/occ"
mkdir -p "$DEST"
$OCC maintenance:mode --on
trap '$OCC maintenance:mode --off' EXIT
sudo -u postgres pg_dump nextcloud | gzip > "$DEST/db-$DATE.sql.gz"
tar czf "$DEST/data-$DATE.tar.gz" -C /var/www/nextcloud data config
find "$DEST" -maxdepth 1 -type f -name '*.gz' -mtime +14 -delete
Make it executable and schedule it nightly from root's crontab:
sudo chmod +x /usr/local/bin/nc-backup.sh
sudo crontab -e
# 0 3 * * * /usr/local/bin/nc-backup.sh
A backup that lives on the same VPS as the data disappears with the VPS. Ship $DEST off the box nightly; the restic guide covers encrypted off-site backups end to end.
Apps worth installing
- Memories for photo browsing (Google Photos replacement)
- Talk for video calls
- Notes with Markdown support
- Mail if you want a webmail client tied to your address
- Collabora Online for browser-based document editing (heavyweight; needs its own VPS for >5 users)
That's a self-hosted productivity stack that costs less per year than two months of Office 365 Family.
Continue reading