Containere WordPress, MariaDB, Redis si Caddy intr-un stack Docker Compose
Docker

WordPress cu Docker Compose: configurare pentru productie

Metoda clasica de a gazdui WordPress este sa instalezi Apache, PHP si MySQL direct pe server. Merge foarte bine pana in ziua in care ai nevoie de alta versiune de PHP, te muti pe un server nou sau incerci sa-ti amintesti ce fisier de configurare ai modificat acum opt luni. Docker Compose rezolva exact problema asta: tot site-ul devine un singur fisier lizibil plus cateva foldere pe care le poti salva, copia si reconstrui oriunde. In acest tutorial construim un stack WordPress Docker Compose pe care l-am rula linistiti pentru o afacere reala. Avem WordPress, MariaDB si Redis in containere, Caddy in fata cu HTTPS automat, limite de upload suficient de mari pentru o biblioteca media reala, volume persistente, WP-CLI pentru treburile plictisitoare si o rutina de backup pe care chiar o testezi, nu doar speri ca merge.

Toate componentele sunt open source si vin din proiectele lor oficiale: WordPress , MariaDB , Redis , Caddy si Docker . Explicam fiecare concept prima data cand apare, asa ca poti urmari ghidul chiar daca este primul tau proiect cu containere.

Cum arata stack-ul nostru WordPress Docker Compose

Incepem cu cativa termeni pe care ii vom folosi tot timpul:

  • Container: un proces izolat care ruleaza pe server cu propriul sistem de fisiere, propriile biblioteci si setari. Spre deosebire de o masina virtuala, nu are un sistem de operare complet, asa ca porneste intr-o secunda si consuma foarte putina memorie.
  • Imagine: sablonul read-only din care se creeaza un container, de exemplu wordpress:php8.3-apache. Partea de dupa doua puncte este tag-ul, care alege versiunea.
  • Docker Compose: un utilitar care citeste un singur fisier YAML (compose.yaml) ce descrie mai multe containere si legaturile dintre ele. Apoi le porneste, le opreste sau le actualizeaza pe toate cu o singura comanda.
  • Volum: spatiu de stocare gestionat de Docker care supravietuieste cand un container este sters sau recreat. Baza de date si fisierele urcate stau in volume, niciodata in container.
  • Port: o “usa” numerotata a serverului. Traficul web foloseste portul 80 (HTTP) si 443 (HTTPS). A publica un port inseamna a face un container accesibil din internet.
  • Reverse proxy: un server care primeste primul fiecare cerere a vizitatorului, se ocupa de HTTPS si trimite cererea mai departe catre containerul potrivit din spatele lui.
  • Retea Docker: o retea virtuala privata intre containere. Containerele din aceeasi retea se gasesc dupa numele serviciului, de exemplu db sau redis.

Iata arhitectura pe care o construim:

VPS DreamServer (host Docker)retea frontendCaddyHTTPS, certificatevol: caddy_dataWordPressApache + PHP 8.3vol: wp_dataWP-CLIruleaza la cererevol: wp_dataretea backend (interna)Fara porturi publicate,fara acces la internetMariaDB 11.8baza de datevol: db_dataRedis 8object cachefara volumVizitatoribrowsere web443Servicii pe hostAdmin (SSH)ufwsshd :22 (host)ufw pazeste doar serviciile host.Porturile 80/443 Docker ocolesc ufw.
Doar Caddy publica porturi (80 si 443). WordPress, MariaDB si Redis raman in retele Docker private, iar ufw protejeaza serviciile de pe host, cum ar fi SSH.

Stack-ul are cinci servicii:

  1. caddy: reverse proxy-ul. Obtine si reinnoieste automat certificate Let’s Encrypt gratuite si trimite traficul catre WordPress.
  2. wordpress: imaginea oficiala cu Apache si PHP. Nu este expusa niciodata direct in internet.
  3. db: MariaDB, baza de date. Sta intr-o retea interna, fara drum spre internet.
  4. redis: un object cache in memorie. Pastreaza rezultatele interogarilor repetate, ca WordPress sa trimita mai putine interogari catre MariaDB.
  5. wpcli: utilitarul WordPress din linia de comanda. Porneste doar cand il chemam si foloseste aceleasi fisiere si aceleasi setari de baza de date ca WordPress.

De ce Caddy ca reverse proxy

Nginx cu Certbot merge perfect, dar inseamna doua unelte, un timer de reinnoire si un fisier de configurare plin de setari TLS. Caddy se ocupa singur de certificate, are setari HTTPS bune din start si are nevoie de vreo zece randuri de configurare pentru site-ul nostru. In plus, nu are implicit o limita pentru dimensiunea cererii, ceea ce elimina o cauza clasica de upload-uri esuate (celebrul “413 Request Entity Too Large” din Nginx).

Ce tag-uri de imagine folosim si de ce nu :latest

