Skip to content
Serverküche
Search

Loading search … (only available on the published site).

Applications Difficulty: Intermediate

Hardening & optimizing Nextcloud: clear every warning (part 2)

Get your Nextcloud admin overview green: set up HSTS headers, email sending, enforced two-factor auth and brute-force protection with real IPs.

· 13 min read ·Duration: approx. 45 minutes
Table of contents

Your Nextcloud is running – but the admin overview shows yellow warnings, there’s no working email and no second factor protects your accounts. In this second part we turn “running” into “cleanly secured and snappy”: we work through every warning until the overview is green.

What are we building?

By the end, the security & setup overview of your Nextcloud shows no more warnings. Concretely, we build on the installation from part 1 and add: the missing HSTS security header via Traefik, a configured maintenance window, working email sending (for password resets and notifications), enforced two-factor authentication and brute-force protection that sees the real attacker IPs – the basis for Fail2ban.

Everything is tested against Nextcloud 34.0.1 behind Traefik v3.7 on Debian 13 with PHP 8.5. This is legwork, but exactly the legwork missing in 90% of all Nextcloud guides – and it makes the difference between “somehow online” and “properly operated”.

Prerequisites

  • A running Nextcloud set up as in self-hosting Nextcloud (part 1): classic nextcloud image behind Traefik, with MariaDB, Redis and a cron sidecar. The container names from part 1 (nc-app, nc-db, nc-redis, nc-cron) and the Compose folder ~/nextcloud are the basis here.
  • SSH access to the server and the command-line tool occ – we call it as in part 1 in the app container: docker exec -u www-data nc-app php occ <command>.
  • A Traefik reverse proxy with the proxy network and the resolver le as in the tutorial reverse proxy with Traefik – we attach the security headers as a Traefik middleware.

Keeping occ in hand

occ is Nextcloud’s admin tool. Because the containers in part 1 got fixed names (container_name), you address the app container directly – no matter which directory you’re in:

Terminal
docker exec -u www-data nc-app php occ status

You should see installed: true and your version. We won’t abbreviate this prefix (docker exec -u www-data nc-app php occ …) in the following – so you can copy every command directly. Only the few docker compose … commands (like the container restart in step 3) you still run from the ~/nextcloud folder.

Step by step

Step 1: Read the admin overview as a stock-take

Log in as administrator and open Administration settings → Overview. At the top is the “Security & setup warnings” section. Right after a standard installation it looks like this:

The Nextcloud admin overview lists several yellow warnings about the maintenance window, MIME-type migrations and HTTP headers
Starting point: three warnings and several notices in the admin overview

Nextcloud distinguishes three levels: yellow warnings (⚠, you should fix), blue notices (ℹ, usually optional) and silently passed checks. On our fresh instance there are three warnings and a few notices:

  • Maintenance window start is not set (step 2)
  • MIME-type migrations available (step 2)
  • HTTP headers – the Strict-Transport-Security header is missing (step 3)
  • Email test – no mail server configured yet (step 4)
  • Two-factor configuration – 2FA is available but not enforced (step 5)

You also get the same report on the command line – handy to check progress in between without clicking through the web interface:

Terminal
docker exec -u www-data nc-app php occ setupchecks

The command outputs each check with ✓, ⚠ or ✗. We now work through exactly this list from top to bottom.

Step 2: Fix the two quick warnings

Two warnings are done with one command each.

Set the maintenance window. Nextcloud runs compute-intensive cleanup jobs once daily (previews, activities, calendar repeats). Without a defined time window they run at some point – even in the middle of the day. Set a start time when hardly anyone works. The value is a full hour in UTC; 1 sets the start to 01:00 UTC – the check afterwards reports the window from 1:00 to 7:00 UTC:

Terminal
docker exec -u www-data nc-app php occ config:system:set maintenance_window_start --type=integer --value=1

To verify, the setup check afterwards confirms:

Ausgabe
✓ Maintenance window start: Maintenance window to execute heavy background jobs is between 1:00 UTC and 7:00 UTC

