Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Cloud / Hosting 12.11.2026 · 12 min Lesezeit Geprüft 12.11.2026

WordPress auf 1 GB RAM: Bricht der Mini-Server ein? Lasttest mit und ohne Grenzen

Ohne Grenzen friert ein Server mit 1 GB RAM unter WordPress-Last minutenlang ein, dann beendet der Kernel die Datenbank. Mit festen Apache-Prozessen und Speichergrenzen bleibt er erreichbar, mit Cache Enabler schafft er 50 statt 4 Aufrufe pro Sekunde.

Mit KI erstellt – redaktionelle Prüfung ausstehend

Werbung Mit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Symbolbild: kleiner Server mit grünem Auslastungsbalken

WordPress mit Datenbank auf einem Server mit 1 GB RAM und einer vCPU: Läuft das, und was passiert, wenn mehr Besucher kommen, als der Server schafft? Diese Anleitung misst es auf einem netcup VPS piko G11s nach. Ein zweiter Server schickt mit k6 stufenweise mehr Seitenaufrufe, erst an WordPress in der Standardeinstellung des offiziellen Docker-Images, dann mit Grenzen für Apache und Speicher, zuletzt mit einem Seiten-Cache.

Dies ist Teil 3 der Reihe „Mini-Server“. In Teil 2 schaffte derselbe Server eine statische Website mit rund 1.450 Aufrufen pro Sekunde. WordPress ist ein ganz anderes Kaliber.

Was Sie brauchen

  • Einen Server mit Docker und 1 GB RAM, hier der Mini-Server aus Teil 1. Lasttests nur auf eigenen Systemen und nur kurz.
  • Eine Domain mit A-Eintrag auf den Server, Ports 80 und 443 offen.
  • Einen zweiten Rechner mit Docker als Lastgenerator, wie in Teil 2 beschrieben.
  • Grundwissen zu Docker, siehe Docker Compose unter Linux.

Schritt 1: WordPress mit Datenbank und Caddy

Der Stack besteht aus Caddy für HTTPS, MariaDB 11.4 und dem offiziellen Image wordpress:php8.3-apache, dazu WP-CLI für die Einrichtung. Er liegt in /root/mini/wp. Zuerst die Passwörter als Dateien anlegen. Die Datei .db-pw muss für den Webserver im Container lesbar sein:

# mkdir -p /root/mini/wp && cd /root/mini/wp
# umask 077
# openssl rand -base64 24 > .db-pw
# openssl rand -base64 24 > .db-root-pw
# openssl rand -base64 24 > .wp-pw
# chmod 444 .db-pw
# mkdir -p html db && chown 33:33 html

Die Datei compose.yaml in der Standardeinstellung, also ohne Grenzen für Speicher und Prozesse:

name: mini-wp
services:
  caddy:
    image: caddy:2.11.7-alpine
    restart: unless-stopped
    ports: ["80:80", "443:443", "443:443/udp"]
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
  db:
    image: mariadb:11.4
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: wp
      MARIADB_USER: wp
      MARIADB_PASSWORD_FILE: /run/secrets/db_pw
      MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_pw
    secrets: [db_pw, db_root_pw]
    volumes: [./db:/var/lib/mysql]
  wp:
    image: wordpress:php8.3-apache
    restart: unless-stopped
    depends_on: [db]
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: wp
      WORDPRESS_DB_USER: wp
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_pw
    secrets: [db_pw]
    volumes:
      - ./html:/var/www/html
  cli:
    image: wordpress:cli-php8.3
    profiles: [cli]
    user: "33:33"
    depends_on: [db]
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: wp
      WORDPRESS_DB_USER: wp
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_pw
    secrets: [db_pw]
    volumes:
      - ./html:/var/www/html
volumes:
  caddy_data:
secrets:
  db_pw: {file: ./.db-pw}
  db_root_pw: {file: ./.db-root-pw}

Caddy leitet alles an WordPress weiter und sperrt Anmeldung und Verwaltung von außen, eingerichtet wird mit WP-CLI. Die Sperre von /wp-admin trifft auch admin-ajax.php, das manche Themes im Frontend nutzen, für den Test spielt das keine Rolle. Die Datei Caddyfile, mit Ihrer Domain statt des Beispielnamens:

mini.example.com {
	@gesperrt path /xmlrpc.php /wp-login.php /wp-admin*
	respond @gesperrt 403
	reverse_proxy wp:80
}
# docker compose up -d

Verifizieren: Alle drei Dienste laufen, im Leerlauf belegen WordPress und MariaDB je rund 100 MiB.

# docker stats --no-stream --format '{{.Name}} {{.MemUsage}}'
mini-wp-wp-1 97.27MiB / 957.5MiB
mini-wp-caddy-1 16.21MiB / 957.5MiB
mini-wp-db-1 96.59MiB / 957.5MiB

Schritt 2: WordPress einrichten und Messbeiträge anlegen

WP-CLI installiert WordPress, stellt sprechende Adressen ein und legt 20 Beiträge mit je sechs Absätzen Fülltext an. Warten Sie nach docker compose up -d etwa 30 Sekunden, bis die Datenbank bereit ist und html/wp-config.php existiert, sonst meldet WP-CLI einen Datenbankfehler. Speichern Sie das Skript als einrichten.sh und starten Sie es mit bash einrichten.sh:

#!/bin/bash
cd /root/mini/wp
wp() { docker compose run --rm -T cli wp "$@"; }
wp core install --url=https://mini.example.com --title="Mini-Server Messung" \
  --admin_user=messung --admin_password="$(cat .wp-pw)" --admin_email=admin@example.com --skip-email
wp rewrite structure '/%postname%/'
A="Dieser Absatz ist Fülltext für die Lastmessung. Er enthält genug Wörter, damit die Seite eine realistische Größe bekommt."
T="<!-- wp:paragraph --><p>$A $A $A</p><!-- /wp:paragraph -->"
for n in $(seq 1 20); do
  wp post create --post_status=publish --post_title="Messbeitrag $n" \
    --post_name="messbeitrag-$n" --post_content="$T$T$T$T$T$T" >/dev/null
done
wp post list --post_type=post --format=count

Verifizieren: Die letzte Zeile des Skripts gibt 21 aus, die 20 neuen Beiträge und den Beispielbeitrag. Ein Beitrag antwortet mit 200, die Anmeldeseite mit 403.

# curl -s -o /dev/null -w '%{http_code} %{size_download}\n' https://mini.example.com/messbeitrag-3/
# curl -s -o /dev/null -w '%{http_code}\n' https://mini.example.com/wp-login.php
200 73666
403

Die Startseite ist 123 KB groß, ein Beitrag 74 KB. Mit dem kompletten Stack im Leerlauf waren auf dem Server noch 392 MiB verfügbar, rund 190 MiB weniger als die rund 580 MiB aus Teil 1.

Schritt 3: Last und Beobachtung

Das Lastskript aus Teil 2 bekommt neue Pfade: die Startseite und die 20 Beiträge. Zwei Zähler halten fest, wie viele Anfragen in die Zeitgrenze von 10 Sekunden laufen und wie viele mit einem Serverfehler (5xx) enden. Die Datei last-wp.js:

import http from 'k6/http';
import { Counter } from 'k6/metrics';
const s5xx = new Counter('status_5xx'); const s0 = new Counter('status_timeout');
const ZIEL = 'https://mini.example.com';
const PFADE = ['/'].concat(Array.from({ length: 20 }, (_, i) => `/messbeitrag-${i + 1}/`));
export const options = {
  discardResponseBodies: true,
  scenarios: { last: { executor: 'constant-arrival-rate', rate: Number(__ENV.RATE), timeUnit: '1s',
    duration: '45s', preAllocatedVUs: Math.min(50, Number(__ENV.RATE)), maxVUs: 600 } },
  summaryTrendStats: ['avg', 'med', 'p(90)', 'p(95)', 'p(99)', 'max'],
};
export default function () {
  const p = PFADE[Math.floor(Math.random() * PFADE.length)];
  const r = http.get(ZIEL + p, { timeout: '10s', headers: { 'User-Agent': 'mini-server-messung/3 k6' } });
  if (r.status >= 500) s5xx.add(1);
  if (r.status === 0) s0.add(1);
}

