Self-Host Nextcloud on a VPS with Docker and Caddy
Nextcloud is the open-source answer to Dropbox, Google Drive, and iCloud rolled into one. Files, photos, contacts, calendars, notes, and a small army of optional apps, all running on hardware you control.
The official Docker images make it surprisingly painless to get a production-grade install going. This tutorial walks through the nextcloud:apache image (the simpler of the two official options), with PostgreSQL 15 as the database, Redis for file locking and memory caching, and Caddy in front for automatic HTTPS.
By the end you'll have a working cloud.example.com that syncs files from your laptop, phone, and desktop, plus a daily backup script that captures both the database and the data directory.
TL;DR
- Install Docker and Docker Compose on a fresh VPS
- Point a subdomain like
cloud.example.comat your server - Run Nextcloud + PostgreSQL + Redis + Caddy with one
docker-compose.yml - Add the public hostname to
trusted_domainsand bump PHP limits - Schedule a daily Postgres dump plus a tar of the data directory
Total time: around 20 minutes.
What You Need
- A VPS with at least 2 GB RAM (4 GB recommended once you add a few users) running Ubuntu 22.04 or 24.04
- Plenty of disk. Plan for the full size of every file you plan to store, plus 20 percent for previews, versions, and the trashbin
- A domain you can add DNS records to
- Ports
80and443open to the internet - Root or sudo access
Nextcloud is heavier than most self-hosted apps. The Apache image with PHP-FPM, plus a Postgres and Redis container, idles around 400 to 600 MB of RAM. Under load with a handful of active users it can climb past 1 GB easily, which is why 2 GB is the realistic floor.
Step 1: Point a Subdomain at Your VPS
In your DNS provider, add an A record:
cloud.example.com → YOUR_VPS_IPV4
Add an AAAA record if you use IPv6. Verify:
dig +short cloud.example.com
The output must match your VPS IP before Caddy can issue a Let's Encrypt certificate.
Step 2: Install Docker and Docker Compose
On a fresh Ubuntu box:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo 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" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Confirm it's working:
docker --version
docker compose version
Step 3: Open the Firewall
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Caddy needs port 80 for the ACME HTTP challenge and 443 for HTTPS.
Step 4: Create the Project Directory
sudo mkdir -p /opt/nextcloud
cd /opt/nextcloud
sudo mkdir -p nextcloud-data postgres-data redis-data caddy-data caddy-config php-config
Everything persistent lives under /opt/nextcloud. Back up this directory along with a Postgres dump and you can rebuild the stack on any host.
Step 5: Generate Secrets
# PostgreSQL password
openssl rand -base64 32
# Nextcloud admin password (change after first login if you prefer)
openssl rand -base64 24
Save these somewhere safe. They go into the .env file in the next step.
Step 6: Write the Environment File
Create /opt/nextcloud/.env:
# Domain
NEXTCLOUD_DOMAIN=cloud.example.com
NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com
# Database
POSTGRES_USER=nextcloud
POSTGRES_PASSWORD=REPLACE_WITH_POSTGRES_PASSWORD
POSTGRES_DB=nextcloud
# Admin user (created on first boot)
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=REPLACE_WITH_ADMIN_PASSWORD
Lock down the file:
sudo chmod 600 /opt/nextcloud/.env
Step 7: Tune PHP Limits
Out of the box, the Nextcloud image ships with a 512 MB PHP memory limit and a 512 MB upload cap. Both are too small once you start uploading photos or videos from a phone. Create /opt/nextcloud/php-config/zz-nextcloud.ini:
memory_limit = 1024M
upload_max_filesize = 10G
post_max_size = 10G
max_execution_time = 3600
max_input_time = 3600
output_buffering = 0
opcache.enable = 1
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 10000
opcache.memory_consumption = 256
opcache.save_comments = 1
opcache.revalidate_freq = 60
The OPCache values matter: without them, Nextcloud's admin panel shows a permanent yellow warning, and PHP recompiles every page on every request. The defaults are fine for a tiny WordPress site, not for an app this size.
Step 8: Write the Compose File
Create /opt/nextcloud/docker-compose.yml:
services:
postgres:
image: postgres:15
container_name: nextcloud-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- ./postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- cloud-net
redis:
image: redis:7-alpine
container_name: nextcloud-redis
restart: unless-stopped
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"]
volumes:
- ./redis-data:/data
networks:
- cloud-net
nextcloud:
image: nextcloud:apache
container_name: nextcloud
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
environment:
POSTGRES_HOST: postgres
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
REDIS_HOST: redis
REDIS_HOST_PORT: "6379"
NEXTCLOUD_ADMIN_USER: ${NEXTCLOUD_ADMIN_USER}
NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASSWORD}
NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_TRUSTED_DOMAINS}
OVERWRITEPROTOCOL: https
OVERWRITEHOST: ${NEXTCLOUD_DOMAIN}
TRUSTED_PROXIES: 172.16.0.0/12
APACHE_DISABLE_REWRITE_IP: "1"
volumes:
- ./nextcloud-data:/var/www/html
- ./php-config/zz-nextcloud.ini:/usr/local/etc/php/conf.d/zz-nextcloud.ini:ro
networks:
- cloud-net
caddy:
image: caddy:2
container_name: nextcloud-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
networks:
- cloud-net
networks:
cloud-net:
A few notes:
- The
nextcloud:apacheimage bundles PHP and Apache together. It's the easiest path. The alternativefpm-alpineimage is leaner but needs a separate nginx container in front. Stick with Apache unless you have a reason not to. OVERWRITEPROTOCOLandOVERWRITEHOSTtell Nextcloud it's running behind a proxy that terminates TLS. Without these, generated links usehttp://and break clients.TRUSTED_PROXIEScovers the default Docker bridge subnet. If you use a custom network with a different range, adjust it.- The
nextcloud-datavolume holds the application code, config, and user files. It's where the actual cloud lives.
Step 9: Write the Caddyfile
Create /opt/nextcloud/Caddyfile:
cloud.example.com {
encode zstd gzip
header Strict-Transport-Security "max-age=31536000;"
# Nextcloud's well-known redirects for CalDAV / CardDAV
redir /.well-known/carddav /remote.php/dav/ 301
redir /.well-known/caldav /remote.php/dav/ 301
reverse_proxy nextcloud:80
}
Caddy will request and renew the Let's Encrypt certificate automatically. The well-known redirects keep CalDAV and CardDAV clients happy; without them the Nextcloud admin panel flags a security warning.
Step 10: Start the Stack
cd /opt/nextcloud
sudo docker compose up -d
sudo docker compose logs -f
The first boot takes a minute or two. Nextcloud copies itself into the nextcloud-data volume, runs database migrations, and creates the admin user. You'll see a lot of Initializing nextcloud x.x.x ... lines, then a apache2 -D FOREGROUND line, which means it's ready.
Open https://cloud.example.com in a browser. Log in with the admin user and password from your .env file. You should land on the file dashboard.
Step 11: Add a Backup Script
Two things need backing up: the Postgres database, and the nextcloud-data directory (which holds both the user files and the app config). Create /opt/nextcloud/backup.sh:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/var/backups/nextcloud"
DATE="$(date +%F)"
mkdir -p "$BACKUP_DIR"
# Put Nextcloud into maintenance mode for a consistent snapshot
docker exec -u www-data nextcloud php occ maintenance:mode --on
trap 'docker exec -u www-data nextcloud php occ maintenance:mode --off' EXIT
# Dump Postgres
docker exec nextcloud-postgres \
pg_dump -U nextcloud -d nextcloud \
| gzip > "$BACKUP_DIR/nextcloud-db-$DATE.sql.gz"
# Tar the data directory (config + user files)
tar -czf "$BACKUP_DIR/nextcloud-data-$DATE.tar.gz" \
-C /opt/nextcloud nextcloud-data
# Keep 14 days of backups
find "$BACKUP_DIR" -name "nextcloud-*-*.tar.gz" -mtime +14 -delete
find "$BACKUP_DIR" -name "nextcloud-*-*.sql.gz" -mtime +14 -delete
The trap line guarantees maintenance mode is turned off again even if tar fails halfway. Skipping maintenance mode usually works, but a snapshot taken mid-write can include a half-uploaded file with no row in the database.
Make it executable and schedule it:
sudo chmod +x /opt/nextcloud/backup.sh
echo "30 3 * * * root /opt/nextcloud/backup.sh" | \
sudo tee /etc/cron.d/nextcloud-backup
For off-site safety, sync /var/backups/nextcloud to S3, Backblaze B2, or another VPS with rclone on the same schedule.
Step 12: Install the Sync Clients
Nextcloud has first-party clients for every platform that matters:
- Desktop (Windows / macOS / Linux): download from nextcloud.com/install. On first launch, set the server URL to
https://cloud.example.com. - iOS / Android: install the official Nextcloud app from the App Store or Play Store. Same login flow.
- CalDAV / CardDAV: point your calendar or contacts app at
https://cloud.example.com/remote.php/dav/principals/users/YOURUSER/.
The desktop client supports selective sync, on-demand virtual files (Windows and macOS), and end-to-end encryption for specific folders.
Step 13: Optional Office Suite
If you want Google Docs-style collaborative editing, you can attach an office sidecar.
The two common options are Collabora Online and OnlyOffice Document Server. Both run as a separate container, both register with Nextcloud through an app in the marketplace, and both are heavier than the rest of the stack put together. A minimal Collabora sidecar adds about 600 MB of RAM, OnlyOffice closer to 1 GB.
The integration paths are well documented:
- Install the Nextcloud Office app from the in-app marketplace (this is the Collabora-based option, maintained by Nextcloud GmbH).
- Or install the ONLYOFFICE app and point it at a separate
onlyoffice/documentservercontainer.
We won't go deep on either here. If you only need viewing and lightweight edits, skip the sidecar entirely; Nextcloud renders Office files in read-only mode out of the box.
Troubleshooting
Caddy returns 502 for the first minute or two. Nextcloud is still copying itself into the volume on first boot. Wait for the apache2 -D FOREGROUND line in the logs. After that, 502s usually mean the nextcloud container exited; check docker compose logs nextcloud.
"Access through untrusted domain" page. The hostname you typed isn't in trusted_domains. Either add it to the env var and recreate the container, or run docker exec -u www-data nextcloud php occ config:system:set trusted_domains 1 --value=cloud.example.com.
Uploads cap out at 512 MB or fail with "request entity too large." Either the PHP upload_max_filesize ini override didn't load, or Caddy is in the way. Caddy doesn't have a default body limit, so the suspect is almost always PHP. Check docker exec nextcloud php -i | grep upload_max_filesize to confirm.
Redis connection refused in the admin panel. The redis container hasn't started yet, or REDIS_HOST doesn't match the service name. Inside the same Docker network, the hostname is the service name (redis), not localhost.
Background jobs warning in the admin panel. Nextcloud wants its cron job to run every five minutes. The simplest fix is a host cron entry: */5 * * * * root docker exec -u www-data nextcloud php -f /var/www/html/cron.php.
Going Further
- Collabora or OnlyOffice for full collaborative editing in the browser.
- S3-compatible object storage as primary storage if your server's local disk is small. Nextcloud can write user data straight to S3, MinIO, Wasabi, or B2 instead of
nextcloud-data/data. - Full-text search with the Elasticsearch app, so users can search file contents and not just filenames.
- Talk for self-hosted video calls (it's the same project; install the app from the marketplace and add a TURN server for NAT traversal).
- Imaginary as a sidecar for faster image previews of HEIC, RAW, and large photos.
That's it. A self-hosted cloud that syncs your files, calendar, and contacts without leaking metadata to anyone, running on a VPS that costs less per month than most paid sync services.
Need a VPS that can comfortably handle Nextcloud, Postgres, and Redis side by side? Our Linux plans come with NVMe storage, IPv6, and snapshots out of the box. See the options.