Un tag ca :latest se schimba fara sa-ti dai seama. Intr-o zi, docker compose pull iti aduce o versiune majora noua de baza de date pe care nu ai planificat-o. Noi fixam fiecare imagine pe o linie de versiune majora, ca sa primim in continuare patch-urile de securitate:

  • wordpress:php8.3-apache si wordpress:cli-php8.3: detaliul important este ca nucleul WordPress sta in volum si se actualizeaza singur (din dashboard sau cu WP-CLI). Tag-ul imaginii decide in principal versiunea de PHP si Apache. Exista si varianta php8.4. Orice alegi, tine tag-urile WordPress si CLI pe aceeasi versiune de PHP.
  • mariadb:11.8: linia actuala cu suport pe termen lung (LTS).
  • redis:8-alpine si caddy:2-alpine: versiunile majore actuale, pe baza mica Alpine.

Inainte sa incepi, verifica paginile de tag-uri de pe Docker Hub si notele de lansare ale fiecarui proiect pentru versiuni mai noi. Tag-urile evolueaza, iar tu vrei cea mai noua linie suportata.

De ce ai nevoie

  • Un VPS cu Debian 13 sau Ubuntu 24.04, de preferat KVM. 2 GB RAM sunt confortabili. 1 GB merge pentru un site mic daca adaugi swap. Planifica cel putin 20 GB de disc.
  • Un utilizator cu drepturi sudo si acces SSH. Nu lucram ca root.
  • Un nume de domeniu (in exemple folosim blog.example.com) cu o inregistrare A catre adresa IPv4 a VPS-ului. Adauga si o inregistrare AAAA daca VPS-ul are IPv6.
  • Porturile 80 si 443 libere pe server, deci fara Apache sau Nginx deja instalate pe host.
  • Cam 45 de minute si un terminal.

Cata memorie consuma de fapt toate astea? Mai putin decat se asteapta multi:

RAM in repaus per container (aproximativ)0100200300400RAM in MB (aproximativ)~25 MB~10 MB~130 MB~160 MB~325 MBCaddyRedisMariaDBWordPressTotalContainer, in repaus cu trafic redus
Valori aproximative masurate pe o instalare noua. Sub trafic real cresc procesele PHP si buffer pool-ul MariaDB, iar Redis este limitat la 128 MB, deci un VPS cu 2 GB lasa loc confortabil.

Procedura pe scurt

Vom trece prin opt pasi. Diagrama de mai jos este harta ta, iar fiecare pas are propriul titlu, cu acelasi numar:

WordPress cu Docker Compose in 8 pasi1DNS + updaterecord A, apt, ufw2Instalare DockerEngine + Compose3Proiect + .envparole aleatoare4PHP + Caddylimite upload, TLS5compose.yaml5 servicii6Pornire + setupup -d, instalare7WP-CLI + Rediscache, cron real8Backupdump, tar, restoreServerStackOperare
Pasii 1-2 pregatesc serverul, pasii 3-6 construiesc si pornesc stack-ul, iar pasii 7-8 acopera operarea de zi cu zi: WP-CLI, cache, cron si backup.

Pasul 1: Indreapta domeniul spre VPS si actualizeaza serverul

Mai intai, intra la furnizorul tau de DNS si creeaza o inregistrare A pentru blog.example.com cu adresa IPv4 a VPS-ului. Schimbarile DNS pot dura cateva minute pana se propaga. Caddy are nevoie ca inregistrarea sa functioneze inainte sa pornim stack-ul, pentru ca Let’s Encrypt verifica daca domeniul chiar arata spre serverul tau. Poti verifica direct de pe VPS:

getent hosts blog.example.com

Rezultatul trebuie sa contina IP-ul VPS-ului. Daca folosesti Cloudflare, pune inregistrarea pe “DNS only” (norul gri) cat timp faci configurarea. Poti porni proxy-ul mai tarziu, cu modul SSL “Full (strict)”.

Apoi aducem sistemul la zi:

sudo apt update
sudo apt full-upgrade -y

Acum activam actualizarile automate de securitate. Pe Ubuntu 24.04, unattended-upgrades este instalat si activ din start. Pe Debian 13, il instalezi si il activezi tu:

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Trebuie sa dai reboot? Pe Ubuntu, sistemul creeaza un fisier-semnal cand e nevoie de restart:

[ -f /var/run/reboot-required ] && echo "Este nevoie de reboot"

Debian nu creeaza mereu acest fisier, asa ca regula acolo e simpla: dai reboot daca actualizarea a instalat un pachet linux-image nou (il vezi in output-ul apt). Folosesti sudo reboot si te reconectezi dupa un minut.

La final, configuram un firewall de baza cu ufw pentru host. Ubuntu are ufw instalat deja, Debian nu:

sudo apt install -y ufw        # necesar pe Debian, deja prezent pe Ubuntu
sudo ufw allow OpenSSH         # daca SSH ruleaza pe alt port, permite acel port
sudo ufw allow 80/tcp
sudo ufw allow 443
sudo ufw enable
sudo ufw status

