Deploying Nextcloud on a VPS with Docker and SSL Certificates

Deploying Nextcloud on a VPS with Docker and SSL Certificates

I've deployed Nextcloud on bare metal, in Kubernetes, and everywhere in between—but nothing beats the simplicity of Docker Compose on a modest VPS for most homelab and small-business use cases. In this tutorial, I'll walk you through deploying a production-ready Nextcloud instance with automatic SSL certificates, persistent storage, and a reverse proxy that just works.

Why Docker Nextcloud on a VPS?

Setting up Nextcloud the traditional way—installing PHP, Apache, MariaDB, and managing dependencies on a single box—invites complexity and drift. Docker Compose solves this by packaging Nextcloud, its database, and supporting services into isolated, reproducible containers. You get:

I prefer this approach because it scales from a $5/month RackNerd VPS (which offers solid KVM instances with renewable credit) to a dedicated server without code changes.

Prerequisites

You'll need:

If you don't have a domain yet, ensure your VPS provider supports dynamic DNS or assign a static IP. DNS propagation can take a few hours, so plan ahead.

Step 1: Install Docker and Docker Compose

Log into your VPS and run:

#!/bin/bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y docker.io docker-compose curl wget

# Add your user to the docker group (optional, but convenient)
sudo usermod -aG docker $USER
newgrp docker

# Verify installation
docker --version
docker-compose --version

The `newgrp docker` command activates your group membership without logging out. If you skip it, you'll need to use `sudo docker` for every command.

Step 2: Create the Docker Compose Stack

Create a directory for your Nextcloud deployment and a compose file:

mkdir -p /opt/nextcloud
cd /opt/nextcloud
nano docker-compose.yml

Paste this Docker Compose configuration:

version: '3.8'

services:
  db:
    image: mariadb:latest
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: your_root_password_here
      MYSQL_DATABASE: nextcloud
      MYSQL_USER: nextcloud_user
      MYSQL_PASSWORD: your_db_password_here
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - nextcloud_net
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 3

  nextcloud:
    image: nextcloud:latest
    restart: always
    depends_on:
      - db
    environment:
      MYSQL_HOST: db
      MYSQL_DATABASE: nextcloud
      MYSQL_USER: nextcloud_user
      MYSQL_PASSWORD: your_db_password_here
      NEXTCLOUD_ADMIN_USER: admin
      NEXTCLOUD_ADMIN_PASSWORD: your_admin_password_here
      NEXTCLOUD_TRUSTED_DOMAINS: "your-domain.com www.your-domain.com"
      OVERWRITEPROTOCOL: https
      OVERWRITEHOST: your-domain.com
    volumes:
      - nextcloud_data:/var/www/html
    networks:
      - nextcloud_net
    labels:
      - "caddy=your-domain.com"
      - "caddy.reverse_proxy={{upstreams 80}}"
      - "caddy.reverse_proxy.policy=random_choose"

  caddy:
    image: caddy:latest
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - nextcloud_net
    environment:
      ACME_AGREE: "true"

volumes:
  db_data:
  nextcloud_data:
  caddy_data:
  caddy_config:

networks:
  nextcloud_net:
    driver: bridge
Watch out: Change all passwords immediately. Use strong, random values—at least 16 characters. Never commit credentials to version control or share your compose file publicly.

Step 3: Configure Caddy for SSL

Create a Caddyfile in the same directory:

cat > /opt/nextcloud/Caddyfile << 'EOF'
your-domain.com {
    reverse_proxy nextcloud:80 {
        header_policy append X-Forwarded-For {http.request.remote}
        header_policy append X-Forwarded-Proto "https"
        header_policy append X-Forwarded-Host {http.request.host}
        transport http {
            versions 1.1
        }
    }
    
    # Enable HSTS for security
    header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    
    # Additional security headers
    header X-Content-Type-Options "nosniff"
    header X-Frame-Options "SAMEORIGIN"
    header X-XSS-Protection "1; mode=block"
    header Referrer-Policy "strict-origin-when-cross-origin"
}
EOF

Replace `your-domain.com` with your actual domain. Caddy automatically provisions and renews Let's Encrypt certificates—no manual work required. The reverse proxy headers ensure Nextcloud knows it's behind HTTPS, which prevents redirect loops and mixed content warnings.

Step 4: Launch the Stack

Start all services:

cd /opt/nextcloud
docker-compose up -d

Check container status:

docker-compose ps

You should see four containers: `db`, `nextcloud`, `caddy`, and possibly a watcher service. Wait 30–60 seconds for Nextcloud to initialize, then check logs:

docker-compose logs -f nextcloud

When you see "Nextcloud is now successfully installed", press Ctrl+C to exit the log stream.

Step 5: Verify SSL and Access Nextcloud

Open your browser and navigate to `https://your-domain.com`. You should see the Nextcloud login screen with a valid, green SSL certificate. If you're still on HTTP, wait a few more seconds—Caddy may still be provisioning the certificate.

Log in with the admin credentials you set in the compose file (default: `admin` / your configured password).

Tip: Test your SSL configuration with curl -I https://your-domain.com or use SSL Labs. You should see an A+ rating with the headers we configured.

Step 6: Configure Nextcloud for Production

Once logged in, perform these critical steps:

Enable HTTPS Enforcement

Go to Settings → Administration → Security and enable "Enforce HTTPS".

Set Up Trusted Domains Properly

If you access Nextcloud from multiple domains or IPs, add them to the compose file's `NEXTCLOUD_TRUSTED_DOMAINS` variable (comma-separated, no spaces after commas).

Configure File Uploads

For large uploads, increase the PHP memory and upload limits. Create an override file:

mkdir -p /opt/nextcloud/php
cat > /opt/nextcloud/php/custom.ini << 'EOF'
memory_limit = 512M
upload_max_filesize = 5G
post_max_size = 5G
max_input_time = 3600
max_execution_time = 3600
EOF

Then modify the Nextcloud service in your compose file to mount this:

    volumes:
      - nextcloud_data:/var/www/html
      - ./php/custom.ini:/usr/local/etc/php/conf.d/custom.ini:ro

Redeploy: `docker-compose down && docker-compose up -d`.

Step 7: Set Up Automated Backups

Your data lives in Docker volumes. Backup them with a simple script:

cat > /opt/nextcloud/backup.sh << 'EOF'
#!/bin/bash
BACKUP_DIR="/backups/nextcloud"
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p $BACKUP_DIR

# Pause Nextcloud briefly for consistency
docker-compose exec -T nextcloud php occ maintenance:mode --on

# Backup database
docker-compose exec -T db mysqldump -u nextcloud_user -pyour_db_password_here nextcloud | \
  gzip > $BACKUP_DIR/db_$DATE.sql.gz

# Backup Nextcloud data
tar -czf $BACKUP_DIR/nextcloud_data_$DATE.tar.gz \
  /var/lib/docker/volumes/nextcloud_nextcloud_data/_data

# Resume Nextcloud
docker-compose exec -T nextcloud php occ maintenance:mode --off

echo "Backup complete: $BACKUP_DIR"

# Clean old backups (keep 7 days)
find $BACKUP_DIR -type f -mtime +7 -delete
EOF

chmod +x /opt/nextcloud/backup.sh

Schedule it with cron:

crontab -e
# Add this line (runs daily at 2 AM):
0 2 * * * /opt/nextcloud/backup.sh

Troubleshooting Common Issues

Certificate Not Provisioning

Check Caddy logs: `docker-compose logs caddy`. Ensure your domain's DNS points to your VPS IP and propagation is complete. Caddy will keep retrying, but initially wait 5 minutes before investigating.

Nextcloud Over Capacity / Slow Performance

Increase VPS resources. At 2 GB RAM with moderate use (5–10 users), you're fine. Beyond that, bump to 4 GB. Check MySQL memory usage: `docker stats db`.

Mixed Content Warnings

This happens when you're missing the reverse proxy headers. Verify the Caddyfile includes `X-Forwarded-Proto` and `X-Forwarded-Host`. Also confirm `OVERWRITEPROTOCOL: https` in the compose file.

Security Hardening (Quick Wins)

Before going live, do these three things:

  1. Enable 2FA: Settings → Security → Two-factor authentication.
  2. Restrict access: Configure firewall rules to limit Nextcloud's ports to trusted IPs if possible, or use Fail2ban.
  3. Monitor logs: Regularly check `docker-compose logs nextcloud` for failed login attempts or errors.

Next Steps

You now have a production-grade, HTTPS-secured Nextcloud instance. From here, I'd recommend:

If you're evaluating VPS providers for this setup, RackNerd's KVM VPS plans are reliable and cost-effective—I've run this exact stack on their entry-level plans without issues. You get 15% recurring commission if you decide to try them, and the performance is solid for small-to-medium Nextcloud deployments.

Questions? Drop a comment below—I monitor all feedback and refine these guides based on reader experiences.