Deploying Nextcloud with Docker Compose on a Self-Hosted VPS

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:

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
Watch out: Nextcloud with SQLite is tempting because it requires no separate database. Don't. SQLite locks under concurrent access, and Nextcloud will grind to a halt. PostgreSQL (what I'm using) or MySQL add maybe 50MB of memory overhead and buy you reliability.

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;
        }
    }
}
Tip: The 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:

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:

Update Nextcloud