Die Stufen fährt ein angepasstes Skript aus Teil 2. Es bricht ab, sobald mehr als 10 % der Anfragen scheitern oder 95 % der Antworten länger als 8 Sekunden brauchen, ohne Cache mit Raten von 1 bis 15, mit Cache von 10 bis 150 Aufrufen pro Sekunde. Den Durchgang mit Grenzen ohne Cache ließ es zum Vergleich ohne Abbruch bis 15 laufen. Auf dem Mini-Server schreibt eine kleine Schleife alle 2 Sekunden den verfügbaren Speicher, die Zahl der Apache-Prozesse und den Zähler des OOM-Killers mit, Datei /root/mini/probe.sh:

#!/bin/sh
while true; do
  A=$(awk '/MemAvailable/{print int($2/1024)}' /proc/meminfo)
  O=$(awk '/^oom_kill /{print $2}' /proc/vmstat)
  echo "$(date -u +%T) avail=$A oom=$O apache=$(pgrep -c apache2) load=$(cut -d' ' -f1 /proc/loadavg)"
  sleep 2
done
# chmod 700 /root/mini/probe.sh
# nohup timeout 1800 /root/mini/probe.sh > probe.log 2>&1 < /dev/null &

Verifizieren: Im Protokoll steht alle 2 Sekunden eine neue Zeile. Bleibt eine Lücke, war der Server zu beschäftigt, um die Schleife laufen zu lassen.

# tail -2 probe.log
15:46:42 avail=361 oom=0 apache=8 load=1.01
15:46:46 avail=361 oom=0 apache=8 load=0.93

Standard ohne Grenzen: Der Server friert ein

Bis 4 Aufrufe pro Sekunde lief alles sauber, 95 % der Antworten kamen in höchstens 440 Millisekunden, die CPU war zu rund 70 % beschäftigt. Bei 6 Aufrufen pro Sekunde kippte es. Weil die CPU nicht mehr nachkam, warteten immer mehr Anfragen, und Apache startete für jede einen eigenen Prozess. Das Image erlaubt bis zu 150. Innerhalb von 15 Sekunden stieg die Zahl von 37 auf 59 Prozesse, der verfügbare Speicher fiel von 197 auf 69 MiB.

Liniendiagramm: verfügbarer Speicher und Apache-Prozesse, in der Standardeinstellung fällt der Speicher auf nahe null und der Server antwortet 6 Minuten 39 Sekunden lang nicht, mit Grenzen bleibt er über 350 MiB
Verfügbarer Speicher und Apache-Prozesse: In der Standardeinstellung bricht der Speicher ein und der Server antwortet minutenlang nicht, mit Grenzen bleibt alles stabil

Danach kam aus der Prüfschleife 6 Minuten und 39 Sekunden lang kein einziger Messwert. Ab etwa einer Minute danach war der Server rund 5 Minuten lang auch per SSH nicht erreichbar, die Verbindung lief in eine Zeitüberschreitung. Ohne Swap hat der Kernel keinen Ausweg: Er wirft Programmcode aus dem Speicher, der Sekunden später wieder gebraucht wird, und verbringt fast seine ganze Zeit damit. Solange er dabei noch Dateiseiten verwerfen kann, gilt das für ihn als Fortschritt, deshalb greift der OOM-Killer erst spät ein. Hier dauerte es Minuten, und er traf nicht Apache, sondern die Datenbank. Die Angabe total-vm ist nur reservierter Adressraum, entscheidend ist anon-rss, also rund 102 MiB:

Out of memory: Killed process 82700 (mariadbd) total-vm:8590912036kB, anon-rss:104552kB, file-rss:128kB, shmem-rss:0kB, UID:999 pgtables:512kB oom_score_adj:0

Der Grund: Der Kernel bewertet jeden Prozess einzeln (oom_score, im Kern der belegte Arbeitsspeicher) und beendet den mit der höchsten Bewertung. Das war MariaDB mit 102 MiB, jeder Apache-Prozess war einzeln viel kleiner. Docker startete die Datenbank sofort neu. Rund anderthalb Minuten später war der Speicher wieder frei, die Last brauchte noch einige Minuten. In dieser Stufe brachen 61 % der Anfragen nach 10 Sekunden ab. Für Besucher heißt das: Die Website war mehrere Minuten weg, und der Admin kam nicht auf den Server, um etwas dagegen zu tun.

Schritt 4: Grenzen für Apache und Speicher

