When teams outgrow consumer cloud drives, they usually need three things at once: secure document sharing, predictable storage costs, and stronger control over access policies. Nextcloud is a practical answer, but many first deployments fail in production because operators treat it like a single-container app instead of a full stack with state, caching, TLS, and background jobs. This guide shows a production-oriented deployment of Nextcloud using Docker Compose, NGINX, MariaDB, and Redis on Ubuntu, with concrete hardening and operational checks you can use in real environments.
The workflow below is built for small-to-mid sized organizations running 20 to 500 users, where uptime and data integrity matter more than “quick demo” setup speed. You’ll deploy isolated services on a private Docker network, terminate TLS at NGINX, enforce strict secret handling through an environment file, and validate file locking/caching with Redis so large uploads and concurrent edits remain stable. You’ll also get a troubleshooting section for the issues that show up most often during week one of operation.
Architecture and flow overview
This deployment uses four containers: nextcloud-app (PHP/Apache application), mariadb (primary relational datastore), redis (distributed cache and file locking), and nginx (reverse proxy/TLS entrypoint). End users connect only to NGINX over HTTPS. NGINX forwards requests to the Nextcloud app over the internal Docker network; Nextcloud reads/writes metadata in MariaDB and uses Redis for lock coordination and cache acceleration.
Why this shape works in production: it separates edge concerns (TLS, request limits, headers) from application/runtime concerns, and it keeps stateful services explicit so backups and upgrades can be managed independently. You can roll proxy and app changes with lower risk, while keeping database and data volumes controlled by policy.
# Logical traffic flow
# Browser -> NGINX:443 -> Nextcloud app:80
# Nextcloud -> MariaDB:3306
# Nextcloud -> Redis:6379
If the copy button is unavailable in your browser, manually select the command block and copy it.
Prerequisites
- Ubuntu 22.04/24.04 host with at least 4 vCPU, 8 GB RAM, and SSD storage.
- A fully qualified domain name (for example
cloud.example.com) with A/AAAA DNS records pointed at your server. - Docker Engine + Docker Compose plugin installed.
- Ports 80 and 443 open to the internet (or your edge load balancer).
- A non-root sudo user for operations and a backup destination for DB + data volumes.
# Verify host baseline
uname -a
lsb_release -a
docker --version
docker compose version
free -h
df -h
If one-click copy is blocked by your theme/editor, manually copy commands from the block.
Step-by-step deployment
Step 1: Prepare directories and permissions
Create a dedicated stack directory and persistent volume mounts. Keep app code/config, database storage, and user files separate so backup and restore operations remain predictable.
sudo mkdir -p /opt/nextcloud/{app,db,redis,nginx/conf.d,data}
sudo chown -R $USER:$USER /opt/nextcloud
cd /opt/nextcloud
If copy doesn’t work, select this block and copy manually.
Step 2: Create environment file for secrets and runtime values
Do not hardcode credentials in docker-compose.yml. Store secrets in .env, lock permissions, and keep it out of Git. Use long random strings for database and admin credentials.
cat > /opt/nextcloud/.env <<'EOF'
DOMAIN=cloud.example.com
TZ=UTC
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud
MYSQL_PASSWORD=replace-with-32-char-random-secret
MYSQL_ROOT_PASSWORD=replace-with-40-char-root-secret
NEXTCLOUD_ADMIN_USER=ncadmin
NEXTCLOUD_ADMIN_PASSWORD=replace-with-strong-admin-password
REDIS_HOST=redis
EOF
chmod 600 /opt/nextcloud/.env
Manual copy fallback: highlight the command block and copy directly.
Step 3: Write Docker Compose stack
This compose file pins service responsibilities and keeps each container restartable in isolation. MariaDB and Redis stay internal-only. NGINX is the only internet-facing component.
cat > /opt/nextcloud/docker-compose.yml <<'EOF'
services:
mariadb:
image: mariadb:11
container_name: nc-mariadb
restart: unless-stopped
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
env_file: .env
environment:
- MARIADB_DATABASE=${MYSQL_DATABASE}
- MARIADB_USER=${MYSQL_USER}
- MARIADB_PASSWORD=${MYSQL_PASSWORD}
- MARIADB_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- TZ=${TZ}
volumes:
- ./db:/var/lib/mysql
networks: [nextcloud_net]
redis:
image: redis:7-alpine
container_name: nc-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- ./redis:/data
networks: [nextcloud_net]
app:
image: nextcloud:29-apache
container_name: nc-app
restart: unless-stopped
depends_on: [mariadb, redis]
env_file: .env
environment:
- MYSQL_HOST=mariadb
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- REDIS_HOST=${REDIS_HOST}
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
- NEXTCLOUD_TRUSTED_DOMAINS=${DOMAIN}
- TZ=${TZ}
volumes:
- ./app:/var/www/html
- ./data:/var/www/html/data
networks: [nextcloud_net]
nginx:
image: nginx:1.27-alpine
container_name: nc-nginx
restart: unless-stopped
depends_on: [app]
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/certs:/etc/nginx/certs:ro
networks: [nextcloud_net]
networks:
nextcloud_net:
driver: bridge
EOF
If clipboard integration is stripped, manually copy this code block.
Step 4: Configure NGINX reverse proxy
Use a dedicated server block with secure TLS defaults, larger client body size for uploads, and forwarding headers so Nextcloud sees the original protocol and client IP. If you terminate TLS elsewhere, keep equivalent headers at your edge.
cat > /opt/nextcloud/nginx/conf.d/nextcloud.conf <<'EOF'
server {
listen 80;
server_name cloud.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name cloud.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
client_max_body_size 2G;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
location / {
proxy_pass http://app:80;
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 https;
}
}
EOF
Manual-copy fallback: select and copy the block if the button does not respond.
Step 5: Launch and initialize
Start MariaDB and Redis first, then app/proxy. After first boot, complete any remaining setup from the web installer if prompted. In most cases, the environment variables will pre-seed admin/database settings.
cd /opt/nextcloud
docker compose pull
docker compose up -d
sleep 10
docker compose ps
docker compose logs --tail=80 app nginx mariadb redis
If copy UX is unavailable, copy commands manually from this code block.
Configuration and secrets handling best practices
After the stack is live, configure Nextcloud for reverse-proxy awareness and Redis locking. Keep these values in config.php under configuration management and avoid ad-hoc edits through web consoles where possible. At minimum, ensure overwriteprotocol is set to HTTPS and trusted domains are strict. Add Redis settings for file locking consistency under concurrent upload and WebDAV traffic.
docker exec -u www-data nc-app php occ config:system:set trusted_domains 1 --value="cloud.example.com"
docker exec -u www-data nc-app php occ config:system:set overwriteprotocol --value="https"
docker exec -u www-data nc-app php occ config:system:set memcache.local --value="\\OC\\Memcache\\APCu"
docker exec -u www-data nc-app php occ config:system:set memcache.locking --value="\\OC\\Memcache\\Redis"
docker exec -u www-data nc-app php occ config:system:set redis host --value="redis"
Manual-copy fallback: highlight this block and copy the commands directly.
For secret rotation, rotate database and admin passwords on a schedule, then update container env files and restart services in sequence (database, app, proxy). Back up /opt/nextcloud/.env securely in your secret manager, not in plain CI logs. If you use external object storage or SMTP credentials, inject them through environment variables or mounted secret files with least privilege and strict filesystem ACLs.
Verification checklist
Validation should cover availability, security headers, background jobs, and application health—not just a successful login. Run the checks below after deployment and after each upgrade.
# Endpoint and TLS checks
curl -I https://cloud.example.com
# Container health and logs
docker compose ps
docker compose logs --tail=100 app mariadb redis nginx
# Nextcloud health and background jobs
docker exec -u www-data nc-app php occ status
docker exec -u www-data nc-app php occ background:cron
docker exec -u www-data nc-app php occ db:add-missing-indices
docker exec -u www-data nc-app php occ maintenance:repair
If the copy button is blocked, manually copy this checklist from the code area.
Expected outcomes: HTTPS responds with a valid certificate chain, HSTS is present, app logs show no persistent DB connection failures, Redis locking errors are absent, and occ status returns installed=true with a stable version string. During load tests (for example, simultaneous uploads), latency should stay predictable and no repeated lock-timeout warnings should appear.
Common issues and fixes
1) “Access through untrusted domain” warning
Cause: trusted domains mismatch between DNS and Nextcloud config. Fix by adding all canonical hostnames through occ config:system:set trusted_domains and avoid wildcard domains unless absolutely necessary.
2) Large file uploads fail with 413 Request Entity Too Large
Cause: proxy body limits too small. Increase client_max_body_size in NGINX and confirm PHP upload/post limits in the app container. Restart services and retest with a known large file.
3) Background tasks not running
Cause: cron mode not configured or cron not scheduled. Set background jobs to cron mode and schedule the container cron runner. Avoid AJAX mode in production because execution frequency depends on interactive user traffic.
4) Slow previews and dashboard delays
Cause: missing cache/locking settings, underprovisioned CPU, or cold storage. Confirm Redis integration, tune PHP workers, and monitor container CPU/memory pressure. For very large libraries, evaluate object storage and dedicated preview generation windows.
5) Database lock waits during peak traffic
Cause: undersized MariaDB resources and long-running transactions. Ensure SSD-backed storage, tune InnoDB buffer settings for available RAM, and review slow query logs before increasing concurrency caps.
6) Upgrade failures from jumping too many versions
Cause: skipping supported upgrade paths. Follow sequential major/minor upgrades, snapshot volumes before each step, and validate with occ upgrade plus post-upgrade smoke tests.
FAQ
Should I run Nextcloud and MariaDB on the same VM in production?
For small and medium workloads, yes—if the VM is sized properly and has fast SSD storage. For larger deployments or strict compliance boundaries, split database and app layers to reduce blast radius and improve scaling flexibility.
Do I need Redis if the app already works without it?
You can start without Redis in test environments, but production should use it for file locking and cache consistency. Without Redis, concurrent edits and WebDAV operations are more likely to show lock contention and degraded responsiveness.
How should I back up this stack?
Back up both database and file data on a schedule, and test restore procedures monthly. A practical baseline is nightly MariaDB dumps plus incremental snapshots of /opt/nextcloud/data and configuration volumes.
What is the safest way to perform upgrades?
Take snapshots first, pull tested images, put Nextcloud in maintenance mode, run upgrade commands, then validate end-to-end login/upload/share workflows before exiting maintenance mode. Keep rollback artifacts until verification passes.
Can I use an external object store later?
Yes. Many teams start with local volumes and migrate to S3-compatible object storage as data grows. Plan migration windows carefully, benchmark latency, and verify retention/versioning policies before cutover.
How do I enforce stronger security for internet exposure?
Use TLS 1.2/1.3 only, strict firewall policies, fail2ban or equivalent controls, multi-factor authentication, and regular dependency/image patching. Pair this with centralized logs and alerting for auth anomalies.
Related internal guides
- Production Guide: Deploy Gitea with Docker Compose + Traefik + PostgreSQL on Ubuntu
- Production Guide: Deploy Outline Wiki with Docker Compose + Caddy + PostgreSQL on Ubuntu
- Production Guide: Deploy Uptime Kuma with Docker Compose + NGINX + PostgreSQL on Ubuntu
Talk to us
If you want support designing or hardening your collaboration platform, we can help with architecture, migration planning, and production readiness.