How To Self-Host Nextcloud With Docker Compose on Ubuntu
Tricknowtech Team 11 min read
Goal
In this tutorial, you will deploy Nextcloud and a MariaDB database as separate Docker containers using Docker Compose, complete the web-based admin setup, verify data persistence, and optionally reverse-proxy the instance through Nginx with a Let's Encrypt certificate.
Prerequisites
A server/VPS running Ubuntu 22.04 or 24.04 with a non-root user in the sudo group
Docker Engine and the Docker Compose plugin already installed and running
(Recommended, not required) A registered domain pointed at the server's IP, with Nginx and Certbot already set up for HTTPS reverse-proxying
Basic comfort with the Linux command line and a text editor
Let an AI agent do this for you
Copy a ready-made prompt for an AI coding assistant with terminal access to your server (Claude Code, Cursor, or similar) — it can carry out the steps below for you. Review what it plans to run before it executes anything.
Nextcloud is a self-hosted alternative to Dropbox or Google Drive: file sync and sharing, calendars, contacts, and a growing app ecosystem, all running on infrastructure you control. The official Nextcloud Docker image ships as a two-container pattern — the PHP application and a separate MariaDB database — wired together with Docker Compose and backed by named volumes so your data survives container restarts and image upgrades.
This tutorial deploys that stack on a single Ubuntu server, walks through the first-run setup wizard, and covers the optional (but recommended for anything beyond local testing) step of putting it behind an Nginx reverse proxy with a real TLS certificate.
Step 1 — Create a project directory
Docker Compose projects work best as one directory per stack, holding the compose file and any supporting config. Use /opt for service data that isn't tied to a specific user account:
bash
mkdir -p /opt/nextcloud && cd /opt/nextcloud
Step 2 — Write the Docker Compose configuration
Create docker-compose.yml in this directory. This follows the pattern documented by Nextcloud's own official image: an app container and a db container on the same Compose network, each with a named volume so data persists independently of the container's lifecycle. The app container talks to the database using the hostname db — Docker Compose's built-in DNS resolves that to the db service automatically, no manual networking required.
“Replace both password placeholders with real, unique, strong values before running this anywhere but a throwaway test box. Never commit a docker-compose.yml containing real passwords to a public (or even private) git repository.”
A cleaner practice than hardcoding passwords in the compose file is to keep them in a separate .env file in the same directory and reference them with Compose's built-in ${VARIABLE} substitution — Compose loads .env automatically, keeping secrets out of version control if you add .env to .gitignore:
Tricknowtech VPS Hosting
Dedicated KVM resources and full root access — deployed in under 60 seconds, no ticket required.
The .env file holds real database passwords, so restrict its permissions as shown above and keep it out of any repository. Then reference MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} and MYSQL_PASSWORD=${MYSQL_PASSWORD} in the compose file instead of the literal values. This step is optional — the plain compose file above works identically — but it's the better habit once this becomes a real deployment.
Step 3 — Start the stack
bash
docker compose up -d
Compose pulls both images, creates the two named volumes, and starts db before app (the depends_on directive controls start order, though not full database-readiness — the Nextcloud image itself retries the database connection on boot, so a brief restart loop in the app container's first few seconds is normal). Confirm both containers are running:
bash
docker compose ps
You should see db and app both listed as Up. If you want to watch Nextcloud finish its own initialization, tail its logs:
bash
docker compose logs -f app
Step 4 — Complete the Nextcloud setup wizard
Open a browser to the server's IP address on port 8080 — for example http://203.0.113.10:8080, substituting your actual server IP (or, if you've already set up a reverse proxy and domain per Step 5, use the domain instead). This loads Nextcloud's web-based first-run screen.
You're prompted to create the initial admin account: choose a username and password here. This account is completely separate from the MySQL credentials set in Step 2 — it's what you'll actually log into the web UI with.
Step 5 — (Recommended) Reverse-proxy through Nginx with HTTPS
Accessing Nextcloud directly on port 8080 over plain HTTP is fine for local testing, but file-sync credentials and data shouldn't travel unencrypted, and exposing a nonstandard port directly to the internet is more surface area than necessary. If you already have Nginx and Certbot set up for reverse-proxying (as in an earlier Nginx/Let's Encrypt tutorial), add a server block for your Nextcloud domain following that same pattern, pointing at the app container's published port on localhost:
Test and reload Nginx, then run Certbot for the domain the same way you would for any other reverse-proxied site — it rewrites this block to add the TLS listener and redirect HTTP to HTTPS:
Nextcloud itself doesn't know it's sitting behind a proxy yet, and by default it rejects requests for any hostname it doesn't recognize. Tell it about the domain and that the outer connection is HTTPS, using Nextcloud's occ command-line tool inside the app container:
Log in to the Nextcloud web UI with the admin account created in Step 4. Upload a test file through the Files app, then confirm the data actually lives in the named volume — not just in the running container's writable layer — by restarting the stack:
bash
docker compose restart
Refresh the Files app once the containers are back up. The test file should still be there. If it is, both nextcloud_data and db_data are correctly persisting outside the container lifecycle, which is the whole point of using named volumes instead of storing state inside the container image.
Step 7 — Troubleshooting: "Access through untrusted domain"
This is the single most common error when reverse-proxying Nextcloud: you configured Nginx and Certbot correctly, but visiting https://example.com shows Nextcloud's own error page refusing the connection. It means the hostname you're using to reach Nextcloud isn't in its trusted_domains list — a security check baked into Nextcloud itself, independent of anything Nginx does.
Fix it with the occ command shown in Step 5 (adjust the index and value if you're adding an additional domain rather than the first one):
Look for the 'trusted_domains' array and add the domain as a new entry, matching the existing array syntax. Save, exit the container shell, and reload the page — the error should be gone. If you're still seeing the warning after this, double-check that overwriteprotocol is set to https and that Nginx is actually forwarding the X-Forwarded-Proto header shown in the config block above; without it, Nextcloud can't tell the outer connection is encrypted even once the domain itself is trusted.