Self-host Nextcloud on a VPS with Docker and pay in Bitcoin or Monero

· 8 min read

Nextcloud 35 on Ubuntu 24.04: All-in-One behind Caddy or compose with Postgres and Redis, disk sizing by plan, cron, off-box backups, optional Collabora.

What Nextcloud on a rented server gets you

Nextcloud is the usual answer to "I want my files, calendar, contacts and photos off Google and Dropbox but still synced to every device". The server side is PHP plus a database; the clients are solid on every desktop and phone. Running it on a VPS instead of a home NAS gives you a public IP without port forwarding, bandwidth that does not compete with your household, and a machine that stays up when the power at home does not.

The tradeoff is that your files sit on someone else’s hardware. Two things reduce what that means in practice: pay for the server without an account or identity attached, and keep the data encrypted at rest where it matters (Nextcloud’s server-side encryption app, or client-side encryption with Cryptomator for the sensitive folders). The payment part is what this site is built for; the rest is standard Nextcloud administration covered below.

There are two sensible ways to deploy it in 2026. Nextcloud All-in-One (AIO) is the project’s own Docker orchestrator: one master container that installs and updates Nextcloud, Postgres, Redis, Apache, and optional Collabora, Talk, full-text search and a backup tool. docker-compose with the official nextcloud image gives you a smaller, fully hand-controlled stack. AIO is the right default for most people; the compose route is for those who want to see every moving part.

Sizing by disk, then by RAM

Disk is the decision. Nextcloud itself, the OS and the containers take 8 to 12 GB; everything else is your files plus previews (budget 10 to 20 percent of photo storage for thumbnails) plus database. RAM: Nextcloud with Redis and Postgres is comfortable at 4 GB; add 2 GB if you enable Collabora and another 1 to 2 GB for Talk or full-text search. Nextcloud 35 is the current major release (35.0.2 as of mid-October 2026), and it requires PHP 8.3 or newer, which the images already carry.

PlanSpecsFrom (EU)Roughly usable for filesGood for
BVCX232 vCPU / 4 GB / 40 GB$11/mo~25 GBCalendar, contacts, notes, documents; a few GB of photos
BVCX334 vCPU / 8 GB / 80 GB$16/mo~60 GBOne or two people with photo backup, Collabora optional
BVX21 / BVA213-4 vCPU / 4-8 GB / 80 GB$18 / $20/mo~60 GBSame as above; BVX21 also offered in the US and Singapore
BVX31 / BVA314-8 vCPU / 8-16 GB / 160 GB$32 / $40/mo~140 GBHousehold, phone photo auto-upload, Collabora and Talk
BVX418 vCPU / 16 GB / 240 GB$60/mo~220 GBSmall team or a large photo library

Prepare the server

Everything below assumes a fresh Ubuntu 24.04 image and a root shell (prefix sudo if you created a user). Patch first, then lock down SSH and the firewall before anything else is listening.

apt update && apt full-upgrade -y
apt install -y ufw unattended-upgrades curl gnupg

# SSH: keys only. Put your public key in /root/.ssh/authorized_keys first and
# confirm you can log in with it before disabling passwords.
cat > /etc/ssh/sshd_config.d/00-keys-only.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
EOF
sshd -t && systemctl restart ssh

# Firewall: deny everything inbound except SSH for now.
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw --force enable

Docker, Caddy and DNS

Both deployment routes need Docker and a TLS terminator. Caddy on the host is used here because it handles Let’s Encrypt without ceremony; nginx with certbot is fine if you already know it. Create A and AAAA records for cloud.example.com pointing at the server before starting, and open 80 and 443.

# Docker Engine + Compose plugin from Docker's own repository (not the Ubuntu snap).
apt install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  > /etc/apt/sources.list.d/docker.list
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker version && docker compose version
# Caddy from the project's apt repository. It obtains and renews Let's Encrypt
# certificates on its own, so there is no certbot to schedule.
apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install -y caddy