Mehr Apache-Prozesse helfen nicht, wenn die eine CPU ohnehin voll ist, sie verbrauchen nur Speicher. Deshalb bekommt Apache eine feste Obergrenze von 8 Prozessen. Die Datei mpm.conf:

<IfModule mpm_prefork_module>
	StartServers		 2
	MinSpareServers		 2
	MaxSpareServers		 4
	MaxRequestWorkers	 8
	MaxConnectionsPerChild   500
</IfModule>

In der compose.yaml bekommt der Dienst wp zusätzlich eine Speichergrenze und die Datei als Einstellung von Apache, der Dienst db ebenfalls eine Speichergrenze. Ergänzen Sie diese Zeilen in den bestehenden Diensten unter services:, die übrigen Einträge bleiben:

  db:
    mem_limit: 256m
  wp:
    mem_limit: 384m
    volumes:
      - ./html:/var/www/html
      - ./mpm.conf:/etc/apache2/mods-available/mpm_prefork.conf:ro
# docker compose up -d

Verifizieren: Apache hat die neue Obergrenze übernommen, und die Container zeigen ihre Speichergrenzen.

# docker compose exec -T wp grep MaxRequestWorkers /etc/apache2/mods-enabled/mpm_prefork.conf
# docker stats --no-stream --format '{{.Name}} {{.MemUsage}}'
MaxRequestWorkers	 8
mini-wp-wp-1 28.54MiB / 384MiB
mini-wp-db-1 101.1MiB / 256MiB
mini-wp-caddy-1 35.42MiB / 957.5MiB

Mit Grenzen: Überlast ohne Absturz

Die Grenze lag wieder bei 4 Aufrufen pro Sekunde, mehr gibt die eine CPU für WordPress ohne Cache nicht her. Darüber stiegen die Fehler: 10 % bei 5 Aufrufen pro Sekunde, 40 % bei 8, fast alle ab 10. Bis 10 Aufrufe pro Sekunde waren alle Fehler Zeitüberschreitungen, die Anfragen warteten in der Schlange vor Apache länger als 10 Sekunden. Erst bei 15 Aufrufen pro Sekunde kamen 22 Serverfehler dazu.

Der Unterschied liegt beim Server selbst. Apache blieb bei höchstens 9 Prozessen, 8 Arbeitsprozesse plus der Steuerprozess, der WordPress-Container bei 125 MiB, verfügbar waren nie weniger als 356 MiB. Die Prüfschleife lief ohne Lücke durch, kein Prozess wurde beendet, kein Container neu gestartet. Nach der Last sank die Systemlast innerhalb von rund zwei Minuten wieder auf Leerlauf. Die Grenzen machen WordPress nicht schneller, sie sorgen dafür, dass Überlast nur die Website bremst und nicht den ganzen Server lahmlegt.

Schritt 5: Seiten-Cache mit Cache Enabler

Ein Seiten-Cache speichert das fertige HTML und liefert es beim nächsten Aufruf ohne Datenbank und ohne Theme aus. Cache Enabler ist ein schlankes, kostenloses Plugin dafür:

# docker compose run --rm -T cli wp plugin install cache-enabler --activate

Verifizieren: Nach dem ersten Aufruf trägt die Antwort den Kopf x-cache-handler. Wichtig ist der Accept-Header, siehe Typische Fehler.

# curl -s -o /dev/null -D - -H 'Accept: text/html' https://mini.example.com/messbeitrag-3/ | grep -i x-cache-handler
x-cache-handler: cache-enabler-engine

Vom Server selbst abgerufen kam ein Beitrag aus dem Cache nach 90 Millisekunden statt nach 260.

Mit Cache: 50 Aufrufe pro Sekunde

Mit Cache hielt WordPress 50 Aufrufe pro Sekunde ohne Fehler, 95 % der Antworten kamen in 18 Millisekunden. Bei 100 pro Sekunde liefen 4,9 % der Anfragen in die Zeitgrenze, obwohl die CPU noch knapp ein Drittel frei hatte. Die Bremse waren die 8 Apache-Prozesse: Cache Enabler liefert die gespeicherten Seiten weiter über PHP und Apache aus. Mit 20 Prozessen sank der Fehleranteil bei 100 Aufrufen pro Sekunde auf 1,9 %, bei 150 pro Sekunde waren es 30 %. Der Speicher reichte auch dafür: Mit Cache brauchte jeder Prozess wenig, verfügbar blieben mindestens 281 MiB.

