Blog Ghost rulat cu Docker Compose, MySQL si HTTPS pe un VPS
Self-hosting

Ghost cu Docker Compose: blog si newsletter pe VPS-ul tau

Pe Medium, audienta e a platformei, platformele de newsletter iau un procent din fiecare abonament platit, iar un WordPress cu o duzina de pluginuri devine incet un hobby de mentenanta. Ghost sta foarte confortabil la mijloc: o platforma de publicare rapida si moderna, cu un editor curat, membri si newslettere incluse, si fara o gradina zoologica de pluginuri de supravegheat. Daca il rulezi pe propriul VPS, articolele, lista de membri si adresele de email ale cititorilor raman la tine, pe un server pe care il controlezi. In ghidul asta instalam Ghost cu Docker Compose, MySQL si HTTPS automat, configuram emailul pentru login-ul membrilor si pentru newslettere, si incheiem cu o rutina de backup si o metoda sigura de upgrade.

Ghost este open source, sub licenta MIT, dezvoltat de Ghost Foundation, o organizatie non-profit. Site-ul oficial este ghost.org , iar codul sta pe github.com/TryGhost/Ghost . Vom folosi imaginea oficiala Docker a lui Ghost, MySQL 8.4 ca baza de date si Caddy ca poarta de intrare HTTPS. Totul incape intr-un singur dosar si un singur fisier Compose, deci e usor de inteles, de salvat si de mutat mai tarziu pe alt server.

Ce este Ghost si de ce il rulam cu Docker Compose

Ghost este o aplicatie Node.js pentru publicare. Scrii articole intr-un editor pe blocuri, alegi sau incarci o tema, iar cititorii se pot abona ca membri gratuiti sau platitori (abonamentele platite trec prin Stripe). Orice articol poate fi trimis si ca newsletter pe email catre membri. In productie, Ghost are nevoie de o baza de date MySQL 8 pentru articole, membri si setari, plus un dosar numit content in care pastreaza imaginile incarcate, temele si cateva fisiere interne.

Cateva concepte inainte sa incepem, pentru ca le vom folosi tot timpul:

  • Container: un proces izolat, cu propriul sistem de fisiere, pornit dintr-o imagine. Imaginea e o copie “inghetata”, gata de rulat, a unei aplicatii cu toate dependentele ei, iar containerul e o instanta pornita a acelei imagini.
  • Docker Compose: un instrument care citeste un fisier YAML ce descrie mai multe containere (servicii), setarile lor si cum comunica intre ele, si le porneste sau le opreste impreuna cu o singura comanda.
  • Volum / bind mount: un dosar de pe discul serverului mapat in container, ca datele sa supravietuiasca atunci cand containerul e sters si recreat. Asta face posibile upgrade-urile.
  • Port: o “usa” numerotata a serverului. Traficul web foloseste 80 (HTTP) si 443 (HTTPS), SSH foloseste 22, Ghost asculta intern pe 2368, iar MySQL pe 3306.
  • Reverse proxy: un server web care primeste cererile vizitatorilor si le trimite mai departe catre aplicatia potrivita din spatele lui. Al nostru se ocupa si de TLS, criptarea din spatele lacatului din browser.

Cele trei containere din stack

  • caddy: singurul container accesibil din internet. Obtine si reinnoieste automat un certificat gratuit Let’s Encrypt si trimite cererile catre Ghost.
  • ghost: blogul propriu-zis, accesibil doar in reteaua privata Docker.
  • db: MySQL 8.4, tot privat. Nimeni din afara serverului nu poate vorbi cu el.
Ghost cu Docker Compose: drumul unei cereriRetea Docker (doar interna)Vizitatorisi membriporturi 80, 443Caddyproxy HTTPSGhostport 2368MySQL 8.4port 3306email trimisSMTPlogin, invitatiiAPI MailgunnewslettereDisc server(persistent)caddy_data./content./mysqlTuadmin SSHufwpermite SSHsshd :22serviciu hostPorturile publicate de Dockerocolesc ufw, de aceea doarCaddy publica porturi.
Doar Caddy publica porturile 80 si 443. Ghost si MySQL raman in reteaua interna Docker, datele lor stau in dosare pe discul serverului, iar ufw protejeaza serviciile host-ului, cum e SSH.

