<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>2fa – Serverküche</title><link>https://serverkueche.de/en/tags/2fa/</link><description>2fa – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Sun, 19 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/2fa/index.xml" rel="self" type="application/rss+xml"/><item><title>Hardening &amp; optimizing Nextcloud: clear every warning (part 2)</title><link>https://serverkueche.de/en/tutorials/harden-optimize-nextcloud/</link><pubDate>Sun, 19 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/harden-optimize-nextcloud/</guid><description>Get your Nextcloud admin overview green: set up HSTS headers, email sending, enforced two-factor auth and brute-force protection with real IPs.</description><content:encoded><![CDATA[<p>Your Nextcloud is running – but the admin overview shows yellow warnings, there&rsquo;s no working
email and no second factor protects your accounts. In this second part we turn &ldquo;running&rdquo;
into &ldquo;cleanly secured and snappy&rdquo;: we work through every warning until the overview is green.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end, the <strong>security &amp; setup overview</strong> of your Nextcloud shows no more warnings.
Concretely, we build on the installation from <a href="/en/tutorials/self-host-nextcloud/">part 1</a>
and add: the missing <strong>HSTS security header</strong> via Traefik, a configured <strong>maintenance
window</strong>, working <strong>email sending</strong> (for password resets and notifications), <strong>enforced
two-factor authentication</strong> and <strong>brute-force protection that sees the real attacker IPs</strong> –
the basis for Fail2ban.</p>
<p>Everything is tested against <strong>Nextcloud 34.0.1</strong> behind <strong>Traefik v3.7</strong> 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 &ldquo;somehow online&rdquo; and &ldquo;properly operated&rdquo;.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A <strong>running Nextcloud</strong> set up as in
<a href="/en/tutorials/self-host-nextcloud/">self-hosting Nextcloud (part 1)</a>: classic <code>nextcloud</code>
image behind Traefik, with MariaDB, Redis and a cron sidecar. The container names from
part 1 (<code>nc-app</code>, <code>nc-db</code>, <code>nc-redis</code>, <code>nc-cron</code>) and the Compose folder <code>~/nextcloud</code> are
the basis here.</li>
<li><strong>SSH access</strong> to the server and the command-line tool <strong><code>occ</code></strong> – we call it as in part 1
in the app container: <code>docker exec -u www-data nc-app php occ &lt;command&gt;</code>.</li>
<li>A <strong>Traefik reverse proxy</strong> with the <code>proxy</code> network and the resolver <code>le</code> as in the
tutorial <a href="/en/tutorials/traefik-reverse-proxy/">reverse proxy with Traefik</a> – we attach the
security headers as a Traefik middleware.</li>
</ul>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Keeping occ in hand
  </p>
  <div class="prose-kitchen text-sm"><p><code>occ</code> is Nextcloud&rsquo;s admin tool. Because the containers in part 1 got fixed names
(<code>container_name</code>), you address the app container directly – no matter which directory you&rsquo;re
in:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ status</span></span></code></pre></div>
</div>
<p>You should see <code>installed: true</code> and your version. We won&rsquo;t abbreviate this prefix
(<code>docker exec -u www-data nc-app php occ …</code>) in the following – so you can copy every command
directly. Only the few <code>docker compose …</code> commands (like the container restart in step 3) you
still run from the <code>~/nextcloud</code> folder.</p>
</div>
</div>
<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-read-the-admin-overview-as-a-stock-take">Step 1: Read the admin overview as a stock-take</h3>
<p>Log in as administrator and open <strong>Administration settings → Overview</strong>. At the top is the
<strong>&ldquo;Security &amp; setup warnings&rdquo;</strong> section. Right after a standard installation it looks like
this:</p>
<p><figure class="my-6"><img src="/en/tutorials/harden-optimize-nextcloud/nc-uebersicht-warnungen_hu_1c52167f2e50ee81.webp" srcset="/en/tutorials/harden-optimize-nextcloud/nc-uebersicht-warnungen_hu_abb4be326f248f94.webp 480w, /en/tutorials/harden-optimize-nextcloud/nc-uebersicht-warnungen_hu_1c52167f2e50ee81.webp 768w, /en/tutorials/harden-optimize-nextcloud/nc-uebersicht-warnungen_hu_486b6513ab603031.webp 1200w, /en/tutorials/harden-optimize-nextcloud/nc-uebersicht-warnungen_hu_2dc71aec63365520.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/harden-optimize-nextcloud/nc-uebersicht-warnungen_hu_19f3c57f5382e577.webp"
    alt="The Nextcloud admin overview lists several yellow warnings about the maintenance window, MIME-type migrations and HTTP headers" title="Starting point: three warnings and several notices in the admin overview"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Starting point: three warnings and several notices in the admin overview</figcaption></figure></p>
