HomeStart / GuidesAnleitungen / Self-hosting Ghost on a VPS/ Ghost auf einem VPS selbst hosten

Self-hosting Ghost on a VPS: MySQL, the url setting, and why members can't sign inGhost auf einem VPS selbst hosten: MySQL, die url-Einstellung und warum sich Mitglieder nicht anmelden können

Ghost's own installer wants a bare Ubuntu box and a CLI that argues with your Node version. The Docker route skips all of that, but it trades one kind of trouble for another: a url setting that has to be exactly right, and a database that is not the one the quickstart docs default to.Ghosts eigener Installer will eine nackte Ubuntu-Box und eine CLI, die mit deiner Node-Version streitet. Der Docker-Weg umgeht das alles, tauscht aber eine Art von Ärger gegen eine andere: eine url-Einstellung, die exakt stimmen muss, und eine Datenbank, die nicht die ist, auf die die Schnellstart-Doku standardmäßig setzt.

SQLite is a demo, not a planSQLite ist eine Demo, kein Plan

Ghost's own local-install docs will happily start you on SQLite, and for kicking the tyres that's fine. Ghost's official Docker image documents SQLite as development-only and expects MySQL for a production install — not MariaDB, not Postgres. If you're coming from Nextcloud or Gitea guides where "any SQL-ish database works," drop that assumption here: the Ghost image is built and tested against MySQL 8, and that's the one to run behind it.Ghosts eigene Anleitung für die lokale Installation lässt dich bereitwillig mit SQLite starten, und zum Reinschnuppern ist das in Ordnung. Ghosts offizielles Docker-Image dokumentiert SQLite ausdrücklich nur für die Entwicklung und erwartet für eine Produktivinstallation MySQL — nicht MariaDB, nicht Postgres. Kommst du von Nextcloud- oder Gitea-Anleitungen, wo „jede SQL-artige Datenbank funktioniert“, lass diese Annahme hier fallen: Das Ghost-Image wird gegen MySQL 8 gebaut und getestet, und genau das gehört dahinter.

The practical effect: your compose file has two services from the start, not one you add later once you've outgrown a file. There's no gentle on-ramp — decide the shape on day one.Die praktische Folge: Deine Compose-Datei hat von Anfang an zwei Services, nicht einen, den du später ergänzt, sobald eine Datei nicht mehr reicht. Es gibt keinen sanften Einstieg — leg die Form am ersten Tag fest.

The compose fileDie Compose-Datei

Pin both image tags. ghost:5 tracks the current Ghost 5.x line; check hub.docker.com/_/ghost for the exact minor you want before you go further than a first test, and re-check it every time you plan to update.Pinne beide Image-Tags. ghost:5 folgt der aktuellen Ghost-5.x-Linie; sieh auf hub.docker.com/_/ghost nach, welche genaue Minor-Version du willst, bevor du über einen ersten Test hinausgehst, und prüfe es jedes Mal erneut, wenn du ein Update planst.

services:
  db:
    image: mysql:8  # check hub.docker.com/_/mysql for the current 8.x point release
    restart: unless-stopped
    environment:
      - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
      - MYSQL_DATABASE=ghost
      - MYSQL_USER=ghost
      - MYSQL_PASSWORD=${DB_PASSWORD}
    volumes:
      - db:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      retries: 10

  ghost:
    image: ghost:5  # check hub.docker.com/_/ghost for the current 5.x minor
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      - database__client=mysql
      - database__connection__host=db
      - database__connection__user=ghost
      - database__connection__password=${DB_PASSWORD}
      - database__connection__database=ghost
      - url=https://blog.example.com
    volumes:
      - content:/var/lib/ghost/content
    ports:
      - "127.0.0.1:2368:2368"

volumes:
  db:
  content:

Put the passwords in a .env file next to the compose file, not inline in the YAML:Leg die Passwörter in eine .env-Datei neben die Compose-Datei, nicht direkt im YAML:

printf 'DB_ROOT_PASSWORD=%s\nDB_PASSWORD=%s\n' "$(openssl rand -hex 24)" "$(openssl rand -hex 24)" > .env
chmod 600 .env