Un detaliu important care ii incurca pe multi: Docker isi scrie propriile reguli de firewall si ocoleste ufw pentru porturile publicate de containere. ufw protejeaza serviciile care ruleaza direct pe host, cum e SSH. Nu filtreaza porturile publicate de Docker. De aceea, in stack-ul nostru doar Caddy publica porturi, iar baza de date si Redis nu sunt publicate deloc.

Pasul 2: Instaleaza Docker Engine si Docker Compose

Instalam Docker din depozitul oficial Docker, nu pachetul mai vechi din distributie, ca sa avem plugin-ul actual Compose v2. Comenzile merg atat pe Debian, cat si pe Ubuntu, pentru ca citesc numele distributiei din /etc/os-release:

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

Fisierul docker.sources foloseste formatul modern deb822, pe care apt il prefera acum. Apoi permitem utilizatorului tau sa ruleze Docker fara sudo, iar dupa aceea te deconectezi si te reconectezi, ca sa se aplice schimbarea de grup:

sudo usermod -aG docker "$USER"
exit

Dupa reconectare, verifici ca totul merge:

docker run --rm hello-world
docker compose version

Trebuie sa vezi “Hello from Docker!” si o versiune de Compose care incepe cu v2. Tine minte ca apartenenta la grupul docker echivaleaza practic cu acces root, deci adauga acolo doar utilizatori in care ai incredere totala.

Nota pentru LXC: Docker intr-un container LXC neprivilegiat are nevoie de optiunea nesting (si de obicei keyctl) activata pe container. Fara ele, daemon-ul Docker nu porneste sau containerele dau erori de permisiuni. Pe un VPS KVM problema nu exista. Daca ai la noi un VPS LXC si Docker nu porneste, cere suportului sa activeze nesting.

Pasul 3: Creeaza folderul proiectului si secretele din .env

Toate fisierele site-ului stau intr-un singur folder. Asa proiectul se salveaza si se muta foarte usor:

mkdir -p ~/wordpress/php
cd ~/wordpress

Parolele nu au ce cauta in compose.yaml. Le punem intr-un fisier .env, pe care Docker Compose il citeste automat din folderul proiectului. Comanda de mai jos genereaza parole aleatoare in hex. Caracterele hex (0-9, a-f) sunt sigure in YAML, shell si PHP, deci nu ai surprize cu escaparea. Inlocuieste domeniul si adresa de email cu ale tale:

cat > .env <<EOF
DOMAIN=blog.example.com
ACME_EMAIL=admin@example.com
DB_NAME=wordpress
DB_USER=wordpress
DB_PASSWORD=$(openssl rand -hex 24)
DB_ROOT_PASSWORD=$(openssl rand -hex 24)
REDIS_PASSWORD=$(openssl rand -hex 24)
EOF
chmod 600 .env
cat .env

chmod 600 face fisierul lizibil doar pentru utilizatorul tau. ACME_EMAIL este adresa la care Let’s Encrypt trimite notificari despre certificate.

O regula pe care merita s-o retii de acum: MariaDB citeste DB_NAME, DB_USER si parolele doar la prima initializare a volumului. Daca schimbi parola in .env mai tarziu, baza de date o pastreaza pe cea veche, iar WordPress va da eroarea “Error establishing a database connection”. Ca sa schimbi o parola mai tarziu, o schimbi intai in MariaDB si abia apoi in .env.

Pasul 4: Seteaza limitele de upload PHP si scrie Caddyfile-ul

Imaginea oficiala WordPress foloseste limita implicita de upload din PHP, de 2 MB, prea mica pentru orice in afara de cateva fotografii. Rezolvam asta cu un mic fisier de configurare PHP montat in container:

cat > php/uploads.ini <<'EOF'
file_uploads = On
upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 120
max_input_vars = 3000
EOF

Ce face fiecare rand:

  • upload_max_filesize: cel mai mare fisier individual pe care il poti urca.
  • post_max_size: cel mai mare formular trimis in total. Trebuie sa fie egal sau mai mare decat limita de upload.
  • memory_limit: cata memorie poate folosi o singura cerere PHP. Page builder-ele si procesarea imaginilor au nevoie de mai mult decat valoarea implicita.
  • max_execution_time: cat poate rula o cerere inainte ca PHP s-o opreasca, util la importuri mari.
  • max_input_vars: ridica limita pentru meniuri mari si pagini de optiuni care altfel s-ar salva doar partial.

Acum Caddyfile-ul, fisierul de configurare al lui Caddy:

cat > Caddyfile <<'EOF'
{
    email {$ACME_EMAIL}
}