<p>Nextcloud distinguishes three levels: <strong>yellow warnings</strong> (⚠, you should fix), <strong>blue notices</strong>
(ℹ, usually optional) and silently passed checks. On our fresh instance there are three
warnings and a few notices:</p>
<ul>
<li>⚠ <strong>Maintenance window start</strong> is not set (step 2)</li>
<li>⚠ <strong>MIME-type migrations available</strong> (step 2)</li>
<li>⚠ <strong>HTTP headers</strong> – the <code>Strict-Transport-Security</code> header is missing (step 3)</li>
<li>ℹ <strong>Email test</strong> – no mail server configured yet (step 4)</li>
<li>ℹ <strong>Two-factor configuration</strong> – 2FA is available but not enforced (step 5)</li>
</ul>
<p>You also get the same report on the command line – handy to check progress in between without
clicking through the web interface:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ setupchecks</span></span></code></pre></div>
</div>
<p>The command outputs each check with ✓, ⚠ or ✗. We now work through exactly this list from top
to bottom.</p>
<h3 id="step-2-fix-the-two-quick-warnings">Step 2: Fix the two quick warnings</h3>
<p>Two warnings are done with one command each.</p>
<p><strong>Set the maintenance window.</strong> Nextcloud runs compute-intensive cleanup jobs once daily
(previews, activities, calendar repeats). Without a defined time window they run <em>at some
point</em> – even in the middle of the day. Set a start time when hardly anyone works. The value
is a <strong>full hour in UTC</strong>; <code>1</code> sets the start to 01:00 UTC – the check afterwards reports the
window from 1:00 to 7:00 UTC:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ config:system:set maintenance_window_start --type<span class="o">=</span>integer --value<span class="o">=</span><span class="m">1</span></span></span></code></pre></div>
</div>
<p>To verify, the setup check afterwards confirms:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">✓ Maintenance window start: Maintenance window to execute heavy background jobs is between 1:00 UTC and 7:00 UTC</span></span></code></pre></div>
</div>
<p><strong>Run MIME-type migrations.</strong> Occasionally Nextcloud learns new file types (e.g. for better
icons and previews). This migration does <em>not</em> run automatically on updates because it can
take a while on large instances. On a fresh cloud it&rsquo;s done in seconds:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ maintenance:repair --include-expensive</span></span></code></pre></div>
</div>
<p>You see a list of repair steps performed (including <code>Repair mime types</code>). Afterwards the check
reports <code>Mimetype migrations available: None</code>. Two fewer warnings.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">ℹ️</span>And the blue notices?
  </p>
  <div class="prose-kitchen text-sm">The notices <strong>&ldquo;AppAPI deploy daemon&rdquo;</strong> and <strong>&ldquo;Server-ID configuration&rdquo;</strong> you can ignore on a