Run MIME-type migrations. Occasionally Nextcloud learns new file types (e.g. for better icons and previews). This migration does not run automatically on updates because it can take a while on large instances. On a fresh cloud it’s done in seconds:

Terminal
docker exec -u www-data nc-app php occ maintenance:repair --include-expensive

You see a list of repair steps performed (including Repair mime types). Afterwards the check reports Mimetype migrations available: None. Two fewer warnings.

And the blue notices?

The notices “AppAPI deploy daemon” and “Server-ID configuration” you can ignore on a normal single-server installation. The first concerns only the installation of external “Ex-Apps” via a Docker daemon, the second only setups spread across several PHP servers. You need neither here – that’s why they stay notices and not warnings.

Step 3: Set the HSTS security header via Traefik

The third warning is the most important: the Strict-Transport-Security header (HSTS) is missing. It instructs the browser to address this domain exclusively over HTTPS – even when someone types http:// or an attacker tries to redirect the connection. Without HSTS, a small time window for downgrade attacks stays open.

Because our Nextcloud sits behind Traefik, we set the header in the proxy, not in Nextcloud – that’s where it belongs, since Traefik terminates the TLS. In the compose.yaml of your Nextcloud (from part 1), add the middleware nc-secure and attach it to the router. Concretely, two places change in the labels block of nc-app.

First, extend the router line with nc-secure:

YAML
      - "traefik.http.routers.nc.middlewares=nc-dav,nc-secure"

Second, define the header middleware (right by the other nc-app labels):

YAML
      - "traefik.http.middlewares.nc-secure.headers.stsSeconds=15552000"
      - "traefik.http.middlewares.nc-secure.headers.stsIncludeSubdomains=true"

stsSeconds=15552000 is 180 days – the minimum value Nextcloud requires. Apply the change and reload only the app container:

Terminal
docker compose up -d nc-app

After a few seconds, check from your own machine that the header is really delivered:

Terminal
curl -sS -I https://cloud.YOUR_DOMAIN/ | grep -i strict-transport

You should see exactly this:

Ausgabe
strict-transport-security: max-age=15552000; includeSubDomains

In the admin overview the HTTP-header warning thus turns green: “Your server is correctly configured to send security headers.” The remaining headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy …) Nextcloud delivers itself – only HSTS had to come from the proxy.

preload is a one-way street

stsIncludeSubdomains=true extends the HTTPS requirement to all subdomains of cloud.YOUR_DOMAIN (not to siblings like www.YOUR_DOMAIN – the header always applies only to the host that delivers it). Deliberately not set is stsPreload=true: with it you’d signal that you want the domain entered into the browsers' HSTS preload list – and such an entry can only be undone with weeks of lead time. If you ever deliberately want that: the preload list only accepts max-age ≥ 31536000 (1 year) – with the 15552000 above the flag would be ineffective anyway. Only set preload when really every service under the domain permanently speaks HTTPS; the header is fully functional without it.

Step 4: Set up email sending

Without working mail sending, Nextcloud can send no password resets, no share notifications and no security warnings – and the “Email test” notice stays. We catch up on that.

Open Administration settings → Basic settings and scroll to the “Email server” section. Enter your mail provider’s credentials:

  • Send mode: SMTP
  • Encryption: SSL/TLS (port 465) or STARTTLS (port 587) – never unencrypted
  • From address: e.g. cloud @ YOUR_DOMAIN
  • Server address: host and port of your provider
  • Authentication: enable and store username/password

At the top, under Personal information, enter an email address for your admin account (the test mail goes there). Then click “Send test email”. If the mail arrives, Nextcloud confirms it with a green message:

The email server is configured with SMTP, a test send is confirmed at the top right with “Email sent”
Successful test send – the values in the image come from an internal test mail server and are NOT to be adopted for real mailboxes; there belong the host, port 587/465 with STARTTLS/SSL and your provider's credentials

App password instead of account password

If your mail provider itself uses two-factor authentication (Gmail, Mailbox.org, many others), your normal login password does not work here. Create a dedicated app password in your provider’s account and enter that. It can also be revoked specifically without changing your main password.

