Self-Host Nextcloud on a VPS: Your Own Private Cloud Storage

Published on | Updated

Host Nextcloud on a VPS with Docker Compose: MariaDB, Redis, a cron container and Caddy for HTTPS, plus sync clients, security steps and storage planning.

A VPS control panel with a disk usage bar and a bandwidth meter

Nextcloud gives you Google Drive and Dropbox style file sync on a server you control. Your files stay on your VPS, in the data center you picked, under your rules. Here is how to set it up with Docker Compose, checked against the Nextcloud 35 admin manual.

Quick answer: Point a domain at your VPS, open ports 80 and 443, and run five containers with Docker Compose: the community nextcloud image, MariaDB 11.8, Redis, a cron container for background jobs and Caddy, which gets and renews the HTTPS certificate on its own. Put your passwords in a .env file, write a short Caddyfile and run docker compose up -d. Your storage limit is the VPS disk, so leave room for old file versions, deleted files and previews. If you want the install route Nextcloud itself supports, use Nextcloud All-in-One instead.

Why Self-Hosted Cloud Storage

  • Privacy: your files live on a server you run, not in a consumer cloud account, and you decide who gets an account
  • Data sovereignty: your files stay in the data center you choose
  • Storage you size yourself: no cloud plan caps you, but the VPS disk does (plus any external storage you connect), and versions, deleted files and previews use space too. Our Nextcloud storage planning guide shows how much and how to cap it
  • More than files: Calendar and Contacts (CalDAV and CardDAV), Mail, Deck boards and Talk for chat and video calls; office editing works through Nextcloud Office or Collabora Online, which you set up on top
  • No licence fee for the server: Nextcloud Server is open source under the AGPL-3.0, so you pay for the VPS

Requirements

  • A VPS with root access and Docker Engine plus the Compose plugin, installed from Docker's own packages (they cover Ubuntu 22.04, 24.04 and 26.04)
  • RAM for PHP, MariaDB and Redis together: Nextcloud's manual asks for at least 128 MB per PHP process and recommends 512 MB per process, and real use grows with users, apps and files
  • Disk space for your files plus versions, deleted files and previews (see Storage Planning below)
  • A domain or subdomain with an A record (and an AAAA record for IPv6) pointing at the VPS, and ports 80 and 443 open

Installation with Docker

This guide uses the community nextcloud Docker image. Nextcloud's admin manual calls Nextcloud All-in-One (AIO) the official installation method and says the community options, this image included, are not officially supported by Nextcloud GmbH. The image's own README says volunteers maintain it and that it is designed for expert use. Our Nextcloud setup overview walks through the AIO route. If you want to see and control each part of the stack, carry on here.

Run the docker commands below with sudo unless your user is in the docker group. Make a folder, for example ~/nextcloud, and create a .env file in it with your own long random passwords. Compose reads this file from the project folder and fills in the ${...} values:

MYSQL_ROOT_PASSWORD=change-this-root-password
MYSQL_PASSWORD=change-this-database-password
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=change-this-admin-password

Then create compose.yaml in the same folder and replace cloud.example.com with your domain:

services:
  nextcloud:
    image: nextcloud:stable
    restart: unless-stopped
    volumes:
      - nextcloud_data:/var/www/html
    environment:
      - MYSQL_HOST=db
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
      - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
      - NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com
      - REDIS_HOST=redis
      - APACHE_DISABLE_REWRITE_IP=1
      - TRUSTED_PROXIES=172.28.0.0/16
      - OVERWRITEPROTOCOL=https
      - OVERWRITECLIURL=https://cloud.example.com
    depends_on:
      - db
      - redis

  cron:
    image: nextcloud:stable
    restart: unless-stopped
    volumes:
      - nextcloud_data:/var/www/html
    entrypoint: /cron.sh
    depends_on:
      - db
      - redis

  db:
    image: mariadb:11.8
    restart: unless-stopped
    command: --transaction-isolation=READ-COMMITTED
    volumes:
      - db_data:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}

  redis:
    image: redis:7-alpine
    restart: unless-stopped

  caddy:
    image: caddy:2
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./conf:/etc/caddy
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - nextcloud

