How to Self-Host Nextcloud on Ubuntu with Docker
The Problem: You Want Private Cloud Storage Without Monthly Bills
You're tired of subscription services for cloud storage, and you've got spare hardware sitting in your rack. A self-hosted Nextcloud deployment on your own Ubuntu server gives you full control over your files, zero vendor lock-in, and the satisfaction of running your own infrastructure. This guide walks you through a production-ready Nextcloud setup using Docker Compose, Nginx reverse proxy, and Let's Encrypt SSL—tested on Ubuntu 24.04 LTS.
Prerequisites: What You Need Before Starting
- Ubuntu 24.04 LTS (minimum 2 CPU cores, 4GB RAM, 50GB+ storage)
- Docker 26.1.3+ and Docker Compose 2.29.1+
- A domain name pointing to your public IP (or use a DDNS service)
- Nginx 1.26+
- Basic familiarity with Docker, volumes, and reverse proxies
- A firewall rule allowing ports 80 and 443 inbound
On my T5810 with 24GB RAM running Ubuntu 24.04, I allocate 4 CPU cores to Nextcloud and 8GB RAM for comfortable operation with 30+ users.
Installing Docker and Docker Compose
Start with a clean system update, then add Docker's official repository:
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker $USER
newgrp dockerVerify installation:
docker --version
docker compose versionGotcha #1: You must run newgrp docker in the current shell session or log out and back in. Docker daemon won't show up in your group membership until your shell environment reloads.
Creating the Nextcloud Docker Compose Stack
Create a dedicated directory structure for your Nextcloud homelab setup:
mkdir -p ~/nextcloud/{config,data,postgres,backups}
cd ~/nextcloudNow create your docker-compose.yml. This stack includes PostgreSQL (better than SQLite for production), Redis caching, and a dedicated app container:
version: '3.9'
services:
postgres:
image: postgres:16-alpine
container_name: nextcloud_db
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: your_secure_postgres_password_here
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C"
volumes:
- ./postgres:/var/lib/postgresql/data
restart: unless-stopped
networks:
- nextcloud
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 --requirepass your_secure_redis_password_here
volumes:
- ./redis:/data
restart: unless-stopped
networks:
- nextcloud
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
nextcloud:
image: nextcloud:29-apache
container_name: nextcloud_app
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
POSTGRES_HOST: postgres
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: your_secure_postgres_password_here
REDIS_HOST: redis
REDIS_HOST_PASSWORD: your_secure_redis_password_here
NEXTCLOUD_TRUSTED_DOMAINS: "nextcloud.yourdomain.com"
NEXTCLOUD_DATA_DIR: /var/www/html/data
APACHE_DISABLE_REWRITE_IP: "1"
TRUSTED_PROXIES: "172.17.0.0/16"
volumes:
- ./config:/var/www/html/config
- ./data:/var/www/html/data
- ./themes:/var/www/html/themes
restart: unless-stopped
networks:
- nextcloud
expose:
- 80
networks:
nextcloud:
driver: bridgeGotcha #2: The nextcloud:29-apache image already has Apache configured with mod_rewrite. If you use the base FPM image instead, you'll need Nginx upstream configuration. Stick with Apache for simpler reverse proxy setup unless you're optimizing for extremely low memory usage.
Configuring Nginx as a Reverse Proxy with SSL
Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginxCreate a new Nginx site configuration:
sudo nano /etc/nginx/sites-available/nextcloudPaste this configuration (replace nextcloud.yourdomain.com with your actual domain):
upstream nextcloud_app {
server 172.17.0.3:80;
}
# Redirect HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name nextcloud.yourdomain.com;
location / {
return 301 https://$server_name$request_uri;
}
}
# Main HTTPS server block
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name nextcloud.yourdomain.com;
# SSL certificates (generated by Certbot)
ssl_certificate /etc/letsencrypt/live/nextcloud.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/nextcloud.yourdomain.com/privkey.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer" always;
# Performance
client_max_body_size 10G;
proxy_buffering off;
proxy_redirect off;
location / {
proxy_pass http://nextcloud_app;
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;
proxy_set_header X-Forwarded-Host $server_name;
proxy_connect_timeout 3600s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
}
# Deny access to sensitive paths
location ~ /\.well-known/(caldav|carddav) {
return 301 $scheme://$host/remote.php/dav;
}
}Enable the site and test the configuration:
sudo ln -s /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled/
sudo nginx -tGenerate your SSL certificate with Certbot:
sudo certbot certonly --nginx -d nextcloud.yourdomain.comEnable the Nginx site and reload:
sudo systemctl restart nginxLaunching and Initializing Nextcloud
Start the Docker stack:
docker compose up -dWait 30 seconds for Nextcloud to initialize, then access the web interface at https://nextcloud.yourdomain.com. You'll see the setup wizard. Create an admin account and confirm the database settings are auto-populated from your .yml environment variables.
Once initialized, enable additional caching and background jobs via the web UI or by manually editing the config file. SSH into your server and add these to optimize performance:
docker exec nextcloud_app php occ config:system:set redis 'host' --value 'redis'
docker exec nextcloud_app php occ config:system:set redis 'port' --value '6379'
docker exec nextcloud_app php occ config:system:set redis 'password' --value 'your_secure_redis_password_here'
docker exec nextcloud_app php occ config:system:set memcache.local --value '\OC\Memcache\APCu'
docker exec nextcloud_app php occ config:system:set memcache.distributed --value '\OC\Memcache\Redis'
docker exec nextcloud_app php occ config:system:set memcache.locking --value '\OC\Memcache\Redis'Enable the background job cron service (from within the web UI under Settings > Administration > Background jobs). This ensures large file operations and maintenance tasks run smoothly without relying on user-triggered cron jobs.
Performance Tuning for Your Homelab
Nextcloud's default config works, but you'll notice sluggish large file operations without optimization. Add these tweaks to your Docker container. First, increase the PHP memory limit and upload size by creating a custom Apache config:
mkdir -p config/php-local/
cat > config/php-local/limits.conf << 'EOF'
php_value memory_limit 2048M
php_value upload_max_filesize 10G
php_value post_max_size 10G
php_value max_input_time 3600
php_value max_execution_time 3600
EOFMount this into your container by updating the docker-compose.yml volumes section for the Nextcloud service:
volumes:
- ./config:/var/www/html/config
- ./data:/var/www/html/data
- ./themes:/var/www/html/themes
- ./config/php-local/limits.conf:/usr/local/etc/php/conf.d/limits.conf:roRestart the container:
docker compose restart nextcloudEnable periodic full-text search indexing for file discovery. This runs asynchronously:
docker exec nextcloud_app php occ config:system:set enable_previews --value 'true'
docker exec nextcloud_app php occ app:enable recognize
docker exec nextcloud_app php occ recognize:classify-folder -f /var/www/html/dataCommon Issues and Troubleshooting
504 Gateway Timeout on Large File Uploads
If you're seeing 504 errors when uploading files over 1GB, your proxy timeouts are too short. Verify the proxy_read_timeout and proxy_send_timeout are both set to 3600s in your Nginx config, then check the Nextcloud logs:
docker logs nextcloud_app | tail -50Also confirm your Docker volume has sufficient IOPS. On spinning disks, uploads will timeout. Consider using an SSD for ./data volume if you're running on older storage.
Nextcloud Reporting "Untrusted Domain"
Update the trusted domains in your config after initial setup. The environment variable only sets it during first initialization:
docker exec nextcloud_app php occ config:system:set trusted_domains 1 --value 'nextcloud.yourdomain.com'
docker exec nextcloud_app php occ config:system:set trusted_domains 2 --value 'www.nextcloud.yourdomain.com'PostgreSQL Connection Errors After Container Restart
The Postgres container health check prevents Nextcloud from starting before the database is ready. If you still see connection errors, check that both containers are on the same Docker network:
docker network ls
docker network inspect nextcloudEnsure both postgres and nextcloud containers are listed in the Containers section.
Redis Connection Failures
If Redis fails to connect, verify the password is identical in both the Redis container environment variable and the Nextcloud caching config commands. A single character mismatch will silently fail Redis operations without throwing obvious errors—check the logs:
docker logs nextcloud_app | grep -i redisMaintaining Your Installation
Docker image updates roll out regularly. Keep Nextcloud current with:
docker compose pull
docker compose up -dThis pulls the latest image tag and recreates the container. Your data persists in the named volumes. After major version bumps (e.g., 28 to 29), run the upgrade command from within the app:
docker exec nextcloud_app php occ upgradeBackup your PostgreSQL database weekly:
docker exec nextcloud_db pg_dump -U nextcloud nextcloud | gzip > ~/nextcloud/backups/nextcloud_$(date +%Y%m%d).sql.gzWhat You Now Have
You're running a production-ready self-hosted Nextcloud deployment with encrypted TLS, Redis caching, PostgreSQL persistence, and optimized upload performance. Your data stays on your hardware, zero subscription fees, and you control every layer of the stack.