Step 5: Enforce two-factor authentication

A stolen password is the most common way accounts get taken over. A second factor (a time-based code from an authenticator app, TOTP) makes the password alone worthless. Nextcloud already brings the matching provider – we just have to enable it and make it mandatory.

First enable the TOTP app (it’s included):

Terminal
docker exec -u www-data nc-app php occ app:enable twofactor_totp

Each user then sets up their second factor themselves – under Personal settings → Security → “TOTP (Authenticator app)”. On enabling, Nextcloud shows a QR code you scan with an app like Aegis, andOTP or Google Authenticator. Additionally, the app “Two-Factor Backup Codes” is recommended – the one-time codes save you if the phone is lost.

So that nobody “forgets” the safeguard, you enforce 2FA for the administrators. On the web you find that under Administration settings → Security → “Enforce two-factor authentication”, on the command line it works like this:

Terminal
docker exec -u www-data nc-app php occ twofactorauth:enforce --on --group=admin

The security settings show enforced two-factor authentication for the “admin” group as well as the brute-force status with a correctly recognized client IP
Two-factor requirement for the \"admin\" group – and above it the proof that Nextcloud recognizes the real client IP

Don't lock yourself out

As soon as enforcement is active, every member of the group must set up a second factor on the next login. So set up your own second factor first and keep the backup codes safe before you arm the requirement. If something does go wrong, you lift the enforcement again via the command line: docker exec -u www-data nc-app php occ twofactorauth:enforce --off. A single stuck factor you remove with … php occ twofactorauth:disable USER totp.

Step 6: Brute-force protection with real IPs – basis for Fail2ban

Nextcloud throttles failed logins out of the box: after several failed attempts it delays further requests from the same IP. But this protection stands and falls with Nextcloud seeing the real client IP – and not the internal IP of Traefik. That’s exactly what the TRUSTED_PROXIES setting from part 1 does.

Whether it works you see under Administration settings → Security in the “brute-force allow list” box (in the screenshot above). There stands your current, correctly recognized public IP:

Ausgabe
Your current IP address is recognized as "YOUR_IP". This address is currently not throttled.

If a 172.x.x.x address from the Docker network appears there instead, TRUSTED_PROXIES isn’t taking effect – then the brute-force protection would lump all users together and, in doubt, ban Traefik itself. In that case, back to part 1, step 2/3.

Hard bans with Fail2ban. The built-in throttling only delays; whoever wants to really ban attackers combines Nextcloud with Fail2ban. Nextcloud logs every failed attempt in the JSON log – and thanks to TRUSTED_PROXIES with the real IP:

Ausgabe
{"level":2,"time":"2026-07-18T08:14:05+00:00","remoteAddr":"203.0.113.47","user":"--","app":"no app in context","method":"POST","url":"/login","message":"Login failed: USER (Remote IP: 203.0.113.47)"}

This log lives in the Nextcloud volume. You find the path on the host like this:

Terminal
docker inspect -f '{{ range .Mounts }}{{ if eq .Destination "/var/www/html" }}{{ .Source }}{{ end }}{{ end }}' nc-app

Below it is the file data/nextcloud.log. At that you point a Fail2ban filter that matches on remoteAddr, plus a jail following the pattern from the Fail2ban tutorial. This way, after a few failed attempts, the attacker IP goes straight into the firewall – before the request even reaches Nextcloud.

Step 7: Check caching & PHP – the performance foundation

Finally, we make sure the performance basis is in place. Much of this part 1 already set up automatically via the REDIS_HOST variable – checking doesn’t hurt anyway, because this is exactly where a Nextcloud gets sluggish or snappy.

Nextcloud uses three cache roles. Check all three:

Terminal
docker exec -u www-data nc-app php occ config:system:get memcache.local
docker exec -u www-data nc-app php occ config:system:get memcache.distributed
docker exec -u www-data nc-app php occ config:system:get memcache.locking

Expected:

Ausgabe
\OC\Memcache\APCu
\OC\Memcache\Redis
\OC\Memcache\Redis
  • Local cache (APCu): keeps frequently used data in the RAM of the PHP process – the most noticeable accelerator in everyday use.
  • Distributed cache (Redis): shares cache data across processes, important once the app and cron containers interact.
  • File locking (Redis): prevents two accesses from changing the same file simultaneously and corrupting it. The reason we built in Redis at all in part 1.

If the interplay fits, docker exec -u www-data nc-app php occ setupchecks reports the line ✓ Memcache: Configured. (Passed checks aren’t shown by the web overview – only warnings and notices appear there, which is why you check this via CLI.)

OPcache – PHP’s bytecode cache – is already sensibly preconfigured in the official nextcloud image (including opcache.interned_strings_buffer=32). So you don’t have to adjust anything here; the formerly notorious OPcache warning no longer appears at all with current images. You can check it if needed like this:

Terminal
docker exec -u www-data nc-app php -i | grep opcache.interned_strings_buffer

With that, the overview is warning-free:

The Nextcloud admin overview shows no more yellow warnings, only two optional notices, and reports the current version
Result: no more warnings, version current – only the two optional notices remain

Generate previews proactively

If your users store many photos, the app “Preview Generator” is worth it: an occ cron job generates thumbnails in advance instead of computing them slowly on the fly on first open. If you add it, limit the maximum preview size via occ config:system:set preview_max_x --value=2048 (and preview_max_y), otherwise the storage demand grows quickly.

When things go wrong

After the Traefik restart, the HSTS header doesn’t appear in the curl output. Usually the middleware isn’t on the router. Check that the line traefik.http.routers.nc.middlewares names both middlewares (nc-dav,nc-secure) and that the three nc-secure header labels are in the nc-app block. Then docker compose up -d nc-app. Traefik only picks up changed labels when recreating the container.

The email test fails (“There was a problem sending the email”). Almost always port/encryption or authentication. Combine SSL/TLS with port 465 or STARTTLS with port 587 – not crosswise. If your provider uses 2FA, you need an app password (see step 4). Details are in the Nextcloud log: docker exec -u www-data nc-app php occ log:tail 20.

You can’t get in yourself after enforcing 2FA. You hadn’t set up a second factor yet. Lift the requirement via the command line, set up your factor calmly and arm it again afterwards: docker exec -u www-data nc-app php occ twofactorauth:enforce --off.

In the brute-force box a 172.x.x.x address appears instead of your real IP. TRUSTED_PROXIES doesn’t match the actual proxy subnet. Determine it with docker network inspect proxy -f '{{(index .IPAM.Config 0).Subnet}}' and enter the value in the nc-app environment (part 1, step 2). Otherwise the brute-force protection bans the proxy in an emergency and thus all users.

After a reboot, Nextcloud reports “Redis went away” or gets very slow. The app container started before Redis. Make sure nc-redis is in depends_on (part 1) and runs with restart: unless-stopped – then Docker catches the start-order case itself.

Maintenance & backups

  • Check the overview after every update. New Nextcloud versions bring new checks. After every upgrade take a look at Administration settings → Overview or run occ setupchecks – this way you catch new warnings early.
  • Use the official security scan regularly. The Nextcloud Security Scan checks your domain from outside and assigns a grade. With HSTS and current versions, A/A+ is the realistic target; if the grade drops, an update is usually overdue.
  • Keep an eye on second factors. Keep the backup codes separate from the phone. If a user loses their device, you remove their factor with occ twofactorauth:disable USER totp, after which they set it up anew.
  • Clean up app passwords. Every connected client (desktop, phone) gets its own app password. Under Personal settings → Security you see all devices and can specifically revoke individual ones on loss.
  • The hardening replaces no backup. All of this protects against foreign access, not against data loss. The consistent backup of a database dump and data directory stays mandatory – see the maintenance section in part 1 and backups with Restic. Honest about the effort: firmly plan the monthly update pass with a quick look at the overview – then your cloud stays green permanently.

You might also like