ufw allow 80/tcp
ufw allow 443/tcp
# /etc/caddy/Caddyfile
cloud.example.com {
    # AIO's Apache container (or the compose app container) listens here on localhost.
    reverse_proxy 127.0.0.1:11000
}
caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy

Route A: Nextcloud All-in-One behind Caddy

AIO runs a master container that talks to the Docker socket and creates the rest. Because Caddy owns ports 80 and 443, start the master container in reverse-proxy mode: APACHE_PORT tells it where to publish Nextcloud, APACHE_IP_BINDING restricts that to localhost. The AIO admin interface on port 8080 is published to localhost only as well and reached through an SSH tunnel, so it is never exposed to the internet.

docker run \
  --init \
  --sig-proxy=false \
  --name nextcloud-aio-mastercontainer \
  --restart always \
  --publish 127.0.0.1:8080:8080 \
  --env APACHE_PORT=11000 \
  --env APACHE_IP_BINDING=127.0.0.1 \
  --env APACHE_ADDITIONAL_NETWORK="" \
  --env SKIP_DOMAIN_VALIDATION=false \
  --volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \
  --volume /var/run/docker.sock:/var/run/docker.sock:ro \
  ghcr.io/nextcloud-releases/all-in-one:latest

# On your own machine, open a tunnel to the AIO interface:
ssh -L 8080:127.0.0.1:8080 [email protected]
# then browse to https://localhost:8080 (self-signed certificate; accept it)

The interface shows a one-time passphrase on first load; save it, it is the AIO login. Enter cloud.example.com as the domain. The domain check sends a request through your Caddy to a temporary container on port 11000, so Caddy must already be running with the config above. Then tick the optional containers you want (Collabora for in-browser office documents, Talk for calls, Imaginary for faster previews) and press Start containers. First start pulls several images and takes a few minutes; when it finishes the page shows the initial admin password for Nextcloud itself.

Route B: docker-compose with Postgres and Redis

If you prefer four containers you can read in one screen, this is the official example from the nextcloud/docker repository with the ports pinned to localhost and the image tags pinned to a major version. Nextcloud can only be upgraded one major version at a time, which is why :latest is a bad idea here.

# /opt/nextcloud/compose.yaml
services:
  db:
    image: postgres:17-alpine
    restart: always
    volumes:
      - db:/var/lib/postgresql/data
    env_file: db.env

  redis:
    image: redis:alpine
    restart: always

  app:
    image: nextcloud:35-apache
    restart: always
    ports:
      - "127.0.0.1:11000:80"
    volumes:
      - nextcloud:/var/www/html
      - ./data:/var/www/html/data
    environment:
      - POSTGRES_HOST=db
      - REDIS_HOST=redis
      - NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com
      - OVERWRITEPROTOCOL=https
      - OVERWRITEHOST=cloud.example.com
      - TRUSTED_PROXIES=127.0.0.1
      - PHP_MEMORY_LIMIT=1024M
      - PHP_UPLOAD_LIMIT=4G
    env_file: db.env
    depends_on: [db, redis]

  cron:
    image: nextcloud:35-apache
    restart: always
    volumes:
      - nextcloud:/var/www/html
      - ./data:/var/www/html/data
    entrypoint: /cron.sh
    depends_on: [db, redis]

volumes:
  db:
  nextcloud:
mkdir -p /opt/nextcloud/data && cd /opt/nextcloud
cat > db.env <<EOF
POSTGRES_DB=nextcloud
POSTGRES_USER=nextcloud
POSTGRES_PASSWORD=$(openssl rand -hex 24)
EOF
chmod 600 db.env
docker compose up -d
docker compose logs -f app    # until "Initializing finished"

Browse to https://cloud.example.com, create the admin account (the database fields are pre-filled from the environment, do not switch to SQLite) and then switch background jobs to Cron under Administration settings, Basic settings. The separate cron container runs cron.php every five minutes, which is what keeps previews, trash expiry and app updates moving. The ./data bind mount keeps your actual files on the host filesystem where rsync and restic can see them.