{$DOMAIN} {
    encode zstd gzip

    # XML-RPC e o tinta frecventa de brute-force; sterge aceste doua randuri daca folosesti Jetpack sau aplicatia mobila
    @xmlrpc path /xmlrpc.php
    respond @xmlrpc 403

    header {
        X-Content-Type-Options "nosniff"
        Referrer-Policy "strict-origin-when-cross-origin"
    }

    reverse_proxy wordpress:80
}

# Decomenteaza daca ai si o inregistrare DNS pentru www:
# www.{$DOMAIN} {
#     redir https://{$DOMAIN}{uri} permanent
# }
EOF

{$DOMAIN} si {$ACME_EMAIL} sunt variabile de mediu pe care le vom da containerului Caddy. encode comprima raspunsurile. reverse_proxy wordpress:80 trimite totul catre containerul WordPress, pe care Caddy il gaseste dupa nume in reteaua Docker. Caddy adauga automat header-ul X-Forwarded-Proto: https. Imaginea oficiala WordPress citeste acest header, deci WordPress stie ca vizitatorul este pe HTTPS si nu apar bucle de redirect. Imaginea este configurata si sa aiba incredere in X-Forwarded-For venit din retele private, asa ca log-urile si pluginurile de securitate vad IP-ul real al vizitatorului, nu adresa lui Caddy.

Pasul 5: Scrie fisierul WordPress Docker Compose

Aceasta este inima configurarii. Creeaza compose.yaml:

name: wordpress

x-wp-env: &wp-env
  WORDPRESS_DB_HOST: db
  WORDPRESS_DB_NAME: ${DB_NAME}
  WORDPRESS_DB_USER: ${DB_USER}
  WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
  WORDPRESS_TABLE_PREFIX: wp_
  WORDPRESS_CONFIG_EXTRA: |
    define('WP_REDIS_HOST', 'redis');
    define('WP_REDIS_PORT', 6379);
    define('WP_REDIS_PASSWORD', '${REDIS_PASSWORD}');
    define('WP_CACHE_KEY_SALT', '${DOMAIN}:');
    define('DISABLE_WP_CRON', true);
    define('DISALLOW_FILE_EDIT', true);

services:
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    environment:
      DOMAIN: ${DOMAIN}
      ACME_EMAIL: ${ACME_EMAIL}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - frontend
    depends_on:
      - wordpress

  wordpress:
    image: wordpress:php8.3-apache
    restart: unless-stopped
    environment: *wp-env
    volumes:
      - wp_data:/var/www/html
      - ./php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro
    networks:
      - frontend
      - backend
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: mariadb:11.8
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: ${DB_NAME}
      MARIADB_USER: ${DB_USER}
      MARIADB_PASSWORD: ${DB_PASSWORD}
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MARIADB_AUTO_UPGRADE: "1"
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - backend
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s

  redis:
    image: redis:8-alpine
    restart: unless-stopped
    command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}", "--maxmemory", "128mb", "--maxmemory-policy", "allkeys-lru", "--save", "", "--appendonly", "no"]
    networks:
      - backend

  wpcli:
    image: wordpress:cli-php8.3
    profiles: ["tools"]
    user: "33:33"
    environment:
      <<: *wp-env
      WP_CLI_CACHE_DIR: /tmp/wp-cli-cache
    volumes:
      - wp_data:/var/www/html
    networks:
      - frontend
      - backend
    depends_on:
      db:
        condition: service_healthy

networks:
  frontend:
  backend:
    internal: true

volumes:
  wp_data:
  db_data:
  caddy_data:
  caddy_config:

Sa trecem prin partile care conteaza:

  • name: wordpress fixeaza numele proiectului. Docker pune acest prefix in fata numelor de volume, deci fisierele WordPress ajung intr-un volum numit exact wordpress_wp_data. Scriptul de backup se bazeaza pe acest nume.
  • x-wp-env: &wp-env este un bloc de setari refolosibil (o “ancora” YAML). WordPress si WP-CLI trebuie sa vada exact aceleasi setari de baza de date, asa ca le definim o data si le refolosim cu *wp-env si <<: *wp-env.
  • WORDPRESS_CONFIG_EXTRA adauga randuri PHP in wp-config.php la rulare. Ele configureaza conexiunea Redis, dau fiecarui site propriul prefix de cache, opresc cron-ul intern “declansat de vizitatori” (il inlocuim cu unul real la pasul 7) si dezactiveaza editorul de fisiere din dashboard.
  • Doar Caddy are sectiunea ports:. WordPress, MariaDB si Redis sunt accesibile doar din interiorul retelelor Docker. Portul 443/udp activeaza HTTP/3.
  • Reteaua backend este internal: true, deci MariaDB si Redis nu au deloc acces la internet. WordPress este in ambele retele, ca sa poata descarca actualizari si pluginuri.
  • Healthcheck-ul foloseste scriptul inclus in imaginea MariaDB. condition: service_healthy il face pe WordPress sa astepte pana cand baza de date accepta conexiuni, ceea ce evita erorile la prima pornire.
  • MARIADB_AUTO_UPGRADE ruleaza automat procedura interna de upgrade cand treci la o versiune minora noua de MariaDB.
  • Redis ruleaza ca simplu cache: foloseste maximum 128 MB, elimina cheile cele mai vechi cand se umple si nu scrie nimic pe disc. Pierderea cache-ului la restart nu strica nimic, pentru ca se umple din nou.
  • wpcli este in profilul tools, deci docker compose up nu il porneste. Ruleaza ca utilizator 33:33 (www-data in imaginea WordPress), astfel incat fisierele create de el au acelasi proprietar ca fisierele scrise de Apache.

