Nextcloud File Server Installation: Complete Docker Setup for Private Cloud Storage
I've run Nextcloud on my homelab for three years now, and it's become my single most-used self-hosted application. It replaces Google Drive, OneDrive, and Dropbox entirely—I control every file, every sync, and every backup. This guide walks you through a production-ready Docker Compose installation with PostgreSQL, Redis caching, and proper SSL configuration.
Why Nextcloud Changed My Approach to File Storage
When I first moved away from cloud providers, I tried basic NFS shares and rsync scripts. They worked, but they weren't elegant. Nextcloud gave me something different: a full application layer with web UI, mobile apps that actually sync, versioning, sharing links, and collaborative editing through ONLYOFFICE integration. The killer feature for me was the ability to lock down access with 2FA while still letting family members view shared folders through a simple browser.
The architecture I've settled on uses PostgreSQL instead of SQLite (SQLite causes locking issues at scale), Redis for session caching, and Caddy as a reverse proxy with automatic HTTPS. Total resource footprint on my AMD Ryzen 5600G: about 2GB RAM at rest, spikes to 4GB during large sync operations. Easily runs alongside other services.
Prerequisites and Hosting Recommendation
You'll need a Linux machine with Docker and Docker Compose installed. I recommend at least 4GB RAM and 30GB storage for the application and database. If you don't have hardware at home, RackNerd's KVM VPS plans are solid for this—their entry-level VPS with 2GB RAM and 40GB NVMe runs Nextcloud smoothly, and pricing stays under $20/year with their holiday specials.
You'll also need a domain name (I use a subdomain like files.mydomain.com) and basic DNS access. If you're behind CGNAT or a residential ISP, I'd pair this with Cloudflare Tunnel or Tailscale—more on that below.
Docker Compose Configuration
Here's my production Compose file. I've stripped comments for brevity, but each service is self-explanatory:
version: '3.8'
services:
db:
image: postgres:15-alpine
container_name: nextcloud_db
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: your_secure_db_password_here
volumes:
- db_data:/var/lib/postgresql/data
networks:
- nextcloud_net
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U nextcloud"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: nextcloud_redis
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- nextcloud_net
restart: unless-stopped
app:
image: nextcloud:27-fpm-alpine
container_name: nextcloud_app
depends_on:
db:
condition: service_healthy
environment:
POSTGRES_HOST: db
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: your_secure_db_password_here
REDIS_HOST: redis
NEXTCLOUD_ADMIN_USER: admin
NEXTCLOUD_ADMIN_PASSWORD: your_secure_admin_password_here
NEXTCLOUD_TRUSTED_DOMAINS: "files.mydomain.com localhost"
OVERWRITEPROTOCOL: https
volumes:
- nextcloud_data:/var/www/html
- nextcloud_config:/var/www/html/config
networks:
- nextcloud_net
restart: unless-stopped
web:
image: nginx:alpine
container_name: nextcloud_web
ports:
- "8080:80"
depends_on:
- app
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- nextcloud_data:/var/www/html:ro
networks:
- nextcloud_net
restart: unless-stopped
volumes:
db_data:
redis_data:
nextcloud_data:
nextcloud_config:
networks:
nextcloud_net:
driver: bridge
openssl rand -base64 32 to generate strong secrets. Store them in a password manager, not in version control.Nginx Configuration for FPM
Nextcloud ships with FPM (FastCGI Process Manager) by default, which requires Nginx to handle HTTP requests. Create nginx.conf in the same directory as your Compose file:
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;
keepalive_timeout 65;
gzip on;
gzip_vary on;
gzip_types text/plain text/css text/xml text/javascript
application/x-javascript application/xml+rss
application/javascript application/json;
upstream app {
server app:9000;
}
server {
listen 80;
server_name _;
client_max_body_size 10G;
root /var/www/html;
index index.php;
location ~ \.php$ {
fastcgi_pass app;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_param HTTPS on;
}
location ~ /\.ht {
deny all;
}
location / {
try_files $uri $uri/ /index.php$request_uri;
}
}
}
This configuration runs Nginx on port 8080 inside the container, mapped to your host. I then run Caddy in front (as a separate service or on the host) to handle HTTPS and domain routing.
Starting and Initializing Nextcloud
Bring up the stack:
docker-compose up -d
Wait 30 seconds for the database to initialize. Then check logs:
docker-compose logs -f app
Once you see "Ready to handle connections," access http://localhost:8080 and you should see the Nextcloud setup wizard. The admin credentials match what you set in the environment variables.
Don't skip the important configuration steps inside the wizard. Set up a strong encryption key (Nextcloud generates one), and review the database connection status.
Reverse Proxy Setup with Caddy
I prefer Caddy for its simplicity and automatic HTTPS renewal. If you're running Caddy on your host machine, add this to your Caddyfile:
files.mydomain.com {
reverse_proxy localhost:8080 {
header_uri -X-Forwarded-For
header_uri -X-Forwarded-Proto https
}
encode gzip
}
Reload Caddy and you'll have automatic HTTPS with a Let's Encrypt certificate. If you're using Cloudflare Tunnel for external access (recommended for residential networks), you'd instead create a tunnel and point it at localhost:8080.
Essential Post-Installation Configuration
Log in as admin and navigate to Settings → Administration. I always configure:
- Email: Set a real SMTP server (I use my ISP's mail relay or Mailgun free tier) so Nextcloud can send password resets and notifications.
- Background Jobs: Switch from AJAX to Cron. Run
*/5 * * * * docker exec nextcloud_app php -f /var/www/html/cron.phpon your host. - Data Location: If you want Nextcloud files on a different mount point, configure it here before users start uploading.
- Trusted Domains: Add both your internal IP and external domain to prevent redirect loops.
- Two-Factor Authentication: Enable TOTP enforcement for admin account.
Create user accounts in the Users panel and assign them to groups. I use groups to control access to shared folders without having to set permissions individually.
Mobile Apps and Desktop Sync
Download the official Nextcloud app on iOS or Android. The sync is rock-solid—files update within seconds of changes on other devices. For desktop, I use the Nextcloud Desktop Client on Windows and macOS. It creates a folder that syncs bidirectionally, like Dropbox but with your data under your control.
Performance note: The first sync of a large folder can be slow. I once synced a 200GB photo library and it took about 8 hours. Subsequent syncs are fast because the client only uploads deltas.
Backup Strategy
Your Nextcloud data is in the nextcloud_data volume, and the database is in db_data. I back both up daily using a simple Docker script:
#!/bin/bash
BACKUP_DIR="/backups/nextcloud"
mkdir -p "$BACKUP_DIR"
# Dump database
docker exec nextcloud_db pg_dump -U nextcloud nextcloud | gzip > "$BACKUP_DIR/db_$(date +%Y%m%d_%H%M%S).sql.gz"
# Sync volumes to external drive (assumes /mnt/backup is mounted)
rsync -av --delete /var/lib/docker/volumes/nextcloud_data/_data /mnt/backup/nextcloud/
rsync -av --delete /var/lib/docker/volumes/nextcloud_config/_data /mnt/backup/nextcloud/
# Cleanup old backups (keep 30 days)
find "$BACKUP_DIR" -name "db_*.sql.gz" -mtime +30 -delete
Run this via cron or Watchtower's companion backup service.
Common Issues and Troubleshooting
Blank page after login: Check PHP memory limits. Increase memory_limit in a custom PHP config file mounted to the app container.
Slow file uploads: This is usually Nginx's client_max_body_size being too low. I set it to 10G in the config above, but adjust if you need to upload larger files.
Database connection errors: Ensure the db service is healthy before app starts. The Compose file includes a health check; if you see connection refused, wait another 30 seconds and check logs.
Sync stops on mobile: Go to account settings in the app, toggle sync off and on. Mobile OS background restrictions sometimes pause the daemon; toggling forces a reconnect.
Next Steps
Once Nextcloud is stable, consider adding ONLYOFFICE or Collabora Online for collaborative document editing, or pair it with Immich for photo management. Both integrate seamlessly through the app store.
If you're running this on a VPS (like the RackNerd plans mentioned earlier), enable automatic backups and consider a second VPS in another region for disaster recovery.
Finally, document your setup. I keep a private wiki in Nextcloud itself with passwords, SSL certificate paths, and troubleshooting notes—it's recursive, but useful when you're bringing the server back online after a power failure at 2 AM.