<?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>Cms – Serverküche</title><link>https://serverkueche.de/tags/cms/</link><description>Cms – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>de-DE</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Tue, 22 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/tags/cms/index.xml" rel="self" type="application/rss+xml"/><item><title>WordPress mit Docker &amp; Traefik sauber betreiben</title><link>https://serverkueche.de/tutorials/wordpress-mit-docker/</link><pubDate>Tue, 22 Sep 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/wordpress-mit-docker/</guid><description>WordPress mit Docker und MariaDB hinter Traefik aufsetzen – mit HTTPS, isolierter Datenbank und ehrlicher Update-Strategie statt Wartungs-Albtraum.</description><content:encoded><![CDATA[<p>WordPress betreibt einen großen Teil des Webs – und ist berüchtigt für gehackte Installationen. Der Unterschied zwischen „läuft und ist sicher“ und „wird zur Spam-Schleuder“ liegt im sauberen Aufbau und in ehrlicher Wartung. Dieses Rezept liefert beides: WordPress mit Docker, isolierter Datenbank, HTTPS – und einer klaren Ansage, was du dauerhaft tun musst.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Eine produktionstaugliche WordPress-Installation mit <strong>WordPress 7.1.0</strong> (PHP 8.5) und <strong>MariaDB 11.8</strong> hinter <a href="/tutorials/reverse-proxy-traefik/">Traefik</a>. Die Datenbank läuft in einem <strong>abgeschotteten internen Netz</strong> ohne Internetzugang, WordPress erhält sein HTTPS von Traefik, und wir lösen den klassischen Reverse-Proxy-Stolperstein (Redirect-Schleife/Mixed-Content) sauber. Am Ende hast du einen laufenden Blog – und weißt, wie du ihn sicher hältst.</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>WordPress oder Hugo?
  </p>
  <div class="prose-kitchen text-sm">Wenn du „nur“ eine Website oder einen Blog ohne interaktive Funktionen brauchst, ist eine statische Seite <a href="/tutorials/eigene-website-mit-hugo/">mit Hugo</a> im Betrieb sicherer und wartungsärmer (keine Datenbank, keine Angriffsfläche). WordPress lohnt sich, wenn du das riesige Plugin-Ökosystem, Redakteurs-Workflows oder Shop-/Mitglieder-Funktionen brauchst. Dieses Tutorial nimmt WordPress ehrlich, inklusive seiner Wartungslast.</div>
</div>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Ein Server mit laufendem <a href="/tutorials/reverse-proxy-traefik/">Traefik</a> und <a href="/tutorials/docker-compose-grundlagen/">Docker Compose</a></li>
<li>Eine <a href="/tutorials/domain-mit-server-verbinden/">Domain, die auf den Server zeigt</a> – im Folgenden <code>DEINE_DOMAIN</code></li>
<li>Etwas mehr RAM als für einfache Dienste – WordPress + Datenbank + PHP laufen komfortabel ab dem VPS 2000</li>
</ul>
<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"
     data-track-content data-content-name="Affiliate-Box · /tutorials/wordpress-mit-docker/" data-content-piece="VPS 2000 G12.5">
  <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>🍳 Empfehlung</span>
    <span title="Mit * markierte Links sind Affiliate-Links.">Anzeige</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 2000 G12.5</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">8 vCore · 16 GB RAM · 256 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 26,92 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">WordPress mit Datenbank und PHP läuft entspannt auf dem VPS 2000.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-2000-g12.5-24m-eu?ref=44083#vps-2000-g12.5-12m-eu" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 2000 G12.5 · {page}"
   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">
  Zu netcup →
</a>

  </div><div class="px-4 pb-4"><div class="not-prose my-3 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">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1">
    <span>💶 <strong>5 € Gutschein</strong> für netcup-Neukunden:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Zum Kopieren klicken" class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</button>
    <span class="text-xs text-slate-500 dark:text-slate-400">(nicht für Domains und VPS Lite)</span>
  </p>
  <p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
    <a href="https://www.netcup.com/de/checkout/warenkorb?ref=44083" rel="sponsored noopener" target="_blank"
       data-track-event="Affiliate|netcup: Gutschein einlösen|5 € · {page}"
       class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Im Warenkorb einlösen →</a>
  </p>