Detaliul important din imagine: Docker isi scrie propriile reguli de firewall, asa ca un port publicat de un container e accesibil din internet chiar daca ufw spune altceva. De aceea protectia noastra reala e simpla: Ghost si MySQL nu publica niciun port. Doar Caddy o face, iar Caddy vorbeste doar HTTP si HTTPS.

De ce ai nevoie

  • Un VPS KVM DreamServer cu Debian 13 sau Ubuntu 24.04. Pentru un blog, 1 vCPU, 2 GB RAM si 20 GB de disc sunt confortabile. Merge si cu 1 GB, cu swap si o mica ajustare la MySQL (vezi sectiunea despre RAM).
  • Un domeniu sau subdomeniu pe care il controlezi, de exemplu blog.example.ro, cu acces la inregistrarile DNS.
  • Un utilizator cu sudo si acces SSH la server.
  • Un cont SMTP pentru emailurile tranzactionale: un furnizor de email tranzactional sau o casuta de la hostingul tau de email care permite trimitere SMTP pe portul 587 sau 465.
  • Optional, un cont Mailgun, daca vrei sa trimiti newslettere catre membri (explicam de ce la Pasul 6).
  • Cam 30 pana la 45 de minute.

KVM sau LXC?

Docker merge cel mai bine pe un VPS KVM, unde ai propriul kernel. Intr-un container LXC neprivilegiat, Docker porneste doar daca pe container sunt activate nesting (si de obicei keyctl), iar unele lucruri, cum ar fi crearea de swap, sunt controlate de host si nu functioneaza din interior. Daca esti pe un plan LXC si Docker refuza sa porneasca, scrie-ne la suport sau alege un VPS KVM pentru proiectul asta.

Toata procedura dintr-o privire

Ghost pe VPS in 8 pasi1DNS + VPSupdate, ufw2Dockerengine, compose3Dosar + .envparole, SMTP4Composesi Caddyfile5Pornire stackcont de admin6EmailSMTP, Mailgun7Backupdump + content8Upgradebackup, pull, up
Pasii 1-5 construiesc stack-ul, Pasul 6 configureaza emailul, Pasii 7 si 8 il tin in siguranta in timp. Fiecare caseta corespunde unui titlu numerotat de mai jos.

Pasul 1: Indreapta domeniul spre server si pregateste VPS-ul

Caddy poate obtine certificat doar daca domeniul arata deja spre server, asa ca incepem cu DNS-ul. In panoul furnizorului DNS, creeaza o inregistrare A pentru blog.example.ro cu adresa IPv4 publica a VPS-ului (si o inregistrare AAAA cu adresa IPv6, daca VPS-ul are una). Un TTL mic, de exemplu 300 de secunde, inseamna ca modificarile se propaga mai repede.

Verifica de pe server:

getent hosts blog.example.ro
ip -brief address

Prima comanda ar trebui sa afiseze adresa VPS-ului, iar aceasta sa se regaseasca printre adresele afisate de a doua comanda. Daca nu afiseaza nimic, mai asteapta cateva minute si incearca din nou.

Acum aducem sistemul la zi:

sudo apt update && sudo apt full-upgrade -y

E nevoie de restart? Pe Ubuntu, sistemul creeaza un fisier marcaj atunci cand e necesar:

[ -f /var/run/reboot-required ] && echo "E nevoie de restart" || echo "Nu e nevoie de restart"

Pe Debian fisierul acesta nu e creat mereu, asa ca regula e simpla: daca upgrade-ul a instalat un pachet linux-image nou, reporneste cu sudo reboot.

Urmeaza firewall-ul host-ului. ufw vine instalat implicit pe Ubuntu, dar de obicei nu si pe Debian, deci linia de instalare de mai jos nu strica pe Ubuntu si e necesara pe Debian. Permite SSH inainte sa activezi firewall-ul, altfel te blochezi singur afara. Daca SSH-ul asculta pe alt port, permite acel numar in loc de OpenSSH.

sudo apt install -y ufw
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

In output ar trebui sa apara OpenSSH, 80 si 443 cu ALLOW IN. Tine minte ce am spus mai sus: regulile pentru 80/443 conteaza pentru serviciile instalate direct pe host. Porturile publicate de Docker ocolesc complet ufw, de aceea nu publicam niciodata Ghost sau MySQL.

