Nextcloud Setup - Your Personal Cloud in Docker
Status: Active
Last Updated: 2026-08-14
Category: Cloud - Phase 1: File Storage & Sync
Prerequisites: cloud-storage-concepts, docker-basics, docker-compose-patterns
Time: 2-3 hours
Tags: nextcloud, docker, docker-compose, self-hosted, file-sync, mariadb, redis, reverse-proxy
Summary
Install Nextcloud โ the open-source Dropbox/Google Drive replacement โ using Docker Compose, with a proper production-grade stack: MariaDB database, Redis cache, persistent volumes, and HTTPS behind a reverse proxy. By the end you'll have a working personal cloud at https://cloud.example.com with admin account created and sane defaults applied.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ Deploy Nextcloud via Docker Compose with separate DB/cache containers
- โ Persist all state in named volumes so upgrades never lose data
- โ Configure trusted domains and behind-reverse-proxy settings correctly
- โ Run the initial setup wizard or automate it with environment variables
- โ Apply post-install hardening: phone-home opt-outs, cron background jobs, memory caching
- โ Troubleshoot the most common first-boot failures
Architecture Overview
Nextcloud is a PHP application; production deployments separate concerns:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Internet โโTLSโโโบ โ Reverse proxy (Traefik/Caddy/Nginx) โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ :80 inside docker network
โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโ
โ nextcloud:apache container โ
โโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโโโ
โ โ
โโโโโโโโโโโผโโโโโโ โโโโโโโโผโโโโโโโโ
โ MariaDB โ โ Redis โ
โ (files DB, โ โ (locks, โ
โ users, share)โ โ sessions) โ
โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ
/var/www/html/data โ named volume "nextcloud_data"
Why not the single-container "nextcloud:fpm-alpine + sqlite" quickstart? SQLite collapses under concurrent users, and bundling everything into one container makes backups and upgrades fragile. The compose stack below is the minimum responsible setup.
| Component | Purpose | Resource cost |
|---|---|---|
| nextcloud:apache | App + embedded web server | ~512MBโ1GB |
| mariadb:11 | Metadata: files, shares, users | ~500MB |
| redis:7 | File locking, session cache | ~50MB |
| Reverse proxy | TLS termination | existing infra |
Prerequisites Check
# Docker + compose plugin present?
docker --version && docker compose version
# A DNS record pointing at this host:
# cloud.example.com โ your server IP
# Directory layout for config:
mkdir -p ~/cloud-stack && cd ~/cloud-stack
Pick strong secrets now โ you'll paste them into .env:
openssl rand -hex 24 # DB password
openssl rand -hex 24 # Redis password
openssl rand -hex 32 # (optional) Redis session password
The Docker Compose Stack
Create .env beside compose.yaml:
# .env โ keep OUT of git (or use dotenvx per kb/sysadmin/dotenvx)
MYSQL_ROOT_PASSWORD=REPLACE_WITH_HEX_1
MYSQL_PASSWORD=REPLACE_WITH_HEX_2
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud
REDIS_HOST_PASSWORD=REPLACE_WITH_HEX_3
NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com
NEXTCLOUD_ADMIN_USER=admin # optional: pre-seed admin
NEXTCLOUD_ADMIN_PASSWORD=REPLACE_WITH_HEX_4
Create compose.yaml:
services:
db:
image: mariadb:11
command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW
restart: unless-stopped
volumes:
- db:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass ${REDIS_HOST_PASSWORD}
volumes:
- redis:/data
app:
image: nextcloud:29-apache
restart: unless-stopped
ports:
- "127.0.0.1:8080:80" # bind to loopback only โ proxy terminates TLS
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
volumes:
- nextcloud:/var/www/html # code+config+custom apps
- nextcloud_data:/var/www/html/data # user files (back THIS up)
environment:
- MYSQL_HOST=db
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- REDIS_HOST=redis
- REDIS_HOST_PASSWORD=${REDIS_HOST_PASSWORD}
- NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_TRUSTED_DOMAINS}
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER} # skips wizard if set
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
- PHP_MEMORY_LIMIT=512M
- PHP_UPLOAD_LIMIT=10G
volumes:
db:
redis:
nextcloud:
nextcloud_data:
Bring it up:
docker compose up -d
docker compose logs -f app # wait for "Apache/2.4.x configured"
What Happens: on first boot, the entrypoint waits for MariaDB to accept connections, creates the schema, seeds the admin user from env vars, writes /var/www/html/config/config.php, then starts Apache. First page load takes 30โ60s while caches warm.
Reverse Proxy + HTTPS
Expose only via TLS. Traefik example (see traefik-setup once written):
# added to the app service
labels:
- "traefik.enable=true"
- "traefik.http.routers.nextcloud.rule=Host(`cloud.example.com`)"
- "traefik.http.routers.nextcloud.entrypoints=websecure"
- "traefik.http.routers.nextcloud.tls.certresolver=le"
- "traefik.http.middlewares.nextcloud-hsts.headers.stsSeconds=15552000"
- "traefik.http.middlewares.nextcloud-hsts.headers.stsIncludeSubdomains=true"
- "traefik.http.routers.nextcloud.middlewares=nextcloud-hsts"
Then remove the ports: mapping entirely โ traffic flows only through the proxy network.
Caddy equivalent is two lines in a Caddyfile:
cloud.example.com {
reverse_proxy 127.0.0.1:8080
}
Verify: curl -I https://cloud.example.com โ HTTP/2 200, and the login page loads with a valid certificate.
Post-Install Configuration
Exec into the container and use occ โ Nextcloud's CLI (all admin tasks scriptable):
alias occ='docker compose exec -u www-data app php occ'
# 1. Background jobs via cron mode (not AJAX!)
occ background:cron
# 2. Verify redis is used for locking (should show Redis, not Database)
occ config:system:get memcache.locking
# 3. Add server's own IP to trusted proxies if behind local proxy
occ config:system:set trusted_proxies 0 --value=172.18.0.0/16
# 4. Pretty URLs
occ config:system:set overwrite.cli.url --value=https://cloud.example.com
occ config:system:set htaccess.RewriteBase --value=/
occ maintenance:update:htaccess
# 5. Maintenance window for housekeeping
occ config:system:set maintenance_window_start --type=integer --value=2
Add a host cron job for the 5-minute background worker:
sudo crontab -u www-data -e
# add:
*/5 * * * * docker compose -f /home/YOU/cloud-stack/compose.yaml exec -T -u www-data app php cron.php
What Happens: without cron mode, expensive jobs (file scanning, trash cleanup) run when a random user loads a page โ causing mysterious slowdowns. With cron, they run at 02:00โ04:00 (maintenance_window_start).
Recommended privacy opt-outs
occ config:system:set lookup_server --value="" # no global user directory
occ config:system:set default_phone_region --value="US" # silence setup warning
occ app:disable survey_client recommendations # telemetry apps
occ config:system:set updatechecker --value=false # if you patch via watchtower instead
User Management Basics
occ user:add alice # interactive password set
occ user:add --password-from-env bob # scripted creation
occ group:add family
occ group:adduser family alice
occ user:setting alice settings email alice@example.com
# Quotas โ critical on finite disks:
occ user:setquota alice 100 GB
occ group:setquota family 500 GB
Full web UI management (sharing defaults, federation) continues in nextcloud-clients.
External Storage Preview
Nextcloud can mount S3/SMB/WebDAV locations into users' file trees. Quick sanity check that it works โ enable the app and list mount types:
occ app:enable files_external
occ files_external:list
# Full MinIO integration comes in lesson minio-bucket-management
Upgrades That Don't Lose Data
Because all state lives in volumes, upgrading is version-bump + recreate:
# 0. BACKUP FIRST (lesson restic-setup automates this):
docker compose exec -u www-data app php occ maintenance:mode --on
docker run --rm -v cloud_nextcloud:/src -v $PWD/bak:/dst alpine tar czf /dst/nc-data.tgz -C /src .
docker compose exec db sh -c 'exec mysqldump --single-transaction -unextcloud -p"$MYSQL_PASSWORD" nextcloud' > bak/nc-db.sql
docker compose exec -u www-data app php occ maintenance:mode --off
# 1. Bump one major version at a time (28โ29โ30), never skip
sed -i 's/nextcloud:29-apache/nextcloud:30-apache/' compose.yaml
docker compose up -d
docker compose exec -u www-data app php occ upgrade
docker compose exec -u www-data app php occ status
Common Gotchas & Troubleshooting
"Trusted domain" error on first login.
NEXTCLOUD_TRUSTED_DOMAINS must match exactly what's in your browser bar. Fix live: occ config:system:set trusted_domains 1 --value=cloud.example.com
502 from the proxy but curl localhost:8080 works inside host.
The proxy container isn't on the same Docker network as app. Attach it: docker network connect <proxy-net> cloud-stack-app-1.
Uploads fail over ~100MB through the proxy.
Proxy body-size limit. Nginx: client_max_body_size 10G;. Also confirm PHP_UPLOAD_LIMIT took effect: occ config:system:get upload_max_filesize... actually check PHP directly: php -i | grep upload_max.
Login loops after restart.
Almost always Redis auth mismatch โ the app container can't reach redis with the configured password. Compare config.php redis => password against .env.
Slow page loads, DB CPU pegged.
Missing indexes after big import: occ db:add-missing-indices. And confirm background mode is cron (occ background:cron).
Where did my files go on disk?
Named volume path on host: docker volume inspect cloud-stack_nextcloud_data โ typically /var/lib/docker/volumes/cloud-stack_nextcloud_data/_data. Never edit files there manually โ use WebDAV/occ to avoid DB drift.
Practical Exercises
- Deploy the stack and create two users with quotas (one 5GB, one unlimited).
- Break it on purpose: stop the
dbcontainer and observe the error page โ then restore and verify recovery. - Move the data directory off Docker volumes onto a dedicated ZFS/LVM dataset, using the documented
maintenance:modeโ copy โ updatedatadirectoryprocedure. - Time an end-to-end upgrade backup+restore drill; record how long a full restore takes (you'll need that number in lesson disaster-recovery-testing).
- Set up a second instance on port 8081 for staging experiments.
๐ Related
- Previous: cloud-storage-concepts โ why sync โ backup
- Next: nextcloud-clients โ desktop/mobile sync setup
- nextcloud-collaboration โ OnlyOffice, Talk, Deck
- restic-setup โ backing this deployment up properly
- kb/containers/docker-compose-patterns โ compose patterns used above
- kb/networking/reverse-proxy-introduction โ proxy fundamentals
- kb/security/tls-configuration โ cert details
- kb/sysadmin/secrets โ managing the
.envsecrets safely
Change Log
- 2026-08-14 โ Initial publication as part of the cloud course build-out.