Then bring it up:Dann starte es:

docker compose up -d

The healthcheck above is what makes depends_on actually mean something: on a clean volume, MySQL takes a few seconds to finish initializing before it accepts connections, and without a healthcheck-gated dependency Ghost's first connection attempt can land before the database is ready — plain depends_on: [db] only waits for the container to start, not for MySQL itself. Ghost's first-run migrations run automatically against the ghost database in MySQL the first time the ghost container starts with a clean volume, so give it a minute before you load the URL.Der Healthcheck oben ist das, was depends_on überhaupt erst wirksam macht: Auf einem frischen Volume braucht MySQL ein paar Sekunden, um die Initialisierung abzuschließen, bevor es Verbindungen annimmt, und ohne eine per Healthcheck abgesicherte Abhängigkeit kann Ghosts erster Verbindungsversuch landen, bevor die Datenbank bereit ist — reines depends_on: [db] wartet nur darauf, dass der Container startet, nicht darauf, dass MySQL selbst bereit ist. Ghosts Migrationen beim ersten Start laufen automatisch gegen die Datenbank ghost in MySQL, sobald der ghost-Container zum ersten Mal mit einem frischen Volume startet — gib ihm also eine Minute, bevor du die URL aufrufst.

The url setting is not cosmeticDie Einstellung url ist kein Kosmetikdetail

This is the one Ghost mistake that looks like ten different bugs. Ghost's url environment variable is not a display label — it's baked into how Ghost generates every link it writes: post URLs, the sitemap, RSS, image paths, and the redirect target after admin login. Set it to http:// or to the wrong hostname, and the symptoms are a grab-bag: assets that don't load, a login screen that redirects you somewhere broken, a sitemap full of the wrong domain.Das ist der eine Ghost-Fehler, der aussieht wie zehn verschiedene Bugs. Ghosts Umgebungsvariable url ist kein Anzeige-Label — sie ist fest eingebacken in die Art, wie Ghost jeden Link erzeugt, den es schreibt: Beitrags-URLs, die Sitemap, RSS, Bildpfade und das Umleitungsziel nach dem Admin-Login. Setzt du sie auf http:// oder auf den falschen Hostnamen, sind die Symptome bunt gemischt: Assets, die nicht laden, ein Login-Bildschirm, der dich irgendwohin Kaputtes umleitet, eine Sitemap voller falscher Domain.

Set it once, correctly, before you ever log in as admin: the full https:// URL, matching exactly what's in DNS and what Caddy will serve, with no trailing slash. Changing it later is possible but touches published content, so getting it right on the first boot saves a cleanup pass.Setze sie einmal, korrekt, bevor du dich überhaupt als Admin einloggst: die vollständige https://-URL, die exakt dem entspricht, was im DNS steht und was Caddy ausliefert, ohne abschließenden Schrägstrich. Sie später zu ändern ist möglich, betrifft aber bereits veröffentlichte Inhalte — sie beim ersten Start richtig zu setzen erspart dir einen Aufräumdurchgang.

HTTPS with CaddyHTTPS mit Caddy

blog.example.com {
    reverse_proxy 127.0.0.1:2368
}

Caddy requests and renews the certificate the first time it starts or reloads with this config, provided the A record already resolves to the machine. Point DNS first, let it settle, then start Caddy — point your domain at your service if you haven't done that step before.Caddy fordert das Zertifikat beim ersten Start oder Neuladen mit dieser Konfiguration an und erneuert es danach, vorausgesetzt der A-Eintrag zeigt bereits auf die Maschine. Richte zuerst den DNS-Eintrag ein, lass ihn sich setzen, und starte dann Caddy — deine Domain auf deinen Dienst zeigen lassen, falls du diesen Schritt noch nicht gemacht hast.

Read this before you buy: the port 443 catchLies das, bevor du kaufst: die Sache mit Port 443