Collabora on this route is one more container (collabora/code) proxied on its own hostname, plus the richdocuments app pointed at it. It needs about 2 GB of RAM to itself, which is the reason to start at 8 GB if you want office editing.

Backups: built-in Borg versus rsync/restic off the box

AIO ships a BorgBackup integration: pick a local path in the AIO interface, set a passphrase, and it snapshots the database dump, the config and the data volumes while the containers are stopped. That gives you consistent, deduplicated, encrypted archives on the local disk. It does not get them off the server. Point the AIO backup target at a directory such as /mnt/backup and then sync that directory elsewhere with rsync or rclone on a cron; the Borg repository is already encrypted so the remote end only sees ciphertext.

On the compose route you do the same thing by hand: put Nextcloud into maintenance mode, dump Postgres, snapshot the config and data directories with restic to a remote repository, then leave maintenance mode. A nightly script looks like this.

#!/bin/sh
# /usr/local/bin/nc-backup  (compose route)
set -eu
cd /opt/nextcloud
export RESTIC_REPOSITORY=sftp:backup@backup-host:/srv/restic/nextcloud
export RESTIC_PASSWORD_FILE=/root/.restic-pass
docker compose exec -T -u www-data app php occ maintenance:mode --on
docker compose exec -T db pg_dump -U nextcloud -Fc nextcloud > /var/backups/nextcloud.pgdump
restic backup /var/backups/nextcloud.pgdump /opt/nextcloud/data /opt/nextcloud/compose.yaml /opt/nextcloud/db.env \
  --exclude '/opt/nextcloud/data/appdata_*/preview'
docker compose exec -T -u www-data app php occ maintenance:mode --off
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

The config.php inside the nextcloud volume holds the instance ID and secrets; grab it too (docker compose cp app:/var/www/html/config/config.php /var/backups/) or back up the whole named volume. The automated backups add-on at order time is a convenient extra layer for whole-server rollback, but it lives in the same datacenter as the server; the restic copy is the one that survives a lost server or a forgotten renewal.

Operations: updates, monitoring, firewall

  • AIO updates itself: a daily check pulls new images for the containers it manages and offers the Nextcloud major upgrade in its interface. Enable the daily backup and update window there so the two happen together.
  • Compose updates: bump the tag (35.0.x within a major is safe; 35 to 36 only after the AIO/announcement says the release is out of RC), docker compose pull && docker compose up -d, then docker compose exec -u www-data app php occ upgrade if it did not run automatically, followed by occ db:add-missing-indices.
  • Check Administration, Overview after any change; it lists missing indices, wrong proxy headers, missing cron runs and the PHP memory limit.
  • Monitoring: the /status.php endpoint returns JSON with the version and maintenance flag; point an uptime check at it. Watch df -h for the data mount and docker system df for image bloat.
  • Firewall: ufw should show OpenSSH, 80/tcp and 443/tcp only. Ports 8080 and 11000 are bound to 127.0.0.1 and must not be opened. Talk’s TURN server needs 3478/tcp+udp if you enable it on the compose route; AIO routes Talk through 443 by default.
  • Brute force: Nextcloud has its own login throttling built in. Enable two-factor (TOTP app) for the admin at minimum, and consider the "Enforce 2FA" setting for everyone.

Which plan to pick

For a personal instance with calendar, contacts and documents, BVCX23 (40 GB, from $11/mo) does the job and BVCX33 (80 GB, 8 GB RAM, from $16/mo) adds room for phone photos and Collabora. For a household library start at 160 GB (BVX31 from $32/mo, or BVA31 on Arm). Pick Germany or Finland unless your devices are mostly in North America or Asia, in which case the US and Singapore BVX plans cut sync latency. Configure a server with Ubuntu 24.04, pay in Bitcoin or Monero for the term you want, and point cloud.example.com at the address in the access email.

Ready to run this yourself?

Linux KVM servers with NVMe storage in the EU, US and Singapore. Root access arrives by magic link; no account, no KYC.

Configure a server — pay with Bitcoin, no account needed