Inainte sa pornesti ceva, cere-i lui Compose sa valideze fisierul:

docker compose config --quiet && echo "compose.yaml este valid"

Daca ai o greseala de scriere sau de indentare, Compose iti spune randul. YAML este strict cu indentarea, asa ca foloseste mereu spatii, niciodata tab-uri.

Pasul 6: Porneste stack-ul si instaleaza WordPress

Descarcam imaginile si pornim totul in fundal (-d inseamna detached):

docker compose pull
docker compose up -d
docker compose ps

Dupa 20-40 de secunde, docker compose ps trebuie sa arate caddy, wordpress, db si redis, cu baza de date marcata healthy. Acum urmarim cum isi obtine Caddy certificatul:

docker compose logs -f caddy

Cauta un rand care contine certificate obtained successfully, apoi apasa Ctrl+C ca sa nu mai urmaresti log-urile (containerele merg mai departe). Testeaza de pe server:

curl -I https://blog.example.com

Un WordPress proaspat raspunde cu HTTP/2 302 si un header location: catre /wp-admin/install.php. Asta inseamna ca HTTPS, Caddy si WordPress functioneaza. Confirmam si ca limitele de upload sunt active:

docker compose exec wordpress php -r 'echo ini_get("upload_max_filesize"), " / ", ini_get("post_max_size"), PHP_EOL;'

Rezultatul asteptat este 64M / 64M.

Acum termina instalarea imediat, pentru ca pana atunci oricine gaseste adresa poate completa asistentul de instalare. Ai doua variante:

  • In browser: deschide https://blog.example.com, alege limba si seteaza titlul site-ului, un nume de administrator (nu “admin”) si o parola puternica.
  • Cu WP-CLI, fara sa scrii parola in istoricul shell-ului:
docker compose run --rm wpcli wp core install \
  --url="https://blog.example.com" \
  --title="Site-ul meu" \
  --admin_user="siteadmin" \
  --admin_email="admin@example.com" \
  --skip-email \
  --prompt=admin_password

WP-CLI iti cere parola interactiv si raspunde cu Success: WordPress installed successfully.

Pasul 7: Foloseste WP-CLI pentru Redis cache si un cron real

WP-CLI este interfata oficiala in linie de comanda pentru WordPress. Poate instala pluginuri, rula actualizari, gestiona utilizatori si schimba setari fara sa dai click prin dashboard, si este perfecta pentru scripturi. Serviciul nostru wpcli porneste un container de scurta durata, ruleaza o comanda si apoi se sterge singur (--rm).

Activeaza Redis object cache

docker compose run --rm wpcli wp plugin install redis-cache --activate
docker compose run --rm wpcli wp redis enable
docker compose run --rm wpcli wp redis status

Prima comanda instaleaza pluginul Redis Object Cache. A doua copiaza fisierul object-cache.php in wp-content. Output-ul de status trebuie sa contina Status: Connected. De acum, interogarile repetate (optiuni, meniuri, metadate de articole) sunt servite din memorie. Pe site-urile aglomerate si in zona de administrare, timpul de generare a paginilor scade vizibil.

O scurtatura ca sa scrii doar “wp”

Sa scrii de fiecare data docker compose run --rm wpcli wp devine obositor repede. Adauga un alias:

echo "alias wp='docker compose -f \$HOME/wordpress/compose.yaml run --rm wpcli wp'" >> ~/.bashrc
source ~/.bashrc
wp core version

Alias-ul merge din orice folder, pentru ca -f ii spune lui Compose unde sta proiectul. Cateva comenzi pe care le vei folosi des:

wp plugin list                       # pluginuri instalate si actualizari disponibile
wp core update                       # actualizeaza nucleul WordPress
wp plugin update --all               # actualizeaza toate pluginurile
wp theme update --all                # actualizeaza toate temele
wp user list                         # lista conturilor
wp option get home                   # verifica adresa site-ului
wp search-replace 'http://old.example.com' 'https://blog.example.com' --dry-run

search-replace cu --dry-run iti arata ce s-ar schimba, de exemplu dupa o migrare. Il rulezi apoi fara --dry-run ca sa aplici schimbarile.

Inlocuieste WP-Cron cu un cron adevarat