Pasul 2: Instaleaza Docker Engine si pluginul Compose

Pachetele Docker din depozitele implicite Debian si Ubuntu sunt adesea mai vechi, asa ca folosim depozitul oficial Docker, descris in formatul actual deb822. Variabilele $ID si codename vin din /etc/os-release, deci acelasi bloc merge si pe Debian 13, si pe Ubuntu 24.04.

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
. /etc/os-release
sudo curl -fsSL "https://download.docker.com/linux/$ID/gpg" -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/$ID
Suites: ${UBUNTU_CODENAME:-$VERSION_CODENAME}
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Verifica instalarea:

sudo docker run --rm hello-world
docker compose version

Ar trebui sa vezi mesajul “Hello from Docker!” si o versiune Compose care incepe cu v2 sau mai noua. In ghidul asta punem intentionat sudo in fata comenzilor Docker: sa-ti adaugi utilizatorul in grupul docker e comod, dar grupul acela inseamna practic root pe masina.

Optional, dar recomandat: implicit, Docker pastreaza logurile containerelor la nesfarsit. Ca sa le rotesti, pune asta in /etc/docker/daemon.json. Daca fisierul exista deja, adauga aceste chei in el, nu il suprascrie.

{
  "log-driver": "local",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

Apoi ruleaza sudo systemctl restart docker. Setarea se aplica containerelor create dupa restart.

Pasul 3: Creeaza dosarul proiectului si fisierul .env

Tinem totul in /opt/ghost: fisierul Compose, configuratia Caddy, secretele, dosarele cu date si backup-urile.

sudo mkdir -p /opt/ghost
sudo chown "$USER": /opt/ghost
mkdir -p /opt/ghost/backups
cd /opt/ghost

Genereaza doua parole puternice, una pentru utilizatorul root din MySQL si una pentru utilizatorul bazei de date Ghost. Formatul hex evita caracterele speciale care ar putea incurca YAML-ul sau shell-ul.

openssl rand -hex 24
openssl rand -hex 24

Acum creeaza fisierul .env. Docker Compose il citeste automat din dosarul proiectului si inlocuieste referintele ${VARIABILA} din fisierul Compose.

nano .env
# Adresa publica a blogului, fara https://
DOMAIN=blog.example.ro

# Secretele bazei de date (lipeste cele doua valori generate)
MYSQL_ROOT_PASSWORD=lipeste-prima-valoare
MYSQL_PASSWORD=lipeste-a-doua-valoare

# SMTP pentru emailuri tranzactionale (login membri, invitatii staff)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=postmaster@example.ro
SMTP_PASS=parola-smtp
MAIL_FROM='Blogul meu <noreply@example.ro>'

Restrange accesul la fisier, pentru ca acolo stau toate secretele stack-ului:

chmod 600 .env

Doua lucruri de inteles aici. Primul: valorile MYSQL_* sunt folosite de imaginea MySQL doar la prima pornire, cand dosarul de date e gol. Daca le schimbi ulterior in .env, parolele reale nu se schimba (vezi sectiunea de depanare). Al doilea: completeaza de acum datele SMTP, daca le ai. Versiunile recente de Ghost confirma login-urile staff de pe dispozitive noi cu un cod trimis pe email, deci un email functional nu e un moft, e felul in care intri inapoi in propriul panou de admin. La Pasul 6 explicam combinatiile de port si SMTP_SECURE.

Pasul 4: Scrie docker-compose.yml si Caddyfile

Creeaza fisierul Compose:

nano docker-compose.yml
services:
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - ghost

  ghost:
    image: ghost:6-alpine
    restart: unless-stopped
    environment:
      url: "https://${DOMAIN}"
      database__client: mysql
      database__connection__host: db
      database__connection__user: ghost
      database__connection__password: ${MYSQL_PASSWORD}
      database__connection__database: ghost
      mail__transport: SMTP
      mail__from: ${MAIL_FROM}
      mail__options__host: ${SMTP_HOST}
      mail__options__port: ${SMTP_PORT}
      mail__options__secure: ${SMTP_SECURE}
      mail__options__auth__user: ${SMTP_USER}
      mail__options__auth__pass: ${SMTP_PASS}
    volumes:
      - ./content:/var/lib/ghost/content
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mysql:8.4
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ghost
      MYSQL_USER: ghost
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - ./mysql:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s

volumes:
  caddy_data:
  caddy_config:

Ce face fiecare parte:

  • Fara cheia version:: Docker Compose v2 actual nu mai are nevoie de ea.
  • caddy publica 80 si 443 (plus 443/udp pentru HTTP/3). Volumul caddy_data pastreaza certificatele, ca sa supravietuiasca restarturilor si sa nu atingi limitele Let’s Encrypt.
  • ghost nu are sectiune ports:, deci e accesibil doar din celelalte containere. Ghost isi citeste configuratia din variabile de mediu in care dublul underscore inseamna un nivel, deci database__connection__host devine database.connection.host. Valoarea url trebuie sa fie exact adresa publica HTTPS, altfel apar linkuri gresite sau bucle de redirect.
  • db creeaza baza de date ghost si utilizatorul la prima pornire. Healthcheck-ul face ping la server prin TCP, ceea ce reuseste doar cand MySQL e complet gata, iar condition: service_healthy il face pe Ghost sa astepte, in loc sa cada la primul boot.
  • Tag-urile imaginilor: ghost:6-alpine urmareste versiunea majora actuala Ghost 6, mysql:8.4 e linia MySQL cu suport pe termen lung pe care Ghost o suporta, iar caddy:2-alpine urmareste Caddy 2. Fixand versiunea majora, docker compose pull aduce corecturi de buguri si de securitate, dar nu te surprinde niciodata cu un upgrade major care strica ceva. Verifica release-urile Ghost si documentatia Ghost pentru cea mai noua versiune majora si pentru versiunile MySQL suportate inainte sa incepi. Evita :latest pentru Ghost si MySQL: un salt tacut la o versiune majora noua poate rula migrari ireversibile ale bazei de date.

Acum Caddyfile-ul. Inlocuieste domeniul si emailul (Let’s Encrypt il foloseste pentru avertismente de expirare):

nano Caddyfile
{
	email admin@example.ro
}

blog.example.ro {
	encode zstd gzip
	reverse_proxy ghost:2368
}

Asta e toata configuratia HTTPS. Caddy vede domeniul, cere un certificat, redirectioneaza HTTP catre HTTPS si trimite totul catre containerul ghost pe portul 2368, adaugand header-ul X-Forwarded-Proto, ca Ghost sa stie ca vizitatorul e pe HTTPS.

Valideaza fisierul Compose fara sa-ti afisezi secretele:

sudo docker compose config --quiet && echo "Fisierul Compose e OK"

Pasul 5: Porneste stack-ul Ghost si revendica contul de admin

Porneste totul in fundal:

cd /opt/ghost
sudo docker compose up -d
sudo docker compose ps

Prima rulare descarca imaginile, ceea ce dureaza un minut. ps ar trebui sa arate trei servicii; db trece din starting in healthy, apoi porneste Ghost. Urmareste logul lui Ghost in timp ce isi creeaza tabelele:

sudo docker compose logs -f ghost

Dupa 30 pana la 60 de secunde ar trebui sa vezi o linie asemanatoare cu Ghost booted in ...s, alaturi de adresa site-ului. Apasa Ctrl+C ca sa nu mai urmaresti logul (containerele continua sa ruleze). Verifica si Caddy:

sudo docker compose logs caddy | grep -i certificate

Cauti un mesaj care spune ca certificatul a fost obtinut cu succes.

Acum deschide https://blog.example.ro/ghost/ in browser imediat. Prima persoana care ajunge pe pagina asta creeaza contul de proprietar, asa ca nu lasa o instalare noua nerevendicata. Completeaza titlul site-ului, numele, emailul si o parola lunga. Ajungi in Ghost Admin, iar site-ul public e deja live la https://blog.example.ro.

O nota despre Ghost 6: cateva functii noi, cum ar fi reteaua sociala (ActivityPub) si analytics-ul web integrat, depind de servicii suplimentare care nu fac parte din acest stack minimal. Editorul, temele, membrii si newsletterele functioneaza fara ele.

Pasul 6: Configureaza SMTP si newsletterele

Doua tipuri de email in Ghost

Ghost trimite doua tipuri foarte diferite de email si foloseste canale diferite pentru ele:

  • Email tranzactional: linkul magic pe care un membru da click ca sa se autentifice, confirmarile de inscriere, invitatiile pentru staff, resetarile de parola si codurile de verificare a dispozitivelor staff. Acestea trec prin setarile SMTP puse in .env. Fara ele, membrii nu se pot autentifica deloc, pentru ca login-ul membrilor in Ghost e fara parola.
  • Newslettere in masa: cand publici un articol si alegi sa-l trimiti pe email membrilor. Ghost le trimite doar prin API-ul Mailgun, nu prin SMTP. E o alegere de design a Ghost si inseamna, totodata, ca trimiterea newsletterelor nu depinde de porturile SMTP deschise.

Email tranzactional prin SMTP

Portul si SMTP_SECURE trebuie sa se potriveasca:

  • Portul 587 cu SMTP_SECURE=false: conexiunea porneste necriptata si trece pe criptare prin STARTTLS. E varianta cea mai des intalnita.
  • Portul 465 cu SMTP_SECURE=true: TLS de la primul byte.

Nu folosi portul 25 pentru trimitere; multe retele il blocheaza spre exterior, iar furnizorii se asteapta la trimitere autentificata pe 587 sau 465. Daca modifici ulterior .env, aplica schimbarea recreand containerul:

sudo docker compose up -d

Ca test, invita un utilizator staff din Settings > Staff, sau deschide site-ul intr-o fereastra privata si inscrie-te ca membru prin butonul Portal. Emailul ar trebui sa soseasca in cateva secunde. Daca nu, cauta erori de email:

sudo docker compose logs ghost | grep -iE "mail|smtp"

Newslettere prin Mailgun

Fa-ti un cont Mailgun si adauga un domeniu de trimitere, ideal un subdomeniu precum mg.example.ro. Alege regiunea EU daca cititorii tai sunt in Europa, ca datele de trimitere sa ramana pe infrastructura din UE. Mailgun iti arata inregistrarile DNS de adaugat (SPF, DKIM si un CNAME de tracking); adauga-le si asteapta pana cand Mailgun marcheaza domeniul ca verificat.

Apoi, in Ghost Admin, deschide Settings si scrie “Mailgun” in casuta de cautare din setari. Alege aceeasi regiune, introdu domeniul de trimitere si cheia API privata, apoi salveaza. De acum, cand apesi Publish pe un articol, poti alege sa-l publici si sa-l trimiti pe email. Personalizeaza designul si numele expeditorului din setarile de newsletter si trimite-ti un test din preview-ul editorului inainte de prima campanie reala. Verifica preturile si planul gratuit Mailgun actuale; se mai schimba din cand in cand.

SPF, DKIM si DMARC

Livrabilitatea tine mai ales de DNS. Asigura-te ca domeniul din MAIL_FROM e cel autorizat la furnizorul SMTP, adauga inregistrarile SPF si DKIM primite de la furnizor si publica o inregistrare DMARC, incepand cu o politica de monitorizare, de exemplu v=DMARC1; p=none; rua=mailto:dmarc@example.ro. Emailurile de pe un domeniu fara aceste inregistrari ajung usor in spam sau nu ajung deloc.

Pasul 7: Backup pentru continut si baza de date

Un backup Ghost are trei parti: baza de date (articole, membri, setari), dosarul content (imagini, teme) si configuratia (.env, fisierul Compose, Caddyfile). Baza de date trebuie exportata cu mysqldump, nu copiata ca fisiere brute cat timp MySQL ruleaza, altfel copia poate fi inconsistenta.

Un script de backup

nano /opt/ghost/backup.sh
#!/usr/bin/env bash
# /opt/ghost/backup.sh: exporta baza de date Ghost si arhiveaza continutul si configuratia
set -euo pipefail
umask 077
cd /opt/ghost
mkdir -p backups
STAMP=$(date +%F-%H%M)

# Dump SQL consistent; parola trece prin MYSQL_PWD, nu in linia de comanda
docker compose exec -T db sh -c 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysqldump -uroot --single-transaction --routines --no-tablespaces ghost' \
  | gzip > "backups/ghost-db-$STAMP.sql.gz"

# Imagini, teme si alte fisiere pe care Ghost le tine pe disc
tar -czf "backups/ghost-content-$STAMP.tar.gz" content

# Configuratia (contine secrete, pastreaz-o privat)
tar -czf "backups/ghost-config-$STAMP.tar.gz" .env docker-compose.yml Caddyfile

# Pastreaza local backup-urile din ultimele 14 zile
find backups -type f -mtime +14 -delete

echo "Backup terminat: $STAMP"

Fa-l executabil si ruleaza-l o data:

chmod +x /opt/ghost/backup.sh
sudo /opt/ghost/backup.sh
sudo ls -lh /opt/ghost/backups

Ar trebui sa vezi trei fisiere cu acelasi timestamp, detinute de root si citibile doar de root (-rw-------), pentru ca ele contin emailurile membrilor si parole. Ghilimelele simple din jurul comenzii sh -c conteaza: $MYSQL_ROOT_PASSWORD e expandat in interiorul containerului de baza de date, unde variabila exista deja, deci parola nu apare niciodata in istoricul shell-ului sau in lista de procese.

Ruleaza-l zilnic cu cron

sudo crontab -e

Adauga linia asta ca backup-ul sa ruleze in fiecare noapte la 03:30:

30 3 * * * /opt/ghost/backup.sh >> /var/log/ghost-backup.log 2>&1

Copiaza backup-urile in afara serverului

Un backup pe acelasi disc nu supravietuieste pierderii acelui disc. Pentru ca fisierele sunt detinute de root, da-le utilizatorului tau inainte sa le copiezi de pe propriul calculator:

sudo chown "$USER": /opt/ghost/backups/*

Apoi, de pe laptop:

scp tu@203.0.113.10:/opt/ghost/backups/ghost-db-2026-11-10-0330.sql.gz ./

Pentru o varianta automata, trimite dosarul pe un al doilea server sau intr-un object storage cu instrumentul de backup preferat, criptat, pentru ca datele membrilor sunt date personale conform GDPR. Ca siguranta suplimentara, Ghost Admin poate exporta din setari si continutul ca JSON, si membrii ca CSV.

Restaurare intr-o baza de date noua

Nu importa niciodata un dump peste baza de date live, deja migrata. Opreste Ghost, recreeaza o baza de date goala, importa, apoi restaureaza dosarul content. Foloseste o imagine Ghost de aceeasi versiune cu backup-ul sau mai noua, niciodata mai veche.

cd /opt/ghost
sudo docker compose stop ghost
sudo docker compose exec -T db sh -c 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot -e "DROP DATABASE IF EXISTS ghost; CREATE DATABASE ghost CHARACTER SET utf8mb4;"'
gunzip -c backups/ghost-db-2026-11-10-0330.sql.gz | sudo docker compose exec -T db sh -c 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot ghost'
sudo mv content content.before-restore
sudo tar -xzf backups/ghost-content-2026-11-10-0330.tar.gz -C /opt/ghost
sudo docker compose start ghost
sudo docker compose logs -f ghost

Utilizatorul ghost din baza de date isi pastreaza drepturile dupa ce baza e recreata, deci nu sunt necesare permisiuni suplimentare. Pe un server complet nou, “baza de date noua” e si mai simpla: pune inapoi .env, docker-compose.yml si Caddyfile, ruleaza sudo docker compose up -d db ca MySQL sa initializeze o baza ghost goala, importa dump-ul, dezarhiveaza content-ul, apoi ruleaza sudo docker compose up -d.

Pasul 8: Upgrade Ghost in siguranta

Ghost isi actualizeaza automat schema bazei de date cand porneste o versiune mai noua, iar migrarile acestea sunt intr-un singur sens. Asta e tot motivul rutinei de mai jos: intai backup, notezi versiunea, apoi upgrade.

Actualizari obisnuite in aceeasi versiune majora

cd /opt/ghost
sudo ./backup.sh
sudo docker compose exec ghost printenv GHOST_VERSION
sudo docker compose pull
sudo docker compose up -d
sudo docker compose logs -f ghost

Noteaza versiunea afisata inainte de pull. pull descarca cele mai noi imagini care se potrivesc cu tag-urile tale (ultimul Ghost 6.x, ultimul MySQL 8.4.x, ultimul Caddy 2.x), iar up -d recreeaza doar containerele a caror imagine s-a schimbat. Asteapta linia de boot in log, verifica site-ul si Ghost Admin, apoi curata imaginile vechi:

sudo docker compose exec ghost printenv GHOST_VERSION
sudo docker image prune -f

Citeste release notes inainte de actualizare, mai ales daca folosesti o tema custom.

Trecerea la o versiune majora noua

Cand apare o versiune majora Ghost noua, actualizeaza mai intai la ultimul release al versiunii majore curente (rutina de mai sus), fa un backup proaspat, citeste notele oficiale de upgrade pentru schimbari incompatibile si compatibilitatea temelor, si abia apoi schimba tag-ul in docker-compose.yml, de exemplu de la ghost:6-alpine la urmatoarea versiune majora. Fa acelasi tip de lectura atenta inainte sa muti MySQL pe o versiune majora noua; sa ramai pe linia 8.4 LTS e alegerea linistita pana cand Ghost documenteaza suport pentru ceva mai nou.

Daca un upgrade merge prost

Pentru ca migrarile nu pot fi inversate, rollback-ul inseamna restaurare, nu doar schimbarea tag-ului. Seteaza imaginea la versiunea exacta pe care ai notat-o (de exemplu ghost:6.x.y-alpine, cu numerele reale notate), apoi urmeaza procedura de restaurare de la Pasul 7 cu backup-ul facut chiar inainte de upgrade.

Cata memorie RAM foloseste un stack Ghost?

RAM aproximativ per container (MB)repausactiv (editare, trimitere)0200400600800RAM (MB)~25~45~190~300~380~450~595~795CaddyGhostMySQL 8.4TotalContainer (blog mic, valori aproximative)
Consum aproximativ de memorie pentru un blog mic, cu cateva mii de membri. Cifrele reale depind de trafic, tema si setarile MySQL; verifica-le pe ale tale cu sudo docker stats.

Dupa cum se vede in grafic, cel mai mare consumator e MySQL, nu Ghost. Intregul stack sta pe la aproximativ 600 MB in repaus si se poate apropia de 800 MB cand editezi si trimiti un newsletter, de aceea recomandam 2 GB RAM: ramane loc pentru sistemul de operare, cache-ul de pagini si varfurile de trafic. Vezi cifrele tale cu:

sudo docker stats --no-stream

Pe un VPS cu 1 GB ajuta doua lucruri. Adauga swap (pe KVM; nu functioneaza intr-un container LXC):

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Si dezactiveaza performance schema din MySQL, care economiseste in jur de 100 MB, adaugand command: ["--performance-schema=OFF"] la serviciul db, apoi ruland sudo docker compose up -d.

Depanare si greseli frecvente

  • Nu exista certificat, browserul arata eroare de securitate. DNS-ul inca nu arata spre acest server sau portul 80 nu e accesibil (Let’s Encrypt valideaza pe portul 80). Verifica sudo docker compose logs caddy si getent hosts blog.example.ro.
  • “Port is already allocated” la pornire. Ceva de pe host foloseste deja 80 sau 443, des un Apache sau Nginx preinstalat. Gaseste-l cu sudo ss -tlnp | grep -E ':80 |:443 ' si opreste-l sau dezinstaleaza-l.
  • Prea multe redirectionari. Valoarea url din fisierul Compose nu se potriveste cu felul in care ajung vizitatorii, de exemplu http:// in loc de https://. Daca folosesti Cloudflare in fata, seteaza modul SSL pe “Full (strict)”, niciodata “Flexible”.
  • Ghost scrie in log “Access denied for user ghost”. Ai schimbat MYSQL_PASSWORD in .env dupa prima pornire. MySQL il citeste doar la initializare. Fie pui inapoi valoarea veche, fie schimbi parola reala: ruleaza sudo docker compose exec db mysql -uroot -p (iti cere parola de root), apoi ALTER USER 'ghost'@'%' IDENTIFIED BY 'parola-noua'; si recreeaza Ghost cu sudo docker compose up -d.
  • Ghost reporneste in bucla cu “ECONNREFUSED”. Baza de date nu e pornita. Verifica sudo docker compose ps si sudo docker compose logs db; pe servere mici, MySQL oprit fortat din lipsa de memorie e un clasic (vezi sectiunea despre RAM).
  • Emailurile de login sau inscriere nu ajung niciodata. Testeaza daca portul SMTP e accesibil de pe VPS cu timeout 5 bash -c '</dev/tcp/smtp.example.com/587' && echo deschis. Apoi verifica perechea port si SMTP_SECURE, datele de autentificare si ca MAIL_FROM foloseste un domeniu autorizat la furnizor.
  • Blocat afara din cauza codului de verificare a dispozitivului. Repara intai SMTP-ul. Doar ca ultima solutie, poti adauga security__staffDeviceVerification: "false" in mediul lui Ghost, recreezi containerul, te autentifici, repari emailul si apoi scoti setarea.
  • Erori de permisiuni la upload. Imaginea Ghost corecteaza de obicei proprietarul dosarului content la pornire; daca ai copiat fisiere manual, ruleaza sudo chown -R 1000:1000 /opt/ghost/content.

Note de securitate

  • Doar Caddy publica porturi. Nu adauga ports: la ghost sau db “doar pentru test”; Docker le-ar expune in internet indiferent de ufw.
  • Protejeaza .env si backup-urile. Contin parolele bazei de date, datele SMTP si, in dump-uri, emailurile membrilor. Pastreaza chmod 600, nu pune niciodata .env in Git si cripteaza copiile din afara serverului.
  • Revendica imediat contul de admin dupa prima pornire, foloseste o parola lunga si unica si da conturi de staff doar celor care chiar au nevoie.
  • Securizeaza SSH-ul. Foloseste autentificare cu chei si dezactiveaza autentificarea cu parola dupa ce cheia functioneaza.
  • Tine totul la zi. Activeaza unattended-upgrades pentru host si ruleaza regulat rutina de la Pasul 8, ca Ghost, MySQL si Caddy sa primeasca corecturile de securitate.
  • Trateaza grupul docker ca root. De aceea am folosit sudo; oricine e in grup poate prelua controlul serverului.

Deci, merita sa-ti hostezi singur Ghost?

Daca vrei un blog care arata profesionist din prima, un newsletter care iti apartine tie si nu unei platforme, si date care raman pe un server din Romania, sub controlul tau, Ghost cu Docker Compose e unul dintre cele mai placute stack-uri self-hosted pe care le poti rula. Instalarea inseamna cateva fisiere, backup-ul e un singur script, iar upgrade-urile sunt o rutina de trei comenzi, cu conditia sa respecti regula “intai backup”. Partile care cer atentie sunt livrabilitatea emailului si tinerea la zi a actualizarilor.

Poti porni acest stack pe un VPS de la noi in mai putin de o ora, sau poti lasa echipa noastra de administrare servere sa instaleze Ghost, sa configureze emailul si DNS-ul si sa se ocupe de backup-uri si upgrade-uri in locul tau.

Intrebari frecvente

Pot rula Ghost cu SQLite in loc de MySQL?

SQLite e suportat doar pentru dezvoltare locala. Pentru un site in productie, Ghost suporta oficial MySQL 8, de aceea ghidul foloseste mysql:8.4.

Pot gazdui mai multe bloguri Ghost pe un singur VPS?

Da. Fie creezi un al doilea dosar de proiect cu propriul fisier Compose si propria baza de date, fie adaugi inca o pereche ghost si db in acelasi fisier. In ambele cazuri, un singur Caddy se ocupa de toate domeniile, cu cate un bloc pentru fiecare site in Caddyfile, si doar acel Caddy publica porturile 80 si 443.

Chiar am nevoie de Mailgun?

Doar pentru newsletterele in masa. Inscrierile, login-urile membrilor si emailurile pentru staff merg cu orice furnizor SMTP. Daca doar publici articole si nu le trimiti pe email, SMTP e suficient.

E varianta cu Docker mai lenta decat o instalare clasica?

Nu exista o diferenta masurabila pentru un blog. Containerele ruleaza pe acelasi kernel ca host-ul; overhead-ul e neglijabil comparativ cu latenta de retea si designul temei.

Unde sunt stocate datele mele?

In /opt/ghost/mysql (baza de date), /opt/ghost/content (imagini si teme) si in volumul caddy_data (certificate). Pe un VPS DreamServer, toate raman in data center-ul nostru din Romania.

Avem Incredere & Suntem Membri

Suntem membri ai principalelor organizatii de infrastructura internet.

RIPE NCC MANRS PeeringDB RoTLD DSIX SBIX 4IXP LOCIX Euro-IX RIPE NCC MANRS PeeringDB RoTLD DSIX SBIX 4IXP LOCIX Euro-IX