Deploying Nextcloud with Docker Compose on a Self-Hosted VPS
I've deployed Nextcloud on at least six different VPS platforms, and I keep coming back to Docker Compose because it's predictable and reproducible. If you want a private file-sync service that actually stays synced—without trusting Dropbox, Google Drive, or OneDrive with your data—Nextcloud is the answer. In this tutorial, I'll walk you through a production-ready deployment on a budget VPS (you can grab one from RackNerd for around $40/year), including SSL certificates, persistent storage, and the database setup that doesn't fall apart after a reboot.
Why Nextcloud and Why Docker?
Nextcloud is a fork of OwnCloud that lets you sync files, calendars, contacts, and notes across devices. It's mature, actively maintained, and there's a massive community around it. The best part? You control the server.
Docker Compose keeps everything in one declarative file. No hunting through systemd services or package dependencies. When something breaks, you nuke the containers and start fresh. No orphaned processes. When you're managing a homelab or renting a tiny VPS, that's invaluable.
I prefer Caddy as a reverse proxy in front of Nextcloud because it auto-renews SSL certificates and requires zero configuration beyond pointing it at Nextcloud. But I'll show you how to use standard Nginx too.
Prerequisites and VPS Choice
You'll need:
- A VPS with at least 2GB RAM and 2 vCPU (RackNerd's budget plans hit this)
- A domain name pointing to your VPS IP
- SSH access to the VPS
- Docker and Docker Compose installed (curl -fsSL https://get.docker.com | sh if you're starting fresh)
- Basic knowledge of SSH, ports, and firewalls
I'm using Ubuntu 24.04 LTS as the OS. If you're on Debian 12, 99% of this applies directly.
Directory Structure and Planning
Before we write any YAML, let me show you the directory layout I use:
/root/nextcloud/
├── docker-compose.yml
├── .env
├── nginx/
│ └── nginx.conf
└── data/
├── nextcloud/
├── postgres/
└── certs/
The data directory holds all persistent volumes. I back this up to an external service weekly. Create it now:
mkdir -p /root/nextcloud/data/{nextcloud,postgres,certs}
mkdir -p /root/nextcloud/nginx
cd /root/nextcloud
Writing the Docker Compose File
This is the core of everything. Create docker-compose.yml:
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: nextcloud-db
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- ./data/postgres:/var/lib/postgresql/data
restart: unless-stopped
networks:
- nextcloud-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U nextcloud"]
interval: 10s
timeout: 5s
retries: 5
nextcloud:
image: nextcloud:29-apache
container_name: nextcloud-app
depends_on:
postgres:
condition: service_healthy
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_HOST: postgres
NEXTCLOUD_ADMIN_USER: ${NC_ADMIN_USER}
NEXTCLOUD_ADMIN_PASSWORD: ${NC_ADMIN_PASSWORD}
NEXTCLOUD_TRUSTED_DOMAINS: "${NC_DOMAIN}"
OVERWRITEPROTOCOL: https
OVERWRITEHOST: "${NC_DOMAIN}"
OVERWRITEWEBROOT: /
volumes:
- ./data/nextcloud:/var/www/html
restart: unless-stopped
networks:
- nextcloud-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/status.php"]
interval: 15s
timeout: 5s
retries: 3
nginx:
image: nginx:alpine
container_name: nextcloud-web
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./data/certs:/etc/nginx/certs:ro
- ./data/nextcloud:/var/www/html:ro
depends_on:
- nextcloud
restart: unless-stopped
networks:
- nextcloud-net
networks:
nextcloud-net:
driver: bridge
volumes:
nextcloud:
postgres:
certs:
Now create the .env file (this keeps secrets out of the compose file):
# .env
DB_PASSWORD=your_very_long_random_password_here_32_chars_minimum
NC_ADMIN_USER=admin
NC_ADMIN_PASSWORD=another_very_long_random_password
NC_DOMAIN=nextcloud.yourdomain.com
Replace the domain and passwords with actual values. Generate strong passwords with openssl rand -base64 32.
Nginx Configuration (SSL-Ready)
Create nginx/nginx.conf. This is minimal but production-ready:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 10G;
upstream nextcloud {
server nextcloud:80;
}
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name _;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php$is_args$args;
proxy_pass http://nextcloud;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ \.php$ {
proxy_pass http://nextcloud;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
}
client_max_body_size 10G; line lets you upload large files. Adjust based on your VPS storage. For a 2GB VPS, I'd cap this at 2G.SSL Certificates with Let's Encrypt
I'll use Certbot to generate certificates before starting containers:
apt update && apt install -y certbot
certbot certonly --standalone -d nextcloud.yourdomain.com --agree-tos -m [email protected]
# Certbot will prompt for email and terms. Copy certs to our data directory:
cp /etc/letsencrypt/live/nextcloud.yourdomain.com/fullchain.pem ./data/certs/
cp /etc/letsencrypt/live/nextcloud.yourdomain.com/privkey.pem ./data/certs/
chmod 644 ./data/certs/*
To auto-renew, add a monthly cron job:
echo "0 2 * * 0 certbot renew --quiet && cp /etc/letsencrypt/live/nextcloud.yourdomain.com/* /root/nextcloud/data/certs/" | crontab -
Firewall and Port Forwarding
Open ports 80 and 443 on your VPS firewall:
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
If you're behind a home router (not likely for a VPS, but good practice), port-forward WAN:80 and WAN:443 to your VPS internal IP on those same ports.
Launching the Stack
From the /root/nextcloud directory:
docker-compose up -d
docker-compose logs -f nextcloud
Watch the logs. The first startup takes 2–3 minutes because Nextcloud initializes the database and creates admin accounts. You'll see something like:
nextcloud-app | [28-Mar-2026 14:32:10] Installing NextCloud...
nextcloud-app | [28-Mar-2026 14:32:45] NextCloud installed successfully
Once you see "NextCloud installed successfully", hit Ctrl+C and visit https://nextcloud.yourdomain.com. You should land on the login page. Use the credentials from your .env file.
Post-Installation Hardening
Log in as admin and immediately:
- Enable 2FA: Profile → Security → Two-factor authentication
- Disable WebDAV on untrusted clients: Admin → Settings → Sharing → Restrict to trusted servers
- Set an app password for clients: Profile → Security → App passwords (generates a token for your phone/desktop client)
- Configure SMTP: Admin → Settings → Email server (so Nextcloud can send password resets)
I also disable user registration and use invite links only. Go to Admin → Settings → Sharing and toggle "Allow users to share with everyone".
Backup Strategy
Your data/ directory is everything. Back it up off-site weekly:
#!/bin/bash
# backup-nextcloud.sh
BACKUP_DIR="/mnt/backup"
SOURCE="/root/nextcloud/data"
tar --exclude='*.tmp' -czf "$BACKUP_DIR/nextcloud-$(date +%Y%m%d).tar.gz" "$SOURCE"
find "$BACKUP_DIR" -name "nextcloud-*.tar.gz" -mtime +30 -delete
Run this via cron weekly. For off-site, I use rclone to push to S3-compatible storage (Backblaze B2 costs ~$6/month for 1TB).
Monitoring and Maintenance
Check resource usage regularly:
docker stats nextcloud-app nextcloud-db
du -sh /root/nextcloud/data/*
If your Nextcloud becomes slow, the usual culprits are:
- Database table bloat (run Admin → Maintenance → Run background jobs)
- File locking (PostgreSQL deadlocks; restart the container)
- Lack of memory for PHP (increase the VPS size; 2GB is tight for 50+ users)
Update Nextcloud