Ghost itself doesn't care about NAT; Caddy terminating TLS on port 443 does, and that port is the one every provider treats differently. Under NAT IPv4 you don't automatically own port 443 on the shared address — check what your plan actually forwards before you point DNS at the box and wonder why the certificate request times out.Ghost selbst ist NAT egal; Caddy, das TLS auf Port 443 terminiert, ist es nicht egal, und genau diesen Port behandelt jeder Anbieter anders. Unter NAT-IPv4 gehört dir Port 443 auf der geteilten Adresse nicht automatisch — prüfe, was dein Plan tatsächlich weiterleitet, bevor du DNS auf die Box zeigen lässt und dich fragst, warum die Zertifikatsanfrage in ein Timeout läuft.

Three ways out, roughly in order of least to most hassle:Drei Auswege, ungefähr in der Reihenfolge von wenig zu viel Aufwand:

Read NAT IPv4 vs a dedicated IP and NAT IPv4, ports and forwarding before you commit to a shape; this is the single most common "why doesn't my certificate work" support question for anything that needs 443.Lies NAT IPv4 vs. dedizierte IP und NAT IPv4, Ports und Weiterleitung, bevor du dich auf eine Variante festlegst; das ist die mit Abstand häufigste Support-Frage der Art „warum funktioniert mein Zertifikat nicht“ für alles, was Port 443 braucht.

Mail: the setting that decides whether members can sign inMail: die Einstellung, die entscheidet, ob sich Mitglieder anmelden können

Ghost's member features — signup, magic-link login, password reset — all go out as e-mail, and Ghost does not run its own mail server. Skip mail configuration and the member system is not degraded, it's non-functional: nobody can complete a signup or a password reset, because the e-mail Ghost tries to send simply never arrives.Ghosts Mitgliedsfunktionen — Registrierung, Magic-Link-Login, Passwort-Reset — laufen alle über E-Mail, und Ghost betreibt keinen eigenen Mailserver. Überspringst du die Mail-Konfiguration, ist das Mitgliedersystem nicht eingeschränkt, sondern funktionsunfähig: Niemand kann eine Registrierung oder einen Passwort-Reset abschließen, weil die E-Mail, die Ghost zu senden versucht, schlicht nie ankommt.

Configure an SMTP provider through the mail__ environment variables:Konfiguriere einen SMTP-Anbieter über die Umgebungsvariablen mail__:

      - mail__transport=SMTP
      - mail__from=Blog <[email protected]>
      - mail__options__host=smtp.your-provider.example
      - mail__options__port=587
      - mail__options__auth__user=${SMTP_USER}
      - mail__options__auth__pass=${SMTP_PASSWORD}

Add SMTP_USER and SMTP_PASSWORD to the same .env file as the database passwords. Any transactional SMTP provider works here — Ghost talks plain SMTP, it has no built-in provider list to pick from. This mail__* config covers signup, login and password-reset e-mail only — sending an actual newsletter issue to your member list is a separate feature that Ghost restricts to Mailgun (bulkEmail__mailgun__*); generic SMTP doesn't enable that part. If you only publish and never turn on member signups, you can skip this for members — but Ghost's own admin password reset (the "Forgot password?" link on the /ghost login screen) goes out through this same mail__* transport, so leaving it unset entirely means you have no recovery path if you ever forget your admin password.Füge SMTP_USER und SMTP_PASSWORD derselben .env-Datei hinzu wie die Datenbank-Passwörter. Jeder transaktionale SMTP-Anbieter funktioniert hier — Ghost spricht schlichtes SMTP, es hat keine eingebaute Anbieterliste zum Auswählen. Diese mail__*-Konfiguration deckt nur Registrierungs-, Login- und Passwort-Reset-E-Mails ab — eine tatsächliche Newsletter-Ausgabe an deine Mitgliederliste zu versenden ist eine separate Funktion, die Ghost auf Mailgun beschränkt (bulkEmail__mailgun__*); generisches SMTP schaltet diesen Teil nicht frei. Wenn du nur veröffentlichst und nie Mitgliederregistrierungen aktivierst, kannst du das für Mitglieder überspringen — aber Ghosts eigener Admin-Passwort-Reset (der Link „Forgot password?“ auf dem Login-Bildschirm unter /ghost) läuft über denselben mail__*-Transport, lässt du ihn also komplett unkonfiguriert, hast du keinen Wiederherstellungsweg mehr, falls du dein Admin-Passwort jemals vergisst.