normal single-server installation. The first concerns only the installation of external
&ldquo;Ex-Apps&rdquo; via a Docker daemon, the second only setups spread across several PHP servers. You
need neither here – that&rsquo;s why they stay notices and not warnings.</div>
</div>
<h3 id="step-3-set-the-hsts-security-header-via-traefik">Step 3: Set the HSTS security header via Traefik</h3>
<p>The third warning is the most important: the <strong><code>Strict-Transport-Security</code> header</strong> (HSTS) is
missing. It instructs the browser to address this domain <strong>exclusively over HTTPS</strong> – even
when someone types <code>http://</code> or an attacker tries to redirect the connection. Without HSTS, a
small time window for downgrade attacks stays open.</p>
<p>Because our Nextcloud sits behind Traefik, we set the header <strong>in the proxy</strong>, not in
Nextcloud – that&rsquo;s where it belongs, since Traefik terminates the TLS. In the <code>compose.yaml</code>
of your Nextcloud (from part 1), add the middleware <code>nc-secure</code> and attach it to the router.
Concretely, two places change in the <code>labels</code> block of <code>nc-app</code>.</p>
<p>First, extend the router line with <code>nc-secure</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.nc.middlewares=nc-dav,nc-secure&#34;</span></span></span></code></pre></div>
</div>
<p>Second, define the header middleware (right by the other <code>nc-app</code> labels):</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.nc-secure.headers.stsSeconds=15552000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.nc-secure.headers.stsIncludeSubdomains=true&#34;</span></span></span></code></pre></div>
</div>
<p><code>stsSeconds=15552000</code> is 180 days – the minimum value Nextcloud requires. Apply the change and
reload only the app container:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d nc-app</span></span></code></pre></div>
</div>
<p>After a few seconds, check from your own machine that the header is really delivered:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -sS -I https://cloud.YOUR_DOMAIN/ <span class="p">|</span> grep -i strict-transport</span></span></code></pre></div>
</div>
<p>You should see exactly this:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">strict-transport-security: max-age=15552000; includeSubDomains</span></span></code></pre></div>
</div>
<p>In the admin overview the HTTP-header warning thus turns green: <em>&ldquo;Your server is correctly
configured to send security headers.&rdquo;</em> The remaining headers (<code>X-Content-Type-Options</code>,
<code>X-Frame-Options</code>, <code>Referrer-Policy</code> …) Nextcloud delivers itself – only HSTS had to come from
the proxy.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>preload is a one-way street
  </p>
  <div class="prose-kitchen text-sm"><code>stsIncludeSubdomains=true</code> extends the HTTPS requirement to all subdomains <strong>of
<code>cloud.YOUR_DOMAIN</code></strong> (not to siblings like <code>www.YOUR_DOMAIN</code> – the header always applies only
to the host that delivers it). Deliberately <strong>not</strong> set is <code>stsPreload=true</code>: with it you&rsquo;d
signal that you want the domain entered into the browsers'
<a href="https://hstspreload.org/">HSTS preload list</a> – and such an entry can only be undone with
weeks of lead time. If you ever deliberately want that: the preload list only accepts
<code>max-age</code> ≥ 31536000 (1 year) – with the 15552000 above the flag would be ineffective anyway.
Only set <code>preload</code> when really <em>every</em> service under the domain permanently speaks HTTPS; the
header is fully functional without it.</div>
</div>
<h3 id="step-4-set-up-email-sending">Step 4: Set up email sending</h3>
<p>Without working mail sending, Nextcloud can send no <strong>password resets</strong>, no <strong>share
notifications</strong> and no <strong>security warnings</strong> – and the &ldquo;Email test&rdquo; notice stays. We catch up
on that.</p>
<p>Open <strong>Administration settings → Basic settings</strong> and scroll to the <strong>&ldquo;Email server&rdquo;</strong>
section. Enter your mail provider&rsquo;s credentials:</p>
<ul>
<li><strong>Send mode:</strong> <code>SMTP</code></li>
<li><strong>Encryption:</strong> <code>SSL/TLS</code> (port <strong>465</strong>) or <code>STARTTLS</code> (port <strong>587</strong>) – never unencrypted</li>
<li><strong>From address:</strong> e.g. <code>cloud</code> @ <code>YOUR_DOMAIN</code></li>
<li><strong>Server address:</strong> host and port of your provider</li>
<li><strong>Authentication:</strong> enable and store username/password</li>
</ul>
<p>At the top, under <strong>Personal information</strong>, enter an email address for your admin account (the
test mail goes there). Then click <strong>&ldquo;Send test email&rdquo;</strong>. If the mail arrives, Nextcloud
confirms it with a green message:</p>
<p><figure class="my-6"><img src="/en/tutorials/harden-optimize-nextcloud/nc-email-test_hu_8d45e6800f6b3498.webp" srcset="/en/tutorials/harden-optimize-nextcloud/nc-email-test_hu_fa94582cdd4ce86a.webp 480w, /en/tutorials/harden-optimize-nextcloud/nc-email-test_hu_8d45e6800f6b3498.webp 768w, /en/tutorials/harden-optimize-nextcloud/nc-email-test_hu_c032c70fb8d96766.webp 1200w, /en/tutorials/harden-optimize-nextcloud/nc-email-test_hu_634c0ea546cf8596.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/harden-optimize-nextcloud/nc-email-test_hu_9c1f6dabcee5c7cf.webp"
    alt="The email server is configured with SMTP, a test send is confirmed at the top right with &ldquo;Email sent&rdquo;" title="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&#39;s credentials"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">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&#39;s credentials</figcaption></figure></p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>App password instead of account password
  </p>
  <div class="prose-kitchen text-sm">If your mail provider itself uses two-factor authentication (Gmail, Mailbox.org, many others),
