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:
- Isolation: Each service runs in its own environment; a Nextcloud crash won't kill your database.
- Ease of updates: Pull a new Nextcloud image, recreate the container, and you're done.
- Declarative infrastructure: Your entire stack lives in one YAML file—easy to version and backup.
- SSL automation: Caddy handles certificate renewal automatically via Let's Encrypt.
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:
- A VPS running Ubuntu 22.04 LTS or similar (2 GB RAM minimum; I recommend 4 GB for comfort)
- A domain name pointing to your VPS's public IP
- SSH access with sudo privileges
- Docker and Docker Compose installed
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
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).
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:
- Enable 2FA: Settings → Security → Two-factor authentication.
- Restrict access: Configure firewall rules to limit Nextcloud's ports to trusted IPs if possible, or use Fail2ban.
- 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:
- Install the Files app and configure external storage (SMB, SFTP, S3) for media files.
- Set up Nextcloud Desktop Sync Client on your workstations for seamless file sync.
- Configure LDAP or SAML if you have multiple users to manage.
- Use Watchtower or a scheduled cron job to auto-update Docker images weekly.
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.