Sizing: 1 GiB is tight, 2 GiB is comfortableDimensionierung: 1 GiB ist knapp, 2 GiB ist komfortabel

Ghost's own process is lighter than Nextcloud or a CMS with a plugin ecosystem — it's a single Node app rendering from a fixed theme. The number that changes the picture is MySQL sitting next to it: MySQL's own buffer pool and connection overhead add up fast, and 1 GiB total is tight the moment both containers are running plus whatever the OS itself needs. 2 GiB is comfortable for a single-author or small-team blog, including Caddy and normal traffic, with room for MySQL not to be memory-starved under a burst of concurrent readers.Ghosts eigener Prozess ist leichter als Nextcloud oder ein CMS mit Plugin-Ökosystem — es ist eine einzelne Node-App, die aus einem festen Theme rendert. Die Zahl, die das Bild verändert, ist MySQL daneben: MySQLs eigener Buffer-Pool und Verbindungs-Overhead summieren sich schnell, und 1 GiB insgesamt ist knapp, sobald beide Container laufen plus das, was das Betriebssystem selbst braucht. 2 GiB sind komfortabel für einen Blog mit einem Autor oder einem kleinen Team, inklusive Caddy und normalem Traffic, mit Spielraum, damit MySQL bei einem Schub gleichzeitiger Leser nicht am Arbeitsspeicher hungert.

Disk is mostly images: the content volume holds themes, uploads and any exported member data, and grows with how many images you paste into posts rather than with word count. A database backup adds relatively little unless you have a very large member list.Speicherplatz besteht größtenteils aus Bildern: Das content-Volume enthält Themes, Uploads und alle exportierten Mitgliederdaten und wächst mit der Zahl der Bilder, die du in Beiträge einfügst, nicht mit der Wortzahl. Ein Datenbank-Backup trägt relativ wenig bei, außer du hast eine sehr große Mitgliederliste.

BackupsBackups

Two things, and both matter:Zwei Dinge, und beide zählen:

DUMP="ghost-db-$(date +%F).sql"
docker compose exec db sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" ghost' > "$DUMP"
chmod 600 "$DUMP"

${DB_ROOT_PASSWORD} only exists inside your .env file — Compose reads that file to fill in the YAML at parse time, it never exports the value into your shell. Running the dump from inside the db container instead reads the password from MYSQL_ROOT_PASSWORD, the variable that container actually has set. If a host-side script needs the value directly, load it first with set -a; source .env; set +a.${DB_ROOT_PASSWORD} existiert nur innerhalb deiner .env-Datei — Compose liest diese Datei, um das YAML beim Parsen auszufüllen, exportiert den Wert aber nie in deine Shell. Führst du den Dump stattdessen im db-Container aus, liest er das Passwort aus MYSQL_ROOT_PASSWORD, der Variable, die dieser Container tatsächlich gesetzt hat. Braucht ein Skript auf dem Host den Wert direkt, lade ihn zuerst mit set -a; source .env; set +a.

Copy both off the machine — a dump sitting on the same disk as the instance it backs up is not a backup, it's a second copy of the same risk. back up your VPS is our own write-up of what off-machine actually means.Kopiere beides von der Maschine herunter — ein Dump, der auf derselben Platte liegt wie die Instanz, die er sichert, ist kein Backup, sondern eine zweite Kopie desselben Risikos. Backup deines VPS ist unser eigener Leitfaden dazu, was „von der Maschine herunter“ in der Praxis bedeutet.

UpdatesUpdates

docker compose pull
docker compose up -d

Bump the pinned tag deliberately rather than tracking latest, and read Ghost's release notes for the version you're jumping to before you do — Ghost runs its own database migrations on the first boot after an upgrade, and a migration that goes wrong is much less stressful with a same-day database dump already sitting off the machine, not one you're improvising afterward.Erhöhe das gepinnte Tag bewusst, statt latest zu verfolgen, und lies Ghosts Release Notes für die Version, zu der du wechselst, bevor du es tust — Ghost führt seine eigenen Datenbank-Migrationen beim ersten Start nach einem Upgrade aus, und eine schiefgehende Migration ist deutlich entspannter, wenn bereits ein taggleicher Datenbank-Dump außerhalb der Maschine liegt, statt dass du ihn hinterher improvisierst.

On overnight.hostBei overnight.host

Full disclosure: this is what we sell. If you want the blog without the sysadmin, the managed Ghost container comes with its own hostname and certificate — you get the app and a URL, not a root shell.Zur vollen Transparenz: Das ist, was wir verkaufen. Willst du den Blog ohne die Systemadministration, kommt der gemanagte Ghost-Container mit eigenem Hostnamen und Zertifikat — du bekommst die App und eine URL, keine Root-Shell.

One-click apps — EUR 4 to EUR 12 a month, hosted in Germany (EU). Eight apps: n8n, Uptime Kuma, Vaultwarden, Gitea, Nextcloud, Ghost, Managed WordPress, Private AI Chat. Each customer gets an isolated Docker network and volume, plus a hostname under apps.overnight.host on a real wildcard certificate. Memory and CPU are capped per plan by the container runtime.One-Click-Apps — 4 bis 12 EUR im Monat, gehostet in Deutschland (EU). Acht Apps: n8n, Uptime Kuma, Vaultwarden, Gitea, Nextcloud, Ghost, Managed WordPress, Private AI Chat. Jeder Kunde bekommt ein isoliertes Docker-Netzwerk und -Volume sowie einen Hostnamen unter apps.overnight.host mit einem echten Wildcard-Zertifikat. Arbeitsspeicher und CPU sind je Plan durch die Container-Runtime gedeckelt.

You order in the shop, pay by card (Stripe) or SEPA bank transfer, and your login details are e-mailed to you once the service is set up. Support is e-mail, run by one person, with no guaranteed response time. All prices are final totals under the German small-business rule (§19 UStG); no VAT is added or shown.Du bestellst im Shop, zahlst per Karte (Stripe) oder SEPA-Überweisung, und deine Zugangsdaten werden dir per E-Mail zugeschickt, sobald der Dienst eingerichtet ist. Support läuft per E-Mail, von einer einzelnen Person betrieben, ohne garantierte Reaktionszeit. Alle Preise sind Endpreise. Gemäß § 19 UStG wird keine Umsatzsteuer ausgewiesen.

Order one-click-ghost → · One-click apps overviewOne-Click-Ghost bestellen → · Übersicht One-Click-Apps

Written by the person who runs overnight.host: a small, honest hosting company on dedicated bare metal — Linux VPS, game servers, web hosting. Live status at up.overnight.host.Geschrieben von der Person, die overnight.host betreibt: ein kleines, ehrliches Hosting-Unternehmen auf dedizierter Bare-Metal-Hardware — Linux-VPS, Gameserver, Webhosting. Live-Status unter up.overnight.host.

Technical guidance is informational. Plans, specifications and final prices are listed in the shop and can be ordered directly; VPS, game server, web hosting, one-click app and automation plans are provisioned automatically after payment. Custom configurations are still arranged by e-mail.Technische Hinweise dienen der Information. Pläne, Spezifikationen und Endpreise stehen im Shop und können direkt bestellt werden; VPS-, Gameserver-, Webhosting-, One-Click-App- und Automatisierungs-Pläne werden nach der Zahlung automatisch bereitgestellt. Sonderkonfigurationen werden weiterhin per E-Mail vereinbart.

FAQFAQ

Can I run Ghost on SQLite in production?Kann ich Ghost mit SQLite im Produktivbetrieb laufen lassen?

Ghost's own image documents SQLite as development-only. Point it at MySQL 8 from the start for anything you intend to keep running — there is no in-place conversion from a SQLite file to MySQL later; the supported route is Ghost's JSON content export and import into a fresh MySQL install, so deciding upfront avoids that rebuild.Ghosts eigenes Image dokumentiert SQLite ausdrücklich nur für die Entwicklung. Richte es von Anfang an auf MySQL 8 aus für alles, was du dauerhaft betreiben willst — es gibt später keine direkte Umwandlung einer SQLite-Datei zu MySQL; der unterstützte Weg ist Ghosts JSON-Content-Export und -Import in eine frische MySQL-Installation, weshalb eine frühe Entscheidung dir diesen Neuaufbau erspart.

Why do my images and links point at the wrong domain?Warum zeigen meine Bilder und Links auf die falsche Domain?

The url environment variable is wrong, or it was set after Ghost had already written content with the old value. Fix it to the exact https:// address Caddy serves, restart, and check a fresh post rather than an old one — content written before the fix keeps its old URLs baked in.Die Umgebungsvariable url ist falsch gesetzt, oder sie wurde erst gesetzt, nachdem Ghost bereits Inhalte mit dem alten Wert geschrieben hatte. Korrigiere sie auf die exakte https://-Adresse, die Caddy ausliefert, starte neu, und prüfe einen frischen Beitrag statt eines alten — vor der Korrektur geschriebene Inhalte behalten ihre alten URLs fest eingebrannt.

Why can't anyone sign up or reset their password?Warum kann sich niemand registrieren oder sein Passwort zurücksetzen?

No SMTP is configured. Ghost's member signup and password-reset flow depends entirely on outgoing e-mail through the mail__ settings; without it those e-mails are never sent; add an SMTP provider through the mail__transport/mail__options__* variables and test a real signup afterward.Es ist kein SMTP konfiguriert. Ghosts Ablauf für Mitgliederregistrierung und Passwort-Reset hängt vollständig von ausgehender E-Mail über die mail__-Einstellungen ab; ohne sie werden diese E-Mails nie versendet; füge einen SMTP-Anbieter über die Variablen mail__transport/mail__options__* hinzu und teste danach eine echte Registrierung.

Do I need a dedicated IPv4 for Ghost?Brauche ich eine dedizierte IPv4 für Ghost?

No. A forwarded port 443 (and briefly 80, for the certificate request) is enough for most setups, and a non-standard HTTPS port in the url works too if that's not available. A dedicated IPv4 removes the whole question but is arranged by e-mail, not something you self-serve at checkout.Nein. Ein weitergeleiteter Port 443 (und kurz Port 80, für die Zertifikatsanfrage) reicht für die meisten Setups, und ein nicht standardmäßiger HTTPS-Port in der url funktioniert ebenfalls, falls das nicht verfügbar ist. Eine dedizierte IPv4 beseitigt die ganze Frage, wird aber per E-Mail organisiert und nicht im Self-Service beim Checkout.

Is a managed Ghost different from self-hosting?Unterscheidet sich ein gemanagtes Ghost vom Self-Hosting?

Functionally it runs the same application. The difference is who holds the root shell and the database: a managed container gives you the app, a hostname and a certificate already wired up, and you don't get to touch the compose file, the MySQL instance or the SMTP config yourself. If you want that control, put it on a VPS instead.Funktional läuft dieselbe Anwendung. Der Unterschied ist, wer die Root-Shell und die Datenbank hat: Ein gemanagter Container gibt dir die App, einen Hostnamen und ein bereits fertig verdrahtetes Zertifikat, aber du kommst nicht an die Compose-Datei, die MySQL-Instanz oder die SMTP-Konfiguration selbst heran. Willst du diese Kontrolle, gehört es stattdessen auf einen VPS.

Ready to order?Bereit zu bestellen?

Prices are final totals; no VAT is shown (§19 UStG). Need something the shop does not list? Email us for a written offer.Alle Preise sind Endpreise ohne ausgewiesene USt. (§19 UStG). Du brauchst etwas, das nicht im Shop steht? Schreib uns für ein schriftliches Angebot.

Order now →Jetzt bestellen → Request a custom configIndividuelle Konfiguration anfragen