your normal login password does <strong>not</strong> work here. Create a dedicated <strong>app password</strong> in your
provider&rsquo;s account and enter that. It can also be revoked specifically without changing your
main password.</div>
</div>
<h3 id="step-5-enforce-two-factor-authentication">Step 5: Enforce two-factor authentication</h3>
<p>A stolen password is the most common way accounts get taken over. A <strong>second factor</strong> (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.</p>
<p>First enable the TOTP app (it&rsquo;s included):</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ app:enable twofactor_totp</span></span></code></pre></div>
</div>
<p>Each user then sets up their second factor themselves – under <strong>Personal settings → Security →
&ldquo;TOTP (Authenticator app)&rdquo;</strong>. On enabling, Nextcloud shows a QR code you scan with an app like
Aegis, andOTP or Google Authenticator. Additionally, the app <strong>&ldquo;Two-Factor Backup Codes&rdquo;</strong> is
recommended – the one-time codes save you if the phone is lost.</p>
<p>So that nobody &ldquo;forgets&rdquo; the safeguard, you enforce 2FA for the administrators. On the web
you find that under <strong>Administration settings → Security → &ldquo;Enforce two-factor
authentication&rdquo;</strong>, on the command line it works like this:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ twofactorauth:enforce --on --group<span class="o">=</span>admin</span></span></code></pre></div>
</div>
<p><figure class="my-6"><img src="/en/tutorials/harden-optimize-nextcloud/nc-zweifaktor_hu_852af47470939e12.webp" srcset="/en/tutorials/harden-optimize-nextcloud/nc-zweifaktor_hu_1a27814597c783f5.webp 480w, /en/tutorials/harden-optimize-nextcloud/nc-zweifaktor_hu_852af47470939e12.webp 768w, /en/tutorials/harden-optimize-nextcloud/nc-zweifaktor_hu_4b78d97a12110e38.webp 1200w, /en/tutorials/harden-optimize-nextcloud/nc-zweifaktor_hu_2c4ea048fe38829a.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/harden-optimize-nextcloud/nc-zweifaktor_hu_99357b7cbf56851d.webp"
    alt="The security settings show enforced two-factor authentication for the &ldquo;admin&rdquo; group as well as the brute-force status with a correctly recognized client IP" title="Two-factor requirement for the \&#34;admin\&#34; group – and above it the proof that Nextcloud recognizes the real client IP"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Two-factor requirement for the \&#34;admin\&#34; group – and above it the proof that Nextcloud recognizes the real client IP</figcaption></figure></p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>Don&#39;t lock yourself out
  </p>
  <div class="prose-kitchen text-sm">As soon as enforcement is active, every member of the group <strong>must</strong> set up a second factor on
the next login. So set up <strong>your own</strong> 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: <code>docker exec -u www-data nc-app php occ twofactorauth:enforce --off</code>. A
single stuck factor you remove with <code>… php occ twofactorauth:disable USER totp</code>.</div>
</div>
<h3 id="step-6-brute-force-protection-with-real-ips--basis-for-fail2ban">Step 6: Brute-force protection with real IPs – basis for Fail2ban</h3>
<p>Nextcloud throttles failed logins out of the box: after several failed attempts it delays
further requests from <strong>the same IP</strong>. But this protection stands and falls with Nextcloud
seeing the <strong>real client IP</strong> – and not the internal IP of Traefik. That&rsquo;s exactly what the
<code>TRUSTED_PROXIES</code> setting from part 1 does.</p>
<p>Whether it works you see under <strong>Administration settings → Security</strong> in the &ldquo;brute-force
allow list&rdquo; box (in the screenshot above). There stands your current, correctly recognized
public IP:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Your current IP address is recognized as &#34;YOUR_IP&#34;. This address is currently not throttled.</span></span></code></pre></div>
</div>
<p>If a <code>172.x.x.x</code> address from the Docker network appears there instead, <code>TRUSTED_PROXIES</code>
isn&rsquo;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.</p>
<p><strong>Hard bans with Fail2ban.</strong> The built-in throttling only delays; whoever wants to really ban
attackers combines Nextcloud with <a href="/en/tutorials/fail2ban-setup/">Fail2ban</a>. Nextcloud logs
every failed attempt in the JSON log – and thanks to <code>TRUSTED_PROXIES</code> with the real IP:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;level&#34;:2,&#34;time&#34;:&#34;2026-07-18T08:14:05+00:00&#34;,&#34;remoteAddr&#34;:&#34;203.0.113.47&#34;,&#34;user&#34;:&#34;--&#34;,&#34;app&#34;:&#34;no app in context&#34;,&#34;method&#34;:&#34;POST&#34;,&#34;url&#34;:&#34;/login&#34;,&#34;message&#34;:&#34;Login failed: USER (Remote IP: 203.0.113.47)&#34;}</span></span></code></pre></div>
</div>
<p>This log lives in the Nextcloud volume. You find the path on the host like this:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker inspect -f <span class="s1">&#39;{{ range .Mounts }}{{ if eq .Destination &#34;/var/www/html&#34; }}{{ .Source }}{{ end }}{{ end }}&#39;</span> nc-app</span></span></code></pre></div>
</div>
<p>Below it is the file <code>data/nextcloud.log</code>. At that you point a Fail2ban filter that matches on
<code>remoteAddr</code>, plus a jail following the pattern from the
<a href="/en/tutorials/fail2ban-setup/">Fail2ban tutorial</a>. This way, after a few failed attempts, the
attacker IP goes straight into the firewall – before the request even reaches Nextcloud.</p>
<h3 id="step-7-check-caching--php--the-performance-foundation">Step 7: Check caching &amp; PHP – the performance foundation</h3>
<p>Finally, we make sure the performance basis is in place. Much of this part 1 already set up
automatically via the <code>REDIS_HOST</code> variable – checking doesn&rsquo;t hurt anyway, because this is
exactly where a Nextcloud gets sluggish or snappy.</p>
<p>Nextcloud uses <strong>three</strong> cache roles. Check all three:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ config:system:get memcache.local
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ config:system:get memcache.distributed
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ config:system:get memcache.locking</span></span></code></pre></div>
</div>
<p>Expected:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">\OC\Memcache\APCu
</span></span><span class="line"><span class="cl">\OC\Memcache\Redis
</span></span><span class="line"><span class="cl">\OC\Memcache\Redis</span></span></code></pre></div>
</div>
<ul>
<li><strong>Local cache (APCu):</strong> keeps frequently used data in the RAM of the PHP process – the most
noticeable accelerator in everyday use.</li>
<li><strong>Distributed cache (Redis):</strong> shares cache data across processes, important once the app and
cron containers interact.</li>
<li><strong>File locking (Redis):</strong> prevents two accesses from changing the same file simultaneously and
corrupting it. The reason we built in Redis at all in part 1.</li>
</ul>
<p>If the interplay fits, <code>docker exec -u www-data nc-app php occ setupchecks</code> reports the line
<code>✓ Memcache: Configured</code>. (Passed checks aren&rsquo;t shown by the web overview – only warnings and
notices appear there, which is why you check this via CLI.)</p>
<p><strong>OPcache</strong> – PHP&rsquo;s bytecode cache – is already sensibly preconfigured in the official
<code>nextcloud</code> image (including <code>opcache.interned_strings_buffer=32</code>). So you don&rsquo;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:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php -i <span class="p">|</span> grep opcache.interned_strings_buffer</span></span></code></pre></div>
</div>
<p>With that, the overview is warning-free:</p>
<p><figure class="my-6"><img src="/en/tutorials/harden-optimize-nextcloud/nc-uebersicht-gruen_hu_3292f5806336612e.webp" srcset="/en/tutorials/harden-optimize-nextcloud/nc-uebersicht-gruen_hu_6b3888444730c10c.webp 480w, /en/tutorials/harden-optimize-nextcloud/nc-uebersicht-gruen_hu_3292f5806336612e.webp 768w, /en/tutorials/harden-optimize-nextcloud/nc-uebersicht-gruen_hu_2a971e5ec7202dca.webp 1200w, /en/tutorials/harden-optimize-nextcloud/nc-uebersicht-gruen_hu_b6c2c3da23b5f506.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/harden-optimize-nextcloud/nc-uebersicht-gruen_hu_f8c15f84dd9e1da.webp"
    alt="The Nextcloud admin overview shows no more yellow warnings, only two optional notices, and reports the current version" title="Result: no more warnings, version current – only the two optional notices remain"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Result: no more warnings, version current – only the two optional notices remain</figcaption></figure></p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Generate previews proactively
  </p>
  <div class="prose-kitchen text-sm">If your users store many photos, the app <strong>&ldquo;Preview Generator&rdquo;</strong> is worth it: an <code>occ</code> 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
<code>occ config:system:set preview_max_x --value=2048</code> (and <code>preview_max_y</code>), otherwise the storage
demand grows quickly.</div>
</div>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>After the Traefik restart, the HSTS header doesn&rsquo;t appear in the <code>curl</code> output.</strong> Usually
the middleware isn&rsquo;t on the router. Check that the line <code>traefik.http.routers.nc.middlewares</code>
names <strong>both</strong> middlewares (<code>nc-dav,nc-secure</code>) and that the three <code>nc-secure</code> header labels
are in the <code>nc-app</code> block. Then <code>docker compose up -d nc-app</code>. Traefik only picks up changed
labels when recreating the container.</p>
<p><strong>The email test fails (&ldquo;There was a problem sending the email&rdquo;).</strong> Almost always
port/encryption or authentication. Combine <code>SSL/TLS</code> with port <strong>465</strong> or <code>STARTTLS</code> with port
<strong>587</strong> – not crosswise. If your provider uses 2FA, you need an <strong>app password</strong> (see step 4).
Details are in the Nextcloud log: <code>docker exec -u www-data nc-app php occ log:tail 20</code>.</p>
<p><strong>You can&rsquo;t get in yourself after enforcing 2FA.</strong> You hadn&rsquo;t set up a second factor yet. Lift
the requirement via the command line, set up your factor calmly and arm it again afterwards:
<code>docker exec -u www-data nc-app php occ twofactorauth:enforce --off</code>.</p>
<p><strong>In the brute-force box a <code>172.x.x.x</code> address appears instead of your real IP.</strong>
<code>TRUSTED_PROXIES</code> doesn&rsquo;t match the actual <code>proxy</code> subnet. Determine it with <code>docker network inspect proxy -f '{{(index .IPAM.Config 0).Subnet}}'</code> and enter the value in the <code>nc-app</code>
environment (part 1, step 2). Otherwise the brute-force protection bans the proxy in an
emergency and thus all users.</p>
<p><strong>After a reboot, Nextcloud reports &ldquo;Redis went away&rdquo; or gets very slow.</strong> The app container
started before Redis. Make sure <code>nc-redis</code> is in <code>depends_on</code> (part 1) and runs with <code>restart: unless-stopped</code> – then Docker catches the start-order case itself.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>Check the overview after every update.</strong> New Nextcloud versions bring new checks. After
every upgrade take a look at <strong>Administration settings → Overview</strong> or run <code>occ setupchecks</code> –
this way you catch new warnings early.</li>
<li><strong>Use the official security scan regularly.</strong> The
<a href="https://scan.nextcloud.com/">Nextcloud Security Scan</a> checks your domain from outside and
assigns a grade. With HSTS and current versions, <strong>A/A+</strong> is the realistic target; if the
grade drops, an update is usually overdue.</li>
<li><strong>Keep an eye on second factors.</strong> Keep the backup codes separate from the phone. If a user
loses their device, you remove their factor with <code>occ twofactorauth:disable USER totp</code>, after
which they set it up anew.</li>
<li><strong>Clean up app passwords.</strong> Every connected client (desktop, phone) gets its own app
password. Under <strong>Personal settings → Security</strong> you see all devices and can specifically
revoke individual ones on loss.</li>
<li><strong>The hardening replaces no backup.</strong> 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 <a href="/en/tutorials/self-host-nextcloud/">part 1</a> and
<a href="/en/tutorials/restic-backups/">backups with Restic</a>. Honest about the effort: firmly plan
the monthly update pass with a quick look at the overview – then your cloud stays green
permanently.</li>
</ul>
]]></content:encoded></item></channel></rss>