Balkendiagramm: höchste saubere Laststufe, ohne Cache 4 Aufrufe pro Sekunde, mit Cache 50
Höchste saubere Laststufe je Einstellung: ohne Cache 4 Aufrufe pro Sekunde, mit Cache 50
Einstellungsauber bis95 % schneller alsbei Überlastverfügbar mindestens
Standard ohne Grenzen4/s440 msServer rund 6 Minuten weg, Datenbank beendet0 MiB
8 Prozesse, Speichergrenzen4/s653 msZeitüberschreitungen, Server erreichbar356 MiB
Mit Cache, 8 Prozesse50/s18 ms4,9 % Fehler bei 100/s420 MiB
Mit Cache, 20 Prozesse50/s24 ms1,9 % Fehler bei 100/s281 MiB

Typische Fehler

Server per SSH nicht erreichbar, im Kernel-Protokoll steht danach Out of memory: Killed process … (mariadbd)

Apache hat ohne Obergrenze so viele Prozesse gestartet, dass der Speicher voll war. Setzen Sie MaxRequestWorkers passend zum Speicher und eine mem_limit für die Container wie in Schritt 4. Kommen Sie gar nicht mehr auf den Server, hilft ein Neustart über die Verwaltungsoberfläche des Anbieters.

Der Cache wirkt im Test mit curl nicht, die Seite kommt immer gleich langsam

Das Werkzeug curl sendet Accept: */*, und Cache Enabler lässt den Cache bei einem Accept-Header ohne text/html aus. Hier kam der Beitrag mit */* nach 260 Millisekunden, mit -H 'Accept: text/html' nach 90. Browser senden text/html mit, k6 sendet gar keinen Accept-Header, beide bekommen die Seite aus dem Cache.

Mit Cache Zeitüberschreitungen, obwohl die CPU noch Luft hat

Alle Apache-Prozesse sind belegt. MaxRequestWorkers vorsichtig erhöhen, hier brachte der Schritt von 8 auf 20 Prozesse weniger Fehler bei 100 Aufrufen pro Sekunde.

Häufige Fragen

Warum hat der OOM-Killer die Datenbank beendet und nicht Apache?

Der Kernel bewertet jeden Prozess einzeln, im Kern nach seinem belegten Arbeitsspeicher, und beendet den mit der höchsten Bewertung. MariaDB war ein einzelner Prozess mit rund 100 MiB, Apache verteilte sich auf viele kleine. Ohne Datenbank zeigt WordPress nur noch eine Fehlerseite, bis Docker sie neu gestartet hat.

Wie viele Apache-Prozesse passen in 1 GB?

Ohne Cache war der WordPress-Container mit 66 Apache-Prozessen bei 552 MiB, also grob 7 bis 8 MiB je Prozess, dazu der Speicher, den PHP während der Anfrage braucht. Da die CPU schon bei 5 Aufrufen pro Sekunde voll war, bringen mehr als eine Handvoll Prozesse ohne Cache nichts. Mit Cache lohnen sich mehr, weil jede Anfrage kurz ist.

Hilft Swap?

Swap kann den harten Absturz abfedern, macht den Server bei Überlast aber noch langsamer. Ob Swap oder zram bei 1 GB etwas bringen, misst ein eigener Teil der Reihe.

Fazit

WordPress läuft auf einem Server mit 1 GB RAM und einer vCPU, aber nur mit Vorsicht. Ohne Cache ist bei 4 Aufrufen pro Sekunde Schluss. Wichtiger als die Grenze ist, was danach passiert: In der Standardeinstellung des Docker-Images frisst Apache den Speicher auf, der Server ist minutenlang nicht erreichbar, und am Ende fällt die Datenbank. Mit einer festen Zahl Apache-Prozesse und Speichergrenzen für die Container wird Überlast zu einer langsamen Website statt zu einem toten Server. Ein Seiten-Cache hebt die Grenze dann auf 50 Aufrufe pro Sekunde, das reicht für die meisten kleinen Websites mit Reserve. Teil 4 der Reihe misst, wie viele kleine Dienste nebeneinander auf den Server passen.

Weiterführende Anleitungen und Quellen

Mini-ServerWordPressLasttestOOM-KillerApachenetcup