</div></div>
</div>

<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="schritt-1-zwei-container-zwei-netze">Schritt 1: Zwei Container, zwei Netze</h3>
<p>WordPress besteht aus zwei Teilen: der PHP-Anwendung und einer <strong>MariaDB</strong>-Datenbank. Ein sauberes Setup trennt beide und schottet die Datenbank ab – sie soll nur von WordPress erreichbar sein und braucht selbst kein Internet. Genau dafür nutzen wir zwei Netze (siehe <a href="/tutorials/docker-netzwerke-verstehen/">Docker-Netzwerke verstehen</a>): das öffentliche <code>proxy</code>-Netz zwischen WordPress und Traefik, und ein <code>internal</code>-Netz zwischen WordPress und Datenbank.</p>
<p><figure class="my-6"><img src="wordpress-schema.svg" alt="Schema: Browser per HTTPS zu Traefik, von dort per HTTP mit X-Forwarded-Proto zu WordPress, WordPress per SQL zur MariaDB im internen Netz" title="Traefik terminiert TLS und meldet WordPress per X-Forwarded-Proto die HTTPS-Herkunft; die MariaDB liegt ohne Internetzugang im internen Netz" loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Traefik terminiert TLS und meldet WordPress per X-Forwarded-Proto die HTTPS-Herkunft; die MariaDB liegt ohne Internetzugang im internen Netz</figcaption></figure></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 /opt/wordpress <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /opt/wordpress</span></span></code></pre></div>
</div>
<h3 id="schritt-2-die-compose-datei">Schritt 2: Die Compose-Datei</h3>
<p>Ersetze <code>DEINE_DOMAIN</code> und alle Passwörter durch eigene Werte:</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">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.8</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 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_DATABASE</span><span class="p">:</span><span class="w"> </span><span class="l">wordpress</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">wordpress</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">DEIN_DB_PASSWORT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">MARIADB_RANDOM_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1&#34;</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">db_data:/var/lib/mysql</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">intern]</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">start_period</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">start_interval</span><span class="p">:</span><span class="w"> </span><span class="l">2s</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">30s</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">5</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">wordpress</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">wordpress:7.1.0-php8.5-apache</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 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">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">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">WORDPRESS_DB_HOST</span><span class="p">:</span><span class="w"> </span><span class="l">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">WORDPRESS_DB_USER</span><span class="p">:</span><span class="w"> </span><span class="l">wordpress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">WORDPRESS_DB_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">DEIN_DB_PASSWORT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">WORDPRESS_DB_NAME</span><span class="p">:</span><span class="w"> </span><span class="l">wordpress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">WORDPRESS_CONFIG_EXTRA</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">        if (isset($$_SERVER[&#39;HTTP_X_FORWARDED_PROTO&#39;]) &amp;&amp; $$_SERVER[&#39;HTTP_X_FORWARDED_PROTO&#39;] === &#39;https&#39;) { $$_SERVER[&#39;HTTPS&#39;] = &#39;on&#39;; }
</span></span></span><span class="line"><span class="cl"><span class="sd">        define(&#39;WP_HOME&#39;, &#39;https://DEINE_DOMAIN&#39;);
</span></span></span><span class="line"><span class="cl"><span class="sd">        define(&#39;WP_SITEURL&#39;, &#39;https://DEINE_DOMAIN&#39;);</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">wp_data:/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">proxy, intern]</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.wp.rule=Host(`DEINE_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.wp.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.wp.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.services.wp.loadbalancer.server.port=80&#34;</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">db_data</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">wp_data</span><span class="p">:</span><span class="w"> </span>{}<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">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 class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">intern</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">internal</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
</div>
<p>Drei Details sind hier entscheidend:</p>
<ul>
<li><strong><code>depends_on: condition: service_healthy</code></strong> – WordPress startet erst, wenn die Datenbank per Healthcheck als bereit gemeldet wird. Ohne das scheitert der erste Start in einer Race Condition.</li>
<li><strong><code>intern</code> ist <code>internal: true</code></strong> – die Datenbank hängt <em>nur</em> in diesem Netz und hat damit keinen Internetzugang; WordPress erreicht sie unter dem Hostnamen <code>db</code>.</li>
<li><strong><code>WORDPRESS_CONFIG_EXTRA</code></strong> – der wichtigste Teil (nächster Schritt).</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>Der Reverse-Proxy-Stolperstein: HTTPS erkennen
  </p>
  <div class="prose-kitchen text-sm">Traefik terminiert TLS und spricht intern per HTTP mit WordPress. WordPress denkt deshalb, es liefe unsicher, und erzeugt <code>http://</code>-Links – das führt zu <strong>Mixed-Content-Fehlern</strong> oder einer <strong>Redirect-Schleife</strong> (<code>ERR_TOO_MANY_REDIRECTS</code>). Die Zeile <code>if (... HTTP_X_FORWARDED_PROTO ... === 'https') { $_SERVER['HTTPS'] = 'on'; }</code> sagt WordPress, dass die ursprüngliche Verbindung HTTPS war. Zusammen mit <code>WP_HOME</code>/<code>WP_SITEURL</code> ist das die saubere Lösung. In Compose müssen die PHP-Variablen als <code>$$</code> geschrieben werden, sonst interpretiert Docker sie als eigene Variablen.</div>
</div>
<h3 id="schritt-3-starten">Schritt 3: Starten</h3>
<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>Docker startet zuerst die Datenbank, wartet auf ihren Healthcheck und startet dann WordPress. Prüfe:</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 ps</span></span></code></pre></div>
</div>
<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">NAME                    IMAGE                           SERVICE     STATUS
</span></span><span class="line"><span class="cl">wordpress-db-1          mariadb:11.8                    db          Up (healthy)
</span></span><span class="line"><span class="cl">wordpress-wordpress-1   wordpress:7.1.0-php8.5-apache   wordpress   Up</span></span></code></pre></div>
</div>
<p>Prüfe von außen, dass WordPress mit gültigem HTTPS antwortet (die 302 leitet zur 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">curl -sI https://DEINE_DOMAIN/ <span class="p">|</span> head -1</span></span></code></pre></div>
</div>
<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">HTTP/2 302</span></span></code></pre></div>
</div>
<h3 id="schritt-4-die-berühmte-5-minuten-installation">Schritt 4: Die berühmte 5-Minuten-Installation</h3>
<p>Öffne <code>https://DEINE_DOMAIN/</code>. WordPress leitet zum Installationsassistenten. Nach der Sprachwahl (Deutsch) füllst du die Grunddaten aus:</p>
<p><figure class="my-6"><img src="/tutorials/wordpress-mit-docker/wp-install_hu_b47848176a26413b.webp" srcset="/tutorials/wordpress-mit-docker/wp-install_hu_8b5e1c4e018763eb.webp 480w, /tutorials/wordpress-mit-docker/wp-install_hu_b47848176a26413b.webp 768w, /tutorials/wordpress-mit-docker/wp-install_hu_f4bb6ce882f2e5bb.webp 1200w, /tutorials/wordpress-mit-docker/wp-install_hu_6b1fb4664d975647.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/wordpress-mit-docker/wp-install_hu_47fd6c7bb056c68.webp"
    alt="Der WordPress-Installationsassistent mit Feldern für Titel, Benutzername, Passwort und E-Mail" title="Die 5-Minuten-Installation: Website-Titel, Admin-Konto und E-Mail"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Die 5-Minuten-Installation: Website-Titel, Admin-Konto und E-Mail</figcaption></figure></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>Kein „admin“ als Benutzername
  </p>
  <div class="prose-kitchen text-sm">Wähle <strong>nicht</strong> den Benutzernamen <code>admin</code> – er ist das erste Ziel jedes Brute-Force-Angriffs auf WordPress. Nimm einen eigenen Namen und ein starkes Passwort. Das ist die einfachste und wirksamste Härtungsmaßnahme überhaupt.</div>
</div>
<p>Nach dem Klick auf „WordPress installieren“ und dem ersten Login landest du im Dashboard:</p>
<p><figure class="my-6"><img src="/tutorials/wordpress-mit-docker/wp-dashboard_hu_1692b0ee98741d7c.webp" srcset="/tutorials/wordpress-mit-docker/wp-dashboard_hu_86c97ff520835c54.webp 480w, /tutorials/wordpress-mit-docker/wp-dashboard_hu_1692b0ee98741d7c.webp 768w, /tutorials/wordpress-mit-docker/wp-dashboard_hu_6eac735e1a9dba35.webp 1200w, /tutorials/wordpress-mit-docker/wp-dashboard_hu_3faab59025abce14.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/wordpress-mit-docker/wp-dashboard_hu_876e51fc7e0601a7.webp"
    alt="Das WordPress-Dashboard nach der Installation, Version 7.1 mit dem Theme Twenty Twenty-Five" title="Das Dashboard: hier verwaltest du Beiträge, Seiten, Design und Plugins"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Das Dashboard: hier verwaltest du Beiträge, Seiten, Design und Plugins</figcaption></figure></p>
<h3 id="schritt-5-die-website-ist-live">Schritt 5: Die Website ist live</h3>
<p>Deine Seite ist sofort öffentlich erreichbar – mit gültigem HTTPS von Traefik, ohne Mixed-Content-Fehler dank der Proxy-Konfiguration:</p>
<p><figure class="my-6"><img src="/tutorials/wordpress-mit-docker/wp-frontend_hu_cbaf94c399fbe57.webp" srcset="/tutorials/wordpress-mit-docker/wp-frontend_hu_2b176ffbcdd51711.webp 480w, /tutorials/wordpress-mit-docker/wp-frontend_hu_cbaf94c399fbe57.webp 768w, /tutorials/wordpress-mit-docker/wp-frontend_hu_5af1d3fe821db4dd.webp 1200w, /tutorials/wordpress-mit-docker/wp-frontend_hu_413154f930c4ba93.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/wordpress-mit-docker/wp-frontend_hu_2bc866196aa1e212.webp"
    alt="Das Frontend der frisch installierten WordPress-Seite mit dem ersten Beitrag „Hallo Welt!&quot;" title="Das Frontend unter der eigenen Domain – der Standard-Beitrag „Hallo Welt!“"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Das Frontend unter der eigenen Domain – der Standard-Beitrag „Hallo Welt!“</figcaption></figure></p>
<h3 id="schritt-6-grundlegende-härtung">Schritt 6: Grundlegende Härtung</h3>
<p>WordPress ist ab Werk funktional, aber nicht optimal abgesichert. Die wichtigsten Sofortmaßnahmen:</p>
<ul>
<li><strong>Automatische Updates aktivieren:</strong> Unter <strong>Dashboard → Aktualisierungen</strong> sicherstellen, dass zumindest Sicherheitsupdates automatisch eingespielt werden.</li>
<li><strong>Unnötige Standard-Inhalte löschen:</strong> Der „Hallo Welt!“-Beitrag, die Beispiel-Seite und das mitgelieferte Beispiel-Plugin (<code>Hello Dolly</code>) können weg.</li>
<li><strong>Weniger ist mehr bei Plugins:</strong> Jedes Plugin ist zusätzliche Angriffsfläche. Installiere nur, was du wirklich brauchst, und nur aus vertrauenswürdiger Quelle.</li>
<li><strong>Login zusätzlich schützen:</strong> Vor die WordPress-Anmeldung (<code>/wp-login.php</code>) gehört idealerweise eine weitere Hürde. Bewährt sind eine Rate-Limit-/BasicAuth-Middleware in Traefik oder ein 2FA-Plugin. Auch <a href="/tutorials/fail2ban-einrichten/">Fail2ban</a> kann WordPress-Login-Versuche auswerten.</li>
<li><strong>XML-RPC im Blick behalten:</strong> Die Datei <code>xmlrpc.php</code> ist ein häufiges Ziel für Brute-Force und DDoS. Wird sie nicht gebraucht, per Traefik-Middleware oder Plugin sperren.</li>
</ul>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<div class="troubleshoot not-prose">
<p><strong>Redirect-Schleife (<code>ERR_TOO_MANY_REDIRECTS</code>) oder Mixed-Content-Warnungen.</strong> Die HTTPS-Erkennung fehlt. Prüfe, dass der <code>WORDPRESS_CONFIG_EXTRA</code>-Block mit <code>HTTP_X_FORWARDED_PROTO</code> und den <code>$$</code>-Zeichen exakt gesetzt ist (siehe Warnung in Schritt 2), und starte den WordPress-Container neu.</p>
<p><strong>WordPress startet nicht, meldet „Error establishing a database connection“.</strong> Meist eine Race Condition oder ein Passwort-Mismatch. Prüfe, dass <code>depends_on: condition: service_healthy</code> gesetzt ist und <code>WORDPRESS_DB_PASSWORD</code> exakt <code>MARIADB_PASSWORD</code> entspricht. Logs: <code>docker compose logs db</code>.</p>
<p><strong>Der erste Start scheitert, obwohl später alles läuft.</strong> Die Datenbank braucht beim allerersten Start einige Sekunden zur Initialisierung. Mit dem Healthcheck ist das abgedeckt; ohne ihn hilft ein erneutes <code>docker compose up -d</code>.</p>
<p><strong>Medien-Uploads schlagen bei großen Dateien fehl.</strong> PHP begrenzt die Upload-Größe. Lege eine eigene <code>uploads.ini</code> an und mounte sie nach <code>/usr/local/etc/php/conf.d/</code> mit z. B. <code>upload_max_filesize = 64M</code> und <code>post_max_size = 64M</code>.</p>
<p><strong>Nach einem Domainwechsel zeigt die Seite ins Leere.</strong> <code>WP_HOME</code>/<code>WP_SITEURL</code> in der Compose-Datei stehen fest verdrahtet – bei einer neuen Domain dort anpassen und Container neu starten (die Werte überschreiben die Datenbank-Einstellung).</p>

</div>

<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<ul>
<li><strong>Ehrlich: WordPress ist Wartungsarbeit.</strong> Anders als eine statische Seite läuft hier aktiver Code mit Datenbank – Kern, Themes und Plugins bekommen regelmäßig Sicherheitsupdates, die <strong>zeitnah</strong> eingespielt werden müssen. Ein vernachlässigtes WordPress wird zuverlässig gekapert. Aktiviere automatische Updates und prüfe monatlich den Aktualisierungsstatus. Den Container-Tag (<code>wordpress:7.1.0-php8.5-apache</code>) hältst du über deinen normalen <a href="/tutorials/docker-stack-aktuell-halten/">Update-Prozess</a> aktuell.</li>
<li><strong>Backup ist zweiteilig – beides ist Pflicht.</strong> WordPress besteht aus <strong>Datenbank</strong> (Beiträge, Einstellungen, Nutzer) <em>und</em> <strong>Dateien</strong> (<code>wp_data</code>-Volume: Uploads, Themes, Plugins). Ein Restic-Snapshot des laufenden MariaDB-Volumes allein ist keine Garantie – sichere die Datenbank zusätzlich per Dump. Wichtig sind dabei <code>-T</code> (sonst hängt der Befehl an der Passwortabfrage und legt eine <strong>leere</strong> Datei an) und das Passwort direkt am <code>-p</code>, ohne Leerzeichen:</li>
</ul>
<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 <span class="nb">exec</span> -T db mariadb-dump -u wordpress -p<span class="s1">&#39;DEIN_DB_PASSWORT&#39;</span> --single-transaction wordpress &gt; backup.sql</span></span></code></pre></div>
</div>
<p>Prüfe danach unbedingt, dass die Datei nicht leer ist (<code>ls -l backup.sql</code>). Dump <strong>und</strong> <code>wp_data</code>-Volume gehören ins <a href="/tutorials/backups-mit-restic/">Restic-Backup</a>.</p>
<ul>
<li><strong>Wiederherstellung testen.</strong> Ein Backup, das nie zurückgespielt wurde, ist nur eine Hoffnung – spiele einen Restore mindestens einmal probeweise durch, damit du im Ernstfall weißt, dass Dump und Dateien zusammenpassen.</li>
</ul>
]]></content:encoded></item></channel></rss>