Nextcloud
Nextcloud holds documents, calendars and contacts, and it syncs them to desktops and phones. It is the stack in this Blueprint with the most moving parts: an application container, a separate web server, a database, a cache, and a background worker.
Verified 29 Jul 2026 against 34.0.2-fpm-alpine · data restored from a backup on a live host
Not exercised: desktop and mobile client sync.
Needs: Traefik running, a domain, and SMTP credentials.
Installation
Section titled “Installation”There is no setup wizard. The stack installs itself from environment variables and secret files — repeatable, reviewable, and with no unauthenticated setup form reachable in between.
From apps/nextcloud/:
-
Terminal window cp .env.example .env -
Nextcloud has to be told which address Traefik talks to it from, or it will think every visitor is Traefik. Print it:
Terminal window docker network inspect proxy-public \--format '{{range .IPAM.Config}}{{.Subnet}} {{end}}'Copy whatever that prints into
NC_TRUSTED_PROXIES— all of it. If you enabled IPv6 there will be two entries, and leaving one out means everyone shares a single address as far as the login protection is concerned: one person mistyping a password locks out everybody.APP_TRAEFIK_HOST=cloud.example.comNC_TRUSTED_PROXIES=172.30.0.0/16TZ=Europe/Vienna -
Set mail. Use port 465 — it is encrypted from the first byte, where port 587 starts unencrypted and switches over partway:
SMTP_HOST=smtp.example.comSMTP_PORT=465SMTP_SECURE=sslSMTP_NAME=your-smtp-loginMAIL_FROM_ADDRESS=nextcloudMAIL_DOMAIN=example.comNote that
MAIL_FROM_ADDRESSis only the part before the@. Together withMAIL_DOMAINabove it makesnextcloud@example.com, which has to be an address your mail provider has verified for sending. -
Create the secrets. All six before the first start:
Terminal window mkdir -p .secretsopenssl rand -base64 32 | tr -d '\n' > .secrets/db_pwd.txtopenssl rand -base64 32 | tr -d '\n' > .secrets/db_root_pwd.txtopenssl rand -hex 32 | tr -d '\n' > .secrets/redis_pwd.txtprintf 'admin' > .secrets/admin_user.txtopenssl rand -base64 24 | tr -d '\n' > .secrets/admin_pwd.txtprintf 'your-smtp-key' > .secrets/smtp_pwd.txtchmod 600 .secrets/*.txtTwo of the six are read by Nextcloud itself while it runs, so it needs permission for those:
Terminal window sudo chown "$USER":82 .secrets/redis_pwd.txt .secrets/smtp_pwd.txtchmod 640 .secrets/redis_pwd.txt .secrets/smtp_pwd.txtThis keeps the files editable for you as well, which matters the day you change a password.
-
Read the administrator password once and store it in your password manager — it is generated, never displayed:
Terminal window cat .secrets/admin_pwd.txt -
Start, and watch the install run:
Terminal window docker compose up -ddocker compose logs -f appExpect
Initializing nextcloudfollowed byNextcloud was successfully installed. The database has a health check the application waits on, so the first start takes a minute.
Verify
Section titled “Verify”docker compose psdocker compose exec -u www-data app php occ statusdocker compose exec -u www-data app php occ config:system:get trusted_proxiesExpected: every container healthy, installed: true, and your proxy subnets
listed.
Then log in and open /settings/admin/overview. That page is the acceptance
test — it runs the project’s own checks against the running instance and it is
more thorough than anything written here. Work until it reports no warnings.
Send a test message before handing the instance to anyone:
docker compose exec -u www-data app php occ user:welcome adminHarden before the first user
Section titled “Harden before the first user”Run these once. Each is occ config:system:set, so re-running is harmless.
-
Set the region, so phone numbers in contacts are parsed:
Terminal window docker compose exec -u www-data app php occ \config:system:set default_phone_region --value="AT" -
Move background jobs to a quiet hour — the value is an hour, UTC:
Terminal window docker compose exec -u www-data app php occ \config:system:set maintenance_window_start --value=1 --type=integer -
Stop new accounts being seeded with example files:
Terminal window docker compose exec -u www-data app php occ \config:system:set skeletondirectory --value="" -
Bound preview generation. Thumbnails are produced by PHP workers; without a limit, one large image occupies a worker long enough for users to notice:
Terminal window docker compose exec -u www-data app php occ \config:system:set preview_max_x --value=2048 --type=integerdocker compose exec -u www-data app php occ \config:system:set preview_max_y --value=2048 --type=integerdocker compose exec -u www-data app php occ \config:system:set preview_max_filesize_image --value=25 --type=integer -
Restrict the administration settings to the networks you administer from. Everything else stays reachable; only
/settings/adminnarrows:Terminal window docker compose exec -u www-data app php occ \config:system:set allowed_admin_ranges 0 --value="100.64.0.0/10"docker compose exec -u www-data app php occ \config:system:set allowed_admin_ranges 1 --value="fd7a:115c:a1e0::/48"Those two ranges are Tailscale’s. Substitute your own, and confirm you can still reach the page from where you work before you rely on it.
Going further
Section titled “Going further”Opening it up
Section titled “Opening it up”The stack ships APP_TRAEFIK_ACCESS=acc-private — reachable from your LAN and
VPN, and from the Docker gateway, which the instance needs to run its own setup
checks. Switch to acc-public once it is configured, verified, and you have
logged in successfully. That is a deliberate change to your attack surface.
An office server such as OnlyOffice calls back over the public domain and
requires acc-public.
Rotating a key
Section titled “Rotating a key”Most editors save by writing a new file and renaming it over the old one. The
container’s mount still points at the file that was replaced, so it keeps reading
the previous value and restart does not help:
docker compose up -d --force-recreate app cronWriting in place — printf '%s' "$KEY" > .secrets/smtp_pwd.txt — avoids this.
Backup
Section titled “Backup”Nextcloud keeps its file index in the database and the files on disk. Capture the
two at different moments under load and the restore shows files the index does
not know about, or index entries pointing at nothing. occ files:scan repairs the
first case and not the second. This is the stack in the Blueprint where quiescing
genuinely matters.
Three things must be captured together — a database dump alone restores an instance that cannot read its own files:
| Where | |
|---|---|
| Database | dumped through Borgmatic’s hook, not copied |
Data directory and config/config.php |
the nextcloud_html volume |
.env and .secrets/ |
the deployment directory |
config.php carries the instance secret and the password salt. Restoring the
database against a regenerated config invalidates sessions and can make encrypted
content unreadable.
What to add to /etc/borgmatic/config.yaml
Section titled “What to add to /etc/borgmatic/config.yaml”-
The data. Deliberately not the database volume — the dump below is the consistent copy, the raw files would be a torn one:
source_directories:- /srv/blueprint- /var/lib/docker/volumes/nextcloud_nextcloud_html/_dataConfirm the volume’s real name with
docker volume ls; it is prefixed with your Compose project name. -
The database. The password is read from the same file the stack already mounts as a secret, so there is no second copy to rotate:
mariadb_databases:- name: nextcloudcontainer: nextcloud-dbusername: nextcloudpassword: "{credential file /srv/blueprint/apps/nextcloud/.secrets/db_pwd.txt}"tls: false -
Maintenance mode around the capture. The error hook is what stops a failed run leaving the instance offline:
commands:- before: actionwhen: [create]run:- docker exec -u www-data nextcloud-app php occ maintenance:mode --on- after: actionwhen: [create]run:- docker exec -u www-data nextcloud-app php occ maintenance:mode --off- after: errorrun:- docker exec -u www-data nextcloud-app php occ maintenance:mode --off -
Validate, then run it once by hand:
Terminal window sudo borgmatic config validatesudo borgmatic create --verbosity 1 --stats
Restoring this stack
Section titled “Restoring this stack”Database first, then the file tree, then the application — and leave maintenance mode on until both halves are in place.
The flag itself is inside the backup. Any archive taken while the quiescing hook
was active holds 'maintenance' => true in config.php, so a restored instance
refuses to serve until you clear it:
docker compose exec -u www-data app php occ maintenance:mode --offUpdates
Section titled “Updates”Nextcloud upgrades one major version at a time. Skipping a version is not supported, and the web updater is disabled in this stack in favour of replacing the image.
- Read the release notes for the version you are moving to.
- Back up, and confirm the backup is readable.
- Raise
APP_TAGin.envby one major version. -
Terminal window docker compose pulldocker compose up -ddocker compose logs -f app - Check
occ statusforneedsDbUpgrade: false, then re-open/settings/admin/overview.
Troubleshooting
Section titled “Troubleshooting”Permission denied reading a secret, while the file looks correct on the
host — the file was replaced by an editor. Recreate the containers rather than
restarting them.
Mail silently does not send, occ reports NOAUTH — the web user cannot
read redis_pwd.txt or smtp_pwd.txt. Check the group ownership from the
installation steps. The container still reports healthy in this state.
Brute-force protection locks out everyone at once — NC_TRUSTED_PROXIES is
incomplete, so every request appears to come from the proxy. Add the missing
subnet, including the IPv6 one.
Setup checks fail to reach the instance itself — with acc-tailscale the
Docker gateway is not permitted. acc-private covers it.
Repository files
Section titled “Repository files”apps/nextcloud/README.md— architecture, network exception, tuningapps/nextcloud/.env.exampleapps/nextcloud/UPSTREAM.md— pinned versions and upgrade notes
Official sources
Section titled “Official sources”© 2026 SecDockBlueCode Apache-2.0Site content licence not yet decided