<?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>Nextcloud – Serverküche</title><link>https://serverkueche.de/en/tags/nextcloud/</link><description>Nextcloud – 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/nextcloud/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><item><title>Self-hosting Nextcloud: your own cloud behind Traefik</title><link>https://serverkueche.de/en/tutorials/self-host-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/self-host-nextcloud/</guid><description>Set up Nextcloud with Docker behind Traefik: your own cloud for files, calendar and contacts – with MariaDB, Redis and automatic HTTPS.</description><content:encoded><![CDATA[<p>Your files, your calendar, your contacts – but on your server instead of at a cloud
corporation. <strong>Nextcloud</strong> is the flagship of self-hosting: a full-featured cloud you run
yourself. In this recipe we set it up cleanly behind Traefik, with its own database,
caching and automatic HTTPS.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end, <strong>Nextcloud 34</strong> runs behind your Traefik proxy, reachable at
<code>https://cloud.YOUR_DOMAIN</code> with a valid Let&rsquo;s Encrypt certificate. Nextcloud is your
private cloud: sync files (like Dropbox), calendar and contacts across all devices, photos,
notes, office documents. You connect the official <strong>desktop sync client</strong> and the <strong>phone
apps</strong> with your server – the data lives exclusively with you.</p>
<p>We deliberately use the <strong>classic <code>nextcloud</code> image</strong> (not &ldquo;Nextcloud All-in-One&rdquo;) plus
<strong>MariaDB</strong> as the database and <strong>Redis</strong> for caching and file locking. This combination is
stable, well documented and fits exactly into the Traefik pattern from the Serverküche.</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>Why not Nextcloud AIO?
  </p>
  <div class="prose-kitchen text-sm">Nextcloud also offers an &ldquo;All-in-One&rdquo; package (AIO). But it brings its <strong>own TLS and own
ports</strong> and wants to handle the reverse-proxy part itself – that clashes with an existing
Traefik. For our setup, the classic image is the right, controllable choice.</div>
</div>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A running <strong>Traefik reverse proxy</strong> with the shared <code>proxy</code> network and the Let&rsquo;s Encrypt
resolver <code>le</code> – set up as in the tutorial
<a href="/en/tutorials/traefik-reverse-proxy/">reverse proxy with Traefik</a>.</li>
<li>A subdomain <code>cloud.YOUR_DOMAIN</code> whose DNS record (A/AAAA) points to your server IP – see
<a href="/en/tutorials/connect-domain-to-server/">connecting a domain to your server</a>.</li>
<li>A working <strong>backup</strong>. A cloud is the place where data loss hurts most – first set up
<a href="/en/tutorials/restic-backups/">backups with Restic</a>.</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>How big does the server need to be?
  </p>
  <div class="prose-kitchen text-sm">Nextcloud is more frugal than its reputation: for private use or a small family (1–5 users),
<strong>2 vCPU and 4 GB RAM</strong> are quite enough – the tested VPS 1000 fits. The bottleneck is
memory: with too little RAM the server starts swapping and gets sluggish. Redis (we build it
in shortly) relieves this noticeably. For Nextcloud Office/Collabora or many parallel users
you should rather plan for 8 GB.</div>
</div>
<p>How big your server should be for your user count is estimated by the
<a href="/en/server-calculator/">server calculator</a> in a few clicks.</p>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCores · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €10.36/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">For a private Nextcloud with a few users, the VPS 1000 is quite enough.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-1000-g12-iv-12m?ref=44083" rel="sponsored noopener" target="_blank"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Go to netcup →
</a>

  </div><div class="px-4 pb-4"><p class="not-prose my-3 flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200">
  <span>💶 <strong>5 € voucher</strong> for new netcup customers:</span>
  <code class="rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</code>
  <span class="text-xs text-slate-500 dark:text-slate-400">(new customers only, no domains)</span>
</p></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-create-the-dns-record">Step 1: Create the DNS record</h3>
<p>At your DNS provider, create an entry <code>cloud.YOUR_DOMAIN</code> that points to your server IP
(A record for IPv4, AAAA for IPv6). Check that it resolves:</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">dig +short cloud.YOUR_DOMAIN</span></span></code></pre></div>
</div>
<p>You should see your server IP. Only once the record is set can Traefik fetch the HTTPS
certificate later.</p>
<h3 id="step-2-determine-the-proxy-subnet">Step 2: Determine the proxy subnet</h3>
<p>Nextcloud sits behind Traefik. So that Nextcloud&rsquo;s brute-force protection recognizes the
<strong>real</strong> visitor IP (and doesn&rsquo;t ban the internal Traefik IP), we have to enter Traefik as a
&ldquo;trusted proxy&rdquo;. For that you need the subnet of your <code>proxy</code> network:</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 network inspect proxy -f <span class="s1">&#39;{{(index .IPAM.Config 0).Subnet}}&#39;</span></span></span></code></pre></div>
</div>
<p>You get something like <code>172.18.0.0/16</code>. <strong>Remember this value</strong> – it goes into the
configuration as <code>TRUSTED_PROXIES</code> shortly.</p>
<h3 id="step-3-create-the-composeyaml">Step 3: Create the compose.yaml</h3>
<p>Create a folder and change into it:</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">mkdir -p ~/nextcloud <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/nextcloud</span></span></code></pre></div>
</div>
<p>Create the <code>compose.yaml</code>. Replace <code>cloud.YOUR_DOMAIN</code>, all passwords and the
<code>TRUSTED_PROXIES</code> subnet from step 2:</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="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nc-db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mariadb:11.4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">nc-db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span>--<span class="l">transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MARIADB_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">A_STRONG_ROOT_PASSWORD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MARIADB_DATABASE</span><span class="p">:</span><span class="w"> </span><span class="l">nextcloud</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MARIADB_USER</span><span class="p">:</span><span class="w"> </span><span class="l">nextcloud</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MARIADB_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">A_STRONG_DB_PASSWORD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">nc_db:/var/lib/mysql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;healthcheck.sh&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--connect&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--innodb_initialized&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">default]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nc-redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">redis:8-alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">nc-redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">redis-server --requirepass A_STRONG_REDIS_PASSWORD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">default]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nc-app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nextcloud:34-apache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">nc-app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">nc-db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">nc-redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_started</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MYSQL_HOST</span><span class="p">:</span><span class="w"> </span><span class="l">nc-db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MYSQL_DATABASE</span><span class="p">:</span><span class="w"> </span><span class="l">nextcloud</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MYSQL_USER</span><span class="p">:</span><span class="w"> </span><span class="l">nextcloud</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MYSQL_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">A_STRONG_DB_PASSWORD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">REDIS_HOST</span><span class="p">:</span><span class="w"> </span><span class="l">nc-redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">REDIS_HOST_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">A_STRONG_REDIS_PASSWORD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">NEXTCLOUD_TRUSTED_DOMAINS</span><span class="p">:</span><span class="w"> </span><span class="l">cloud.YOUR_DOMAIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">OVERWRITEPROTOCOL</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">OVERWRITECLIURL</span><span class="p">:</span><span class="w"> </span><span class="l">https://cloud.YOUR_DOMAIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">TRUSTED_PROXIES</span><span class="p">:</span><span class="w"> </span><span class="m">172.18.0.0</span><span class="l">/16</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PHP_MEMORY_LIMIT</span><span class="p">:</span><span class="w"> </span><span class="l">1024M</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PHP_UPLOAD_LIMIT</span><span class="p">:</span><span class="w"> </span><span class="l">10G</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">nc_html:/var/www/html</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.nc.rule=Host(`cloud.YOUR_DOMAIN`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.nc.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.nc.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.nc.middlewares=nc-dav&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.nc.loadbalancer.server.port=80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># .well-known redirect for CalDAV/CardDAV (see below)</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-dav.redirectregex.regex=https://(.*)/.well-known/(?:card|cal)dav&#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-dav.redirectregex.replacement=https://cloud.YOUR_DOMAIN/remote.php/dav/&#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-dav.redirectregex.permanent=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">default, proxy]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nc_html</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nc_db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
</div>
<p>What matters here:</p>
<ul>
<li><strong>Three containers:</strong> <code>nc-app</code> (Nextcloud), <code>nc-db</code> (MariaDB), <code>nc-redis</code>. Only <code>nc-app</code> is
on the <strong><code>proxy</code> network</strong> (for Traefik) <em>and</em> on the internal <code>default</code> network. Database
and Redis stay exclusively internal – they have <strong>no <code>ports:</code></strong> and are not reachable from
outside.</li>
<li><strong>Two named volumes:</strong> <code>nc_html</code> (program code, <code>config.php</code>, user data) and <code>nc_db</code> (the
MariaDB data). The <code>nc_db</code> volume is mandatory: without the entry, the database lands in an
<strong>anonymous</strong> volume, and a <code>docker compose down</code> followed by <code>up</code> would <strong>lose it
irrecoverably</strong>.</li>
<li><strong><code>healthcheck</code> + <code>depends_on: service_healthy</code>:</strong> Nextcloud starts faster than the
database initializes. Without this healthcheck the initial install fails with a &ldquo;Connection
refused&rdquo;. This way the app container waits until MariaDB is really ready.</li>
<li><strong><code>NEXTCLOUD_TRUSTED_DOMAINS</code>:</strong> without your own domain here, Nextcloud greets you with
&ldquo;Access through untrusted domain&rdquo;.</li>
<li><strong><code>OVERWRITEPROTOCOL: https</code></strong> tells Nextcloud it runs behind HTTPS – otherwise it builds
internal links as <code>http://</code> and logins/redirects break.</li>
<li><strong><code>TRUSTED_PROXIES</code>:</strong> the subnet from step 2. This lets Nextcloud recognize the real client
IP; otherwise the brute-force protection bans the Traefik IP.</li>
<li><strong><code>PHP_MEMORY_LIMIT</code> / <code>PHP_UPLOAD_LIMIT</code>:</strong> the defaults (512 MB) are too small for large
uploads. Set generously here.</li>
<li><strong><code>loadbalancer.server.port=80</code>:</strong> the Apache image listens on port 80 in the container.</li>
<li><strong>The <code>nc-dav</code> middleware</strong> solves a classic Nextcloud problem – more on that shortly.</li>
</ul>
<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>CalDAV/CardDAV: the .well-known redirect
  </p>
  <div class="prose-kitchen text-sm">Nextcloud wants to send calendar/contact clients via <code>/.well-known/caldav</code> and
<code>/.well-known/carddav</code> to <code>/remote.php/dav/</code>. Behind a reverse proxy, Nextcloud&rsquo;s own
redirect doesn&rsquo;t work reliably – this later leads to a warning in the security check and to
problems with calendar setup. The <code>nc-dav</code> middleware above does the redirect directly in
Traefik. Important: the pattern must match the <strong>full <code>https://…</code> URL</strong> – <code>redirectregex</code>
always checks the complete URL, not just the path. A pure path pattern like <code>^/.well-known/…</code>
therefore never matches.</div>
</div>
<h3 id="step-4-start-and-run-through-the-initial-setup">Step 4: Start and run through the initial setup</h3>
<p>Start the stack:</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</span></span></code></pre></div>
</div>
<p>On the first start, MariaDB initializes the database and Nextcloud unpacks itself – that
takes a minute or two. Follow it in the log:</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 logs -f nc-app</span></span></code></pre></div>
</div>
<p>On the first run the image unpacks Nextcloud into the volume; you see lines like
<code>Initializing nextcloud 34.0.1.2 ...</code> and finally <code>Initializing finished</code>, followed by the
Apache start. If <code>restarting</code> appears repeatedly instead, take a look at the &ldquo;When things go
wrong&rdquo; section. With <code>Ctrl+C</code> you leave the log view again (the container keeps running).</p>
<p>Then open <code>https://cloud.YOUR_DOMAIN</code>. Traefik fetches the certificate on the first access
(can take a few seconds). Because we already configured the database and Redis via
environment variables, Nextcloud detects that automatically and only asks for an
<strong>administration account</strong>:</p>
<p><figure class="my-6"><img src="/en/tutorials/self-host-nextcloud/nextcloud-setup_hu_c50c35957723d189.webp" srcset="/en/tutorials/self-host-nextcloud/nextcloud-setup_hu_41179e40d0ac8fe6.webp 480w, /en/tutorials/self-host-nextcloud/nextcloud-setup_hu_c50c35957723d189.webp 768w, /en/tutorials/self-host-nextcloud/nextcloud-setup_hu_64a4383ace5c261b.webp 1200w, /en/tutorials/self-host-nextcloud/nextcloud-setup_hu_ab4c44f37cb05b2f.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/self-host-nextcloud/nextcloud-setup_hu_a6187c5c382c3ab8.webp"
    alt="The Nextcloud initial setup detects the autoconfig and only asks for the name and password of the administration account" title="Initial setup: the database is preconfigured, you only create the admin account"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Initial setup: the database is preconfigured, you only create the admin account</figcaption></figure></p>
<p>Set an admin name and a strong password and click <strong>Install</strong>. Nextcloud sets up the instance
– after that you land on the login page:</p>
<p><figure class="my-6"><img src="/en/tutorials/self-host-nextcloud/nextcloud-login_hu_ab5d9fc01a9c0266.webp" srcset="/en/tutorials/self-host-nextcloud/nextcloud-login_hu_5703a02035c4a1d2.webp 480w, /en/tutorials/self-host-nextcloud/nextcloud-login_hu_ab5d9fc01a9c0266.webp 768w, /en/tutorials/self-host-nextcloud/nextcloud-login_hu_1328b37d05a830c5.webp 1200w, /en/tutorials/self-host-nextcloud/nextcloud-login_hu_82532d9c9d89c6e1.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/self-host-nextcloud/nextcloud-login_hu_ff7e27fbd50c96bb.webp"
    alt="The Nextcloud login page under your own HTTPS domain" title="The login page at cloud.YOUR_DOMAIN – with valid HTTPS from Traefik"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The login page at cloud.YOUR_DOMAIN – with valid HTTPS from Traefik</figcaption></figure></p>
<p>After the login, the <strong>dashboard</strong> greets you, and under <strong>Files</strong> you find your cloud with a
few example files:</p>
<p><figure class="my-6"><img src="/en/tutorials/self-host-nextcloud/nextcloud-dashboard_hu_db3401a59a72d3bd.webp" srcset="/en/tutorials/self-host-nextcloud/nextcloud-dashboard_hu_c4f9d6d8f49e635d.webp 480w, /en/tutorials/self-host-nextcloud/nextcloud-dashboard_hu_db3401a59a72d3bd.webp 768w, /en/tutorials/self-host-nextcloud/nextcloud-dashboard_hu_9086315a95ddf210.webp 1200w, /en/tutorials/self-host-nextcloud/nextcloud-dashboard_hu_860e5d90f28daa69.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/self-host-nextcloud/nextcloud-dashboard_hu_f4c7dc282828c0f.webp"
    alt="The Nextcloud dashboard with the &ldquo;Recommended files&rdquo; widget" title="The dashboard after the first login"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The dashboard after the first login</figcaption></figure></p>
<p><figure class="my-6"><img src="/en/tutorials/self-host-nextcloud/nextcloud-dateien_hu_c6651a5b12cc3b8d.webp" srcset="/en/tutorials/self-host-nextcloud/nextcloud-dateien_hu_c2ff2f12d453b9c0.webp 480w, /en/tutorials/self-host-nextcloud/nextcloud-dateien_hu_c6651a5b12cc3b8d.webp 768w, /en/tutorials/self-host-nextcloud/nextcloud-dateien_hu_b5d67ad5d77fc9b.webp 1200w, /en/tutorials/self-host-nextcloud/nextcloud-dateien_hu_a5c245e619d92d9f.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/self-host-nextcloud/nextcloud-dateien_hu_f58abd3991247124.webp"
    alt="The Files interface of Nextcloud with folders and example files" title="The Files app – the heart of your own cloud"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The Files app – the heart of your own cloud</figcaption></figure></p>
<h3 id="step-5-set-up-background-jobs-cron">Step 5: Set up background jobs (cron)</h3>
<p>Nextcloud has to perform tasks regularly (cleanup, notifications, thumbnails). By default
this happens via &ldquo;AJAX&rdquo; on every page load – which is unreliable. The recommended way is a
<strong>cron sidecar</strong>: a second container with the same image that only runs <code>cron.php</code>. Add a
service to the <code>compose.yaml</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="nt">nc-cron</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nextcloud:34-apache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">nc-cron</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">/cron.sh</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">nc-db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">nc_html:/var/www/html</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">default]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span></span></span></code></pre></div>
</div>
<p>The sidecar shares the <code>nc_html</code> volume with the app and runs the jobs every five minutes.
Apply the change and set the mode in Nextcloud to &ldquo;Cron&rdquo;:</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
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ background:cron</span></span></code></pre></div>
</div>
<p>Under <strong>Administration → Basic settings</strong>, &ldquo;Cron&rdquo; should now be active.</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>occ – the command-line tool
  </p>
  <div class="prose-kitchen text-sm"><p><code>occ</code> is Nextcloud&rsquo;s admin tool. You always call it as the user <code>www-data</code> in the app
container: <code>docker exec -u www-data nc-app php occ &lt;command&gt;</code>. Two useful cleanup commands
right after installation:</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 db:add-missing-indices
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ config:system:set default_phone_region --value<span class="o">=</span>DE</span></span></code></pre></div>
</div></div>
</div>
<h3 id="step-6-connect-the-clients">Step 6: Connect the clients</h3>
<p>Now the real benefit. There are three ways to use your cloud:</p>
<ol>
<li><strong>Desktop sync client</strong> (Windows/macOS/Linux): install the &ldquo;Nextcloud Desktop&rdquo; client,
enter <code>https://cloud.YOUR_DOMAIN</code> as the server address, log in and choose a local folder.
Files are synced like with Dropbox. With <strong>virtual files</strong> (Windows/macOS) they only take
up space on the disk when you open them – handy when your cloud is larger than the local
disk.</li>
<li><strong>Phone app</strong> (Android/iOS): the official Nextcloud app, same server address. Ideal for
automatic <strong>photo upload</strong> from the smartphone.</li>
<li><strong>Calendar &amp; contacts:</strong> in your device&rsquo;s system settings, create a CalDAV/CardDAV account
with the address <code>https://cloud.YOUR_DOMAIN</code> – thanks to the <code>.well-known</code> middleware from
step 3, automatic detection works.</li>
</ol>
<p>Features like calendar, contacts or mail are separate <strong>apps</strong> you install with one click
under <strong>Administration → Apps</strong> – so Nextcloud is extensible without you touching anything on
the server.</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>Don&#39;t work with the admin account
  </p>
  <div class="prose-kitchen text-sm">The administration account from step 4 is meant for <strong>management</strong>, not for everyday use.
Create a normal user account under <strong>Administration → Accounts</strong> and sync your files through
it. That way your powerful admin account isn&rsquo;t permanently logged in on all devices – a
simple but effective security gain. For family or team, create further accounts here; each
gets its own encrypted area.</div>
</div>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>&ldquo;Access through untrusted domain&rdquo; instead of the login page.</strong> Your domain isn&rsquo;t in
<code>trusted_domains</code>. Check <code>NEXTCLOUD_TRUSTED_DOMAINS</code> in the Compose file. To set it
afterwards, use occ: <code>docker exec -u www-data nc-app php occ config:system:set trusted_domains 1 --value=cloud.YOUR_DOMAIN</code>.</p>
<p><strong>The security check reports &ldquo;Your web server is not set up properly to resolve
.well-known/caldav&rdquo;.</strong> The CalDAV/CardDAV redirect isn&rsquo;t working. Check the <code>nc-dav</code>
middleware labels (step 3) and that the router includes them via
<code>...routers.nc.middlewares=nc-dav</code>. The regex must match the full <code>https://…</code> URL –
<code>redirectregex</code> checks the complete URL, not just the path.</p>
<p><strong>Warning &ldquo;The &lsquo;Strict-Transport-Security&rsquo; HTTP header is not set&rdquo;.</strong> The HSTS header is
missing. Set it as a Traefik middleware and attach it to the router:
<code>traefik.http.middlewares.nc-secure.headers.stsSeconds=15552000</code>. Attach it additionally to
the <code>nc-dav</code> middleware on the router (<code>...routers.nc.middlewares=nc-dav,nc-secure</code>). The
header must come from the proxy, not from Nextcloud. In <a href="/en/tutorials/harden-optimize-nextcloud/">part
2</a> we set up exactly this <code>nc-secure</code> middleware
fully (including <code>includeSubdomains</code>).</p>
<p><strong>Large uploads break off or end with a timeout / &ldquo;413&rdquo;.</strong> The PHP limit is too small.
Increase <code>PHP_UPLOAD_LIMIT</code> and <code>PHP_MEMORY_LIMIT</code> (step 3) and restart the container. In
rare cases these variables don&rsquo;t take effect – then mount your own <code>php.ini</code> snippet into the
image.</p>
<p><strong>On the first start the installation aborts with &ldquo;MySQL server has gone away&rdquo; or &ldquo;Connection
refused&rdquo;.</strong> The app container was faster than the database. That&rsquo;s exactly what the
<code>healthcheck</code> with <code>depends_on: condition: service_healthy</code> is for – check that both are
present in your Compose file, and restart with <code>docker compose up -d</code>.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li>
<p><strong>Backups are mandatory for a cloud.</strong> A consistent backup needs <strong>three things</strong>: the
<strong>database</strong> (MariaDB dump), the <strong>data directory</strong> (your files) and the <strong>configuration</strong>
(<code>config.php</code>). Put Nextcloud into maintenance mode briefly before the backup so the
database and files match each other:</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:mode --on
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> nc-db mariadb-dump -u root -pA_STRONG_ROOT_PASSWORD nextcloud &gt; nextcloud-db.sql
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -u www-data nc-app php occ maintenance:mode --off</span></span></code></pre></div>
</div>
<p>Back up the dump together with the <code>nc_html</code> volume <strong>encrypted and off-site</strong> with
<a href="/en/tutorials/restic-backups/">Restic</a> – thanks to the dump you don&rsquo;t need to additionally
back up the raw <code>nc_db</code> volume. The <code>nc_html</code> volume contains both the program code and the
<code>config.php</code> as well as – under <code>data/</code> – <strong>your actual user files</strong>. If your cloud grows a
lot, you can later move this <code>data/</code> directory to a separate, larger volume; for now one
volume for everything is enough.</p>
</li>
<li>
<p><strong>Updates: only one major version per step.</strong> From 34 to 35, never directly to 36. Bump the
image tag (<code>nextcloud:35-apache</code>), then <code>docker compose pull</code> and <code>docker compose up -d</code>.
The container runs the necessary <code>occ upgrade</code> <strong>automatically</strong> on start. <strong>Always make a
backup first.</strong> Bump the cron sidecar to the same version.</p>
</li>
<li>
<p><strong>Check security.</strong> After setup, run the official
<a href="https://scan.nextcloud.com/">Nextcloud Security Scan</a> against your domain (target: grade A)
and work through the warnings from the admin overview. The overview under <strong>Administration →
Overview</strong> runs automatic checks and shows exactly what&rsquo;s still missing. Keep Nextcloud up
to date – security holes are closed promptly, but only if you update.</p>
</li>
<li>
<p><strong>Honest about the effort:</strong> a self-hosted cloud wants to be maintained. Reckon with an
update pass roughly monthly and a regular look at the admin overview. In return, the data
then really belongs to you.</p>
</li>
</ul>
]]></content:encoded></item></channel></rss>