networks:
  default:
    ipam:
      config:
        - subnet: 172.28.0.0/16

volumes:
  nextcloud_data:
  db_data:
  caddy_data:
  caddy_config:

What the less obvious lines do:

  • nextcloud:stable: on 31 October 2026 this tag points to Nextcloud 34.0.5 (Apache variant), while latest points to 35.0.2. Both work with this file; pick one and stay on it.
  • mariadb:11.8: a MariaDB version that both tags support. The Nextcloud 34 manual (the stable tag) marks 11.8 as recommended and does not list 12.3; the Nextcloud 35 manual recommends 12.3 and supports 11.8 too. Do not use mariadb:latest: it points to MariaDB 13, which neither manual lists. The --transaction-isolation=READ-COMMITTED flag is required for Nextcloud on MySQL or MariaDB.
  • NEXTCLOUD_ADMIN_USER and NEXTCLOUD_ADMIN_PASSWORD: with these and the database variables set, the install runs on first start and skips the web-based setup wizard, so no setup page waits on a public URL for whoever opens it first.
  • TRUSTED_PROXIES, APACHE_DISABLE_REWRITE_IP, OVERWRITEPROTOCOL and OVERWRITECLIURL: these tell Nextcloud that Caddy is a trusted proxy and that the public URL uses HTTPS. Caddy sends an X-Forwarded-Host header, and without a trusted proxy Nextcloud's admin overview reports the reverse proxy header setup as incorrect.
  • networks: pins the Compose network to 172.28.0.0/16 (the example range in Docker's Compose docs, inside the private 172.16.0.0/12 block), so the trusted proxy range is known before the first start. If another Docker network on your server already uses that range, pick another private range and change both lines.
  • No ports on the nextcloud service: only Caddy is reachable from outside. Compose puts all services on one network where they find each other by service name, so Caddy reaches Nextcloud at nextcloud:80.

HTTPS with Caddy and Let's Encrypt

Caddy serves your domain over HTTPS with a certificate from a public ACME CA such as Let's Encrypt or ZeroSSL, keeps it renewed and redirects HTTP to HTTPS, as long as the domain's A/AAAA records point to the server, ports 80 and 443 are reachable and its data folder is persistent (the caddy_data volume). You do not need Certbot or a separate certificate step.

Create conf/Caddyfile next to compose.yaml, again with your own domain in the first line:

cloud.example.com {
    header Strict-Transport-Security "max-age=15552000; includeSubDomains"
    redir /.well-known/carddav /remote.php/dav/ 301
    redir /.well-known/caldav /remote.php/dav/ 301
    reverse_proxy nextcloud:80
}

The two redir lines come from Nextcloud's reverse proxy documentation: behind a proxy, Nextcloud's own CalDAV and CardDAV redirects do not work, so the proxy does them and calendar and contact apps still find the endpoints. The HSTS header uses the value from Nextcloud's hardening guide. The Compose file mounts the conf folder instead of the single file because Caddy's image docs warn that with a single-file mount, edits from some editors only reach the container after it is recreated.

Start the stack from the project folder:

cd ~/nextcloud
docker compose up -d

Then open https://cloud.example.com and log in with the admin account from your .env file.

Performance Tuning

PHP Memory

memory_limit is a PHP ini setting, not a Nextcloud config.php option, and Nextcloud's manual says it should be at least 512 MB. The image sets it from the PHP_MEMORY_LIMIT variable, which defaults to 512M. To give PHP more, add PHP_MEMORY_LIMIT with your value to the environment list of the nextcloud service and run docker compose up -d again. PHP_UPLOAD_LIMIT (also 512M by default) sets the PHP upload limits the same way.

Redis Caching

With REDIS_HOST=redis set, the image configures Redis for the distributed cache and for transactional file locking, and APCu is already set as the local cache. That is the split Nextcloud's manual recommends for a single server, so you do not have to edit config.php. The image merges several config files, so check the result with occ instead of reading config.php:

docker compose exec -u www-data nextcloud php occ config:list system

Background Jobs

Nextcloud runs background jobs through AJAX by default, and the manual recommends cron instead. The cron container handles that: the image's crontab runs php -f /var/www/html/cron.php every 5 minutes as www-data, and once cron.php runs from cron, Nextcloud switches its background job setting to Cron by itself. Keep the volumes of the cron and nextcloud services identical.

Mobile and Desktop Sync

Nextcloud has official clients for:

  • Desktop: Windows, macOS and Linux (AppImage); pick folders on your computer and they stay in sync
  • Mobile: Android and iOS apps for file access and sync, which can upload new photos and videos from your phone on their own
  • WebDAV: Nextcloud has its own built-in WebDAV server, so WebDAV clients and tools such as rclone can connect

Security

  • HTTPS only: Caddy redirects HTTP to HTTPS, and the Caddyfile above sends the HSTS header the hardening guide recommends
  • Two-factor authentication: the TOTP provider ships with Nextcloud but is disabled by default (enable it under Apps, Disabled apps), and an admin can enforce 2FA for all users or for selected groups
  • Updates: run docker compose pull and then docker compose up -d. The image disables Nextcloud's built-in web updater, and you can only upgrade one major version at a time, so if you fall behind, step through each major tag (for example 34-apache, then 35-apache)
  • Firewall: allow only SSH, HTTP on port 80 (Caddy needs it for the certificate challenge and the redirect) and HTTPS on 443 (plus 443/UDP for HTTP/3). Ports that Docker publishes bypass ufw rules, so publish only the ports you want public, as the Compose file above does with Caddy
  • Server-side encryption: leave it off unless you add external storage. The manual says it is mainly meant for files on external, third-party storage, does not protect against a compromised server, cannot be undone from the web interface, and that losing the keys means losing the files
  • Backups: back up the config, data and theme folders, any custom apps and the database together, with maintenance mode on (docker compose exec -u www-data nextcloud php occ maintenance:mode --on, and --off when you are done). Backups are not included in the plan price of a Space-Node VPS; you can arrange them through a support ticket, or copy your own to storage outside the VPS

Storage Planning

Nextcloud keeps files on the VPS disk by default, so the disk is your ceiling. Measure what you want to move, then add room for old versions, deleted files and previews, which take space on top of your files. Our Nextcloud storage planning guide shows how much they use, how to cap them, and when external or S3 storage fits. You can also set a storage quota per user so one account cannot fill the disk.

Space-Node's VPS plans come with 60 to 300 GB of NVMe storage on Standard and 20 to 240 GB on Premium, and the order form offers extra NVMe storage as an add-on.

Once it runs, Nextcloud gives you full control over your data. The routine below keeps it that way.

VPS Operator Checklist

  1. Create a non-root user for daily work.
  2. Turn on SSH key login where possible.
  3. Enable basic firewall rules before exposing apps.
  4. Make a backup before risky upgrades.
  5. Keep app data and config paths written down.
  6. Test restore steps before you need them.

What Beginners Usually Miss

The hidden risk is not the first install. It is the second month, when packages need updates and nobody remembers where the config lives. Write down ports, service names, data folders and backup commands in one note.

Also watch disk space. Logs, Docker images, media files and old backups can fill a VPS quietly. A full disk can break databases and make a healthy service look broken.

Where to Go Next

For plans and safer operation, use VPS hosting, VPS SSH security hardening and a backup restore drill.

Real Test Routine

Test each change, write it down and make sure you can undo it. A VPS can run many different services, but every service adds ports, logs, updates and backups. Keep the setup clear enough that you can fix it when tired.

Before making a change, check disk space, memory, active services and open ports. After the change, restart only the service you touched if possible. Then check logs, confirm the port is reachable and make sure the data folder still has the expected files. If the change involves packages or the kernel, make a backup first.

For beginners, the best VPS habit is writing down the service name, config path, data path, backup command and restore command. This turns a panic moment into a checklist. It also helps if someone else needs to help later.

When to Split Services

A single VPS is fine for learning and small projects. Split services when one app can fill disk space, use all memory or need a different update schedule. Databases, media libraries, game servers and public web apps often deserve separate backups even if they live on the same machine.

Sources

All opened on 31 October 2026: