Nextcloud is the free, open-source, self-hosted alternative to Google Drive, Dropbox and OneDrive, and then some. File sync and sharing with desktop and phone apps, calendars, contacts, notes, tasks and hundreds of add-on apps, all on your own server. Pair it with ONLYOFFICE (see my ONLYOFFICE guide) and you get online document editing too.
In this guide we will run Nextcloud with Docker Compose using the official Docker Hub image, following the project's own nextcloud/docker examples: Nextcloud with Apache, MariaDB, Redis for caching and file locking, and a dedicated cron container for background jobs.
nextcloud:35-apache) because Nextcloud can only be upgraded one major version at a time.✅ Before you start
- A Linux host with Docker and Docker Compose v2 installed (
docker compose versionshould work). Usedocker composewith a space: the old Pythondocker-composev1 can crash on modern Docker. - At least 2 GB of RAM and plenty of disk for your files.
- A domain name and a reverse proxy with HTTPS. Nextcloud really wants to run over HTTPS, and the phone apps need it.
- One free port,
8081, for the web UI before the proxy is in place.
STEP 1: User and group IDs (and your secrets)
The official Nextcloud image does not use PUID/PGID. Apache runs as www-data (user and group 33) and the image sets up its own folder permissions on first start.
What you do need are three passwords: one for the MariaDB root user, one for the Nextcloud database user, and one for your Nextcloud admin account. Generate them like this (run it three times):
openssl rand -base64 24Keep them somewhere safe, such as your password manager. In the examples below they appear as placeholders like CHANGE_ME_STRONG_PASSWORD and change-me.
STEP 2: Create the folders
I keep every app's files under one parent folder so a backup is a single job. Create the folders for Nextcloud:
mkdir -p /path/to/appdata/nextcloud/{html,db}When you are done, the layout should look like this:
/path/to/appdata/nextcloud/
docker-compose.yml
db.env # database credentials
html/ # Nextcloud itself, config.php, apps and (by default) your files
db/ # the MariaDB databaseFolder layout
Finally, I give every web app that sits behind my reverse proxy one shared Docker network called proxy. Nextcloud joins it so Traefik or Nginx Proxy Manager can reach it by container name, while its database and helpers stay on the stack's own private network. Create it once (if it already exists, Docker just tells you so):
docker network create proxySTEP 3: The compose file
First create /path/to/appdata/nextcloud/db.env with the database details. Both the database and Nextcloud read it, so the values only live in one place:
MYSQL_PASSWORD=CHANGE_ME_STRONG_PASSWORD
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextclouddb.env
Create /path/to/appdata/nextcloud/docker-compose.yml and paste in the following. It is the official MariaDB + Apache example from nextcloud/docker, with bind mounts, the admin account, trusted domains and the shared proxy network added. Use the copy button in the top right corner of the block.
services:
db:
image: mariadb:lts
container_name: nextcloud-db
restart: always
command: --transaction-isolation=READ-COMMITTED
volumes:
- /path/to/appdata/nextcloud/db:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=CHANGE_ME_STRONG_PASSWORD # MariaDB root password
- MARIADB_AUTO_UPGRADE=1
- MARIADB_DISABLE_UPGRADE_BACKUP=1
env_file:
- db.env
redis:
image: redis:alpine
container_name: nextcloud-redis
restart: always
app:
image: nextcloud:35-apache
container_name: nextcloud
restart: always
ports:
- "8081:80"
volumes:
- /path/to/appdata/nextcloud/html:/var/www/html
environment:
- MYSQL_HOST=db
- REDIS_HOST=redis
- NEXTCLOUD_ADMIN_USER=admin
- NEXTCLOUD_ADMIN_PASSWORD=CHANGE_ME_STRONG_PASSWORD
- NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com 192.168.1.20
- TRUSTED_PROXIES=172.16.0.0/12 192.168.1.0/24
- OVERWRITEPROTOCOL=https
- APACHE_DISABLE_REWRITE_IP=1
env_file:
- db.env
depends_on:
- db
- redis
networks:
- default
- proxy
cron:
image: nextcloud:35-apache
container_name: nextcloud-cron
restart: always
volumes:
- /path/to/appdata/nextcloud/html:/var/www/html
entrypoint: /cron.sh
depends_on:
- db
- redis
networks:
proxy:
external: truedocker-compose.yml
STEP 4: The settings explained
| Setting | What it does |
|---|---|
mariadb:lts |
The database, on the current long-term-support release. --transaction-isolation=READ-COMMITTED is what Nextcloud requires. |
MARIADB_AUTO_UPGRADE |
Lets MariaDB upgrade its system tables itself when the image moves to a newer version. |
db.env |
Database name, user and password, read by both db (to create them) and app (to connect). |
redis |
Memory cache and transactional file locking. The image configures Nextcloud to use it automatically because REDIS_HOST is set. |
nextcloud:35-apache |
Nextcloud 35 with Apache built in. It follows 35.x updates but never jumps a major version on its own. |
/var/www/html |
Nextcloud's code, config/config.php, installed apps and, by default, everyone's files (in data/). Back up this folder and the database. |
NEXTCLOUD_ADMIN_USER / _PASSWORD |
Creates the admin account and runs the installer automatically on the first start. Only used once. |
NEXTCLOUD_TRUSTED_DOMAINS |
Space-separated list of hostnames Nextcloud will answer to. Add your proxy hostname and the host IP. |
TRUSTED_PROXIES |
The addresses of your reverse proxy, so Nextcloud trusts its forwarded headers. 172.16.0.0/12 covers Docker networks; adjust to your setup. |
OVERWRITEPROTOCOL |
Tells Nextcloud that users really reach it over https, even though the proxy talks to it over plain HTTP. |
APACHE_DISABLE_REWRITE_IP |
Stops Apache rewriting the client IP itself, so Nextcloud reads the forwarded host, protocol and client IP from the proxies listed in TRUSTED_PROXIES, as the image docs describe. |
cron |
A second container from the same image that runs /cron.sh: Nextcloud's background jobs every five minutes. |
The
nextcloud/docker repo has a Postgres version of this example: swap db for postgres:alpine (mount /var/lib/postgresql) and use POSTGRES_HOST, POSTGRES_DB, POSTGRES_USER and POSTGRES_PASSWORD instead of the MYSQL_* variables. Both work well.STEP 5: Start Nextcloud
From the folder containing your compose file:
cd /path/to/appdata/nextcloud
docker compose up -d
docker compose logs -fWait until the logs settle down (press Ctrl+C to stop following them; the container keeps running), then check it is up:
docker ps --filter name=nextcloudYou should see nextcloud, nextcloud-db, nextcloud-redis and nextcloud-cron. The first start copies Nextcloud into html/ and runs the installer, which can take a few minutes. Follow the logs until Apache reports it is ready to serve requests.
STEP 6: First-run setup
- Open
http://192.168.1.20:8081and log in asadminwith the password from the compose file. (Once the proxy is in place, always use your HTTPS hostname instead.) - Go to Administration settings → Basic settings → Background jobs and select Cron. The
croncontainer is already running the jobs; this tells Nextcloud to rely on it instead of running jobs on page loads (AJAX). - Open Administration settings → Overview. Nextcloud runs security and setup checks there; work through anything it flags.
- Create user accounts under Accounts, then install the desktop and phone apps.
- Browse Apps for Calendar, Contacts, Notes, Deck and the ONLYOFFICE connector.
You will sometimes need Nextcloud's command-line tool, occ. Always run it as the web server user:
docker compose exec -u 33 app php occ status
docker compose exec -u 33 app php occ maintenance:repair --include-expensive
docker compose exec -u 33 app php occ db:add-missing-indicesSTEP 7: Put Nextcloud behind a reverse proxy
Use a dedicated hostname such as cloud.example.com (it must be in NEXTCLOUD_TRUSTED_DOMAINS). Uploads can be large, so make sure the proxy does not cap the request size.
Option A: Traefik (Docker labels)
If Traefik runs on the same Docker host and is attached to the proxy network, add these labels to the nextcloud service. They assume your HTTPS entry point is called websecure and your certificate resolver is called cloudflare; change them to match your Traefik config.
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=cloudflare
- traefik.http.services.nextcloud.loadbalancer.server.port=80
- traefik.docker.network=proxydocker-compose.yml (extra lines)
Option B: Traefik (file provider)
If Traefik runs on a different machine, add a router and service to your dynamic config file instead:
http:
routers:
nextcloud:
rule: Host(`cloud.example.com`)
entryPoints:
- websecure
tls:
certResolver: cloudflare
service: nextcloud
services:
nextcloud:
loadBalancer:
servers:
- url: http://192.168.1.20:8081dynamic.yml
Option C: Nginx Proxy Manager
Add a Proxy Host for cloud.example.com with scheme http, forward hostname nextcloud (if NPM is on the proxy network) or 192.168.1.20, and forward port 80 (or 8081 if you use the host IP). Request a certificate on the SSL tab and tick Force SSL. Under the Advanced tab add client_max_body_size 0; and proxy_buffering off; so big uploads work.
Put the labels on the app service. For CalDAV/CardDAV discovery (the "well-known" URLs), add a redirect for /.well-known/carddav and /.well-known/caldav to /remote.php/dav/ in your proxy; the Nextcloud overview page tells you if it is missing. The reverse proxy docs cover the details.
Once everything goes through the proxy, change the port mapping to "127.0.0.1:8081:80" so Nextcloud is not reachable over plain HTTP from the LAN.
Updating Nextcloud
Within a major version (35.x), updating is a pull and restart. The image runs the upgrade for you on start:
cd /path/to/appdata/nextcloud
docker compose pull
docker compose up -d
docker image prune -fimage: lines to nextcloud:36-apache, pull and start, and wait for the upgrade to finish before going further. Never skip a version (e.g. 34 to 36). Back up html/ and the database first, and check that your apps support the new version. Do not use the web updater inside Nextcloud with Docker.Troubleshooting
"Access through untrusted domain". The hostname is not in the trusted list. Add it with docker compose exec -u 33 app php occ config:system:set trusted_domains 2 --value=cloud.example.com. (NEXTCLOUD_TRUSTED_DOMAINS only applies on the first install.)
Login loops, or links point to http://. OVERWRITEPROTOCOL=https or TRUSTED_PROXIES is missing or wrong. Check config/config.php in html/.
"Last background job execution ran ... ago". The cron container is not running, or Background jobs is not set to Cron. Check docker logs nextcloud-cron.
Uploads fail with 413. Your proxy limits body size. Remove the limit as shown in STEP 7. (APACHE_BODY_LIMIT controls Apache's own limit inside the container.)
Everything is slow. Redis is not being used. Check that REDIS_HOST=redis is set and look for memcache.locking in config.php.
That's it! Your own cloud, on your own hardware, with nobody scanning your files. ☁️