Implicit, WordPress ruleaza sarcinile programate (publicarea articolelor programate, joburile pluginurilor) doar cand cineva viziteaza site-ul. Pe un site linistit ruleaza cu intarziere, iar pe unul aglomerat verificarea se face la fiecare incarcare de pagina. Am dezactivat asta in compose.yaml, asa ca acum il programam corect de pe host. Pe Debian, instaleaza cron daca lipseste (sudo apt install -y cron). Ubuntu il are deja. Apoi deschide crontab-ul cu crontab -e si adauga randul de mai jos, inlocuind deploy cu numele tau de utilizator:

*/5 * * * * cd /home/deploy/wordpress && docker compose run --rm -T wpcli wp cron event run --due-now --quiet >/dev/null 2>&1

La fiecare cinci minute, ruleaza toate sarcinile WordPress scadente. Optiunea -T dezactiveaza terminalul, pe care cron nu il are.

Pasul 8: Fa backup site-ului si testeaza o restaurare

Un site WordPress inseamna trei lucruri: baza de date (articole, setari, utilizatori), fisierele din wp_data (upload-uri, teme, pluginuri, wp-config.php) si configurarea noastra (compose.yaml, Caddyfile, php/, .env). Scriptul de mai jos le salveaza pe toate trei. Creeaza ~/wordpress/backup.sh:

#!/usr/bin/env bash
# backup.sh: dump baza de date + arhiva wp_data + configurare, pastrate 14 zile
set -euo pipefail
umask 077

cd "$(dirname "$(readlink -f "$0")")"
STAMP="$(date +%F-%H%M)"
DEST="$PWD/backups"
mkdir -p "$DEST"

# 1. Baza de date: dump consistent, parola este citita in interiorul containerului
docker compose exec -T db sh -c \
  'MYSQL_PWD="$MARIADB_PASSWORD" exec mariadb-dump -u "$MARIADB_USER" --single-transaction --quick --routines --triggers "$MARIADB_DATABASE"' \
  | gzip > "$DEST/db-$STAMP.sql.gz"

# 2. Fisiere: upload-uri, teme, pluginuri, wp-config.php
docker run --rm \
  -v wordpress_wp_data:/data:ro \
  -v "$DEST":/backup \
  debian:stable-slim \
  sh -c "tar --numeric-owner -czf /backup/wp-files-$STAMP.tar.gz -C /data . && chown $(id -u):$(id -g) /backup/wp-files-$STAMP.tar.gz && chmod 600 /backup/wp-files-$STAMP.tar.gz"

# 3. Configurare (contine secrete, pastreaz-o privata)
tar -czf "$DEST/config-$STAMP.tar.gz" compose.yaml Caddyfile php .env

# 4. Retentie: sterge arhivele mai vechi de 14 zile
find "$DEST" -type f -name '*.gz' -mtime +14 -delete

echo "Backup $STAMP terminat"

Cateva detalii care merita explicate:

  • Parola bazei de date nu apare niciodata intr-o linie de comanda. Ghilimelele simple fac ca shell-ul tau sa trimita textul neschimbat, iar variabilele sunt expandate in containerul MariaDB, unde exista deja. MYSQL_PWD ii da parola lui mariadb-dump fara s-o puna in lista de procese.
  • --single-transaction ofera o imagine consistenta fara sa blocheze site-ul.
  • Arhiva de fisiere este creata de un container temporar care ruleaza ca root, asa ca facem chown inapoi catre utilizatorul tau. Altfel ai primi “permission denied” cand vrei s-o copiezi de pe server.
  • umask 077 face ca fiecare backup nou sa fie lizibil doar pentru tine, lucru important pentru ca backup-urile contin secretele tale.

Fa-l executabil si ruleaza-l o data:

chmod +x ~/wordpress/backup.sh
~/wordpress/backup.sh
ls -lh ~/wordpress/backups

Trebuie sa vezi trei fisiere: db-...sql.gz, wp-files-...tar.gz si config-...tar.gz. Ca backup-ul sa ruleze in fiecare noapte la 03:30, adauga in crontab -e:

30 3 * * * /home/deploy/wordpress/backup.sh >> /home/deploy/wordpress/backups/backup.log 2>&1

Copiaza backup-urile in afara serverului

Un backup care sta doar pe acelasi VPS dispare odata cu VPS-ul. Trimite-l pe alta masina cu rsync prin SSH. Daca masina aceea foloseste un port SSH nestandard, da-i propriul alias in ~/.ssh/config, ca sa nu-ti afecteze conexiunile SSH obisnuite:

# ~/.ssh/config
Host backup-box
    HostName backup.example.net
    Port 2222
    User backupuser
    IdentityFile ~/.ssh/id_ed25519_backup

Apoi, cu o cheie fara parola instalata pe masina respectiva, programeaza in cron, dupa backup:

45 3 * * * rsync -az /home/deploy/wordpress/backups/ backup-box:/srv/backups/wordpress/

Daca destinatia nu este complet sub controlul tau, cripteaza arhivele inainte sau foloseste un utilitar dedicat de backup, cum este restic .

Testeaza o restaurare (intr-o baza de date goala)

Un backup pe care nu l-ai restaurat niciodata este doar o speranta. Cel mai sigur loc pentru exercitiu este un al doilea VPS, de unica folosinta, cu acelasi folder de proiect. Procedura este aceeasi si pe serverul live. Regula de aur: nu importa niciodata un dump peste baza de date live. Opresti WordPress, recreezi o baza de date goala, apoi importi:

cd ~/wordpress
docker compose stop wordpress

# 1. Sterge si recreeaza o baza de date goala (parola root e citita in container)
docker compose exec -T db sh -c \
  'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" exec mariadb -u root -e "DROP DATABASE IF EXISTS $MARIADB_DATABASE; CREATE DATABASE $MARIADB_DATABASE CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"'

# 2. Importa dump-ul (foloseste numele fisierului tau)
gunzip -c backups/db-2026-11-10-0330.sql.gz | docker compose exec -T db sh -c \
  'MYSQL_PWD="$MARIADB_PASSWORD" exec mariadb -u "$MARIADB_USER" "$MARIADB_DATABASE"'

# 3. Inlocuieste fisierele din volumul wp_data
docker run --rm \
  -v wordpress_wp_data:/data \
  -v "$PWD/backups":/backup:ro \
  debian:stable-slim \
  sh -c "find /data -mindepth 1 -delete && tar --numeric-owner -xzf /backup/wp-files-2026-11-10-0330.tar.gz -C /data"

# 4. Porneste WordPress si goleste object cache-ul
docker compose start wordpress
docker compose run --rm wpcli wp cache flush

Utilizatorul WordPress isi pastreaza drepturile, pentru ca MariaDB nu sterge permisiunile la nivel de baza de date atunci cand baza este stearsa. Deschide site-ul, autentifica-te si verifica cateva articole si imagini. Daca totul arata bine, backup-urile tale sunt reale.

Cum tii stack-ul la zi

Exista doua straturi de actualizat si e util sa le tii separat in minte:

cd ~/wordpress
./backup.sh                 # intotdeauna backup intai
docker compose pull         # imagini noi: patch-uri PHP, Apache, MariaDB, Redis, Caddy
docker compose up -d        # recreeaza doar containerele cu imagine schimbata
docker image prune -f       # sterge imaginile vechi nefolosite
wp core update && wp plugin update --all && wp theme update --all

docker compose pull actualizeaza software-ul din jurul lui WordPress. wp core update actualizeaza WordPress-ul insusi, pentru ca nucleul sta in volum. Ca sa treci la o versiune PHP mai noua, schimbi atat wordpress:php8.3-apache, cat si wordpress:cli-php8.3, rulezi docker compose up -d si testezi tema si pluginurile. Ca sa treci la o linie LTS noua de MariaDB, faci backup, schimbi tag-ul si lasi MARIADB_AUTO_UPGRADE sa faca restul. Nu sari peste versiuni majore intermediare fara sa citesti notele de upgrade MariaDB.

Depanare si greseli frecvente

Caddy nu poate obtine certificatul

Ruleaza docker compose logs caddy. Cauzele obisnuite sunt: inregistrarea A nu arata inca spre acest VPS (verifici cu getent hosts), portul 80 este blocat de un firewall extern sau proxy-ul Cloudflare este activ la prima emitere. Rezolvi cauza si rulezi docker compose restart caddy. Let’s Encrypt limiteaza esecurile repetate, asa ca nu-l reporni in bucla.

“Error establishing a database connection”

Verifica docker compose ps: este db healthy? Daca da, parola din .env difera probabil de cea salvata in baza de date. Se intampla cand .env a fost editat dupa prima pornire sau cand a ramas un volum db_data vechi. Pe o instalare noua, fara continut, docker compose down -v sterge volumele ca sa o iei de la capat. Nu rula niciodata asta pe un site cu date reale.

Bucle de redirect sau avertismente de mixed content

De obicei vin de la un proxy in plus (de exemplu Cloudflare cu SSL “Flexible”) care vorbeste HTTP simplu cu Caddy. Foloseste “Full (strict)”. Verifica si ca wp option get home si wp option get siteurl incep amandoua cu https://.

“The uploaded file exceeds the upload_max_filesize directive”

Verifica valorile PHP cu comanda php -r de la pasul 6. Daca inca arata 2M, calea catre php/uploads.ini din compose.yaml este gresita sau nu ai rulat docker compose up -d dupa ce ai editat-o. Daca ai pus in fata alt proxy, de exemplu Nginx, mareste si client_max_body_size acolo.

WP-CLI spune “permission denied” sau “This does not seem to be a WordPress installation”

Primul mesaj inseamna ca WP-CLI a rulat fara user: "33:33". Al doilea inseamna ca l-ai rulat inainte ca WordPress sa-si copieze fisierele in volum. Porneste intai stack-ul cu docker compose up -d, asteapta cateva secunde, apoi incearca din nou.

“port is already allocated” pentru 80 sau 443

Ceva de pe host foloseste deja portul, de multe ori un Apache sau Nginx instalat mai demult. Il gasesti cu sudo ss -tlnp | grep -E ':80 |:443 ', apoi opresti si dezactivezi acel serviciu.

Am modificat .env sau compose.yaml si nu s-a schimbat nimic

Containerele isi pastreaza setarile vechi pana sunt recreate. Ruleaza docker compose up -d, iar Compose recreeaza doar ce s-a schimbat. (Retine exceptia: parolele MariaDB salvate intr-un volum existent.)

Note de securitate

  • Docker ocoleste ufw. Nu adauga niciodata ports: la db sau redis. Daca ai nevoie vreodata de acces la baza de date de pe laptop, foloseste un tunel SSH in loc sa publici portul 3306.
  • Grupul docker inseamna root. Oricine e in el poate controla tot serverul. Tine grupul cat mai mic.
  • Protejeaza-ti secretele. .env ramane cu chmod 600, nu ajunge niciodata intr-un repository Git public, iar backup-urile il contin si ele. Cripteaza tot ce pleaca de pe server.
  • Securizeaza si WordPress-ul. Foloseste un nume de administrator unic, o parola lunga si un plugin de autentificare in doi pasi. DISALLOW_FILE_EDIT este deja setat, iar XML-RPC este blocat la nivel de proxy.
  • Actualizeaza regulat. Cele mai multe compromiteri WordPress vin din pluginuri vechi, nu din server. Un wp plugin update --all saptamanal (dupa backup) previne multe probleme.
  • HSTS mai tarziu. Dupa ce esti sigur ca HTTPS merge pentru toate subdomeniile pe care le folosesti, poti adauga Strict-Transport-Security "max-age=31536000" in blocul header din Caddy. Nu-l activa din prima zi, pentru ca e greu de anulat.
  • Ca pas suplimentar de securizare, secretele Docker (WORDPRESS_DB_PASSWORD_FILE si MARIADB_PASSWORD_FILE) pot inlocui variabilele de mediu.

Deci, este Docker Compose modul potrivit de a rula WordPress?

Pentru majoritatea site-urilor mici si medii, da. Ai o configurare reproductibila descrisa intr-un singur fisier, HTTPS care se reinnoieste singur, o baza de date neexpusa in internet, un object cache adevarat, limite de upload rezonabile si un backup pe care chiar l-ai restaurat. Mutarea pe un server mai mare devine “copiezi folderul, restaurezi backup-ul, actualizezi DNS-ul”. Pretul este ca acum tu esti sysadmin-ul: cineva trebuie sa aplice actualizarile, sa urmareasca spatiul pe disc si sa verifice ca backup-urile de noapte chiar ajung la destinatie.

Poti porni acest stack pe un VPS din data center-ul nostru din Romania in cateva minute. Daca preferi sa te ocupi de continut si sa lasi actualizarile, monitorizarea si backup-urile in grija altcuiva, echipa noastra de administrare servere poate construi si intretine toata configuratia pentru tine.

Intrebari frecvente

Pot gazdui mai multe site-uri WordPress pe un singur VPS cu aceasta configurare?

Da. Creezi cate un folder pentru fiecare site, fiecare cu propriul compose.yaml (valori name: diferite), propria baza de date si propriile volume, si folosesti o singura instanta Caddy in fata tuturor, conectata la reteaua frontend a fiecarui site. Pe un server, un singur container poate publica porturile 80 si 443.

De ce nu folosim pur si simplu tag-urile :latest?

Pentru ca :latest poate sari la o versiune majora noua la un docker compose pull de rutina, lucru care conteaza mai ales la baza de date. Tag-urile fixate pe versiune majora sau LTS primesc in continuare patch-uri de securitate, iar upgrade-urile raman o decizie constienta.

Merge intr-un container LXC?

Da, daca optiunea nesting este activata pentru container. Containerele LXC neprivilegiate fara nesting nu pot rula corect daemon-ul Docker. Un VPS KVM nu are aceasta limitare.

Ar trebui sa folosesc Nginx in loc de Caddy?

Poti. Nginx cu Certbot este o alegere solida. Nu uita doar sa setezi client_max_body_size pe masura limitelor PHP si sa trimiti mai departe header-ul X-Forwarded-Proto. Noi am ales Caddy pentru ca HTTPS-ul automat si configurarea scurta inseamna mai putine piese care se pot strica.

Unde sunt stocate fizic fisierele urcate?

In volumul Docker wordpress_wp_data, sub /var/lib/docker/volumes/ pe host. Nu edita manual fisierele de acolo. Foloseste WP-CLI, dashboard-ul sau comenzile de backup si restaurare de la pasul 8.

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