Docker Compose za WordPress development: kompletan setup od lokala do produkcije

· 6 min
Sadržaj

Dolazim iz vercel dev sveta. Kucam komandu, dev server radi. Hot reload. HTTPS. Sve "just works."

WordPress lokalni development experience je… drugačiji.

MAMP. Local by Flywheel. XAMPP. Vagrant. Ili, ako ste masohista, ručna instalacija Apache + PHP + MySQL.

Hteo sam setup koji liči na ono što sam navikao. Docker je bio odgovor.

Šta sam hteo

  1. Jedan command za startpnpm dev:docker:start i sve radi
  2. HTTPS lokalno — Jer neke stvari (Turnstile, cookies) zahtevaju HTTPS
  3. Hot reload za temu — Promenim PHP/CSS, osveži se
  4. Email catching — Da vidim email-ove bez slanja
  5. Production parity — Isto što lokalno, to u produkciji

Docker Compose: 6 servisa

# docker-compose.local.yml

services:
  caddy:
    image: caddy:2-alpine
    ports:
      - "80:80"
      - "443:443"
    # Reverse proxy, HTTPS termination

  wordpress:
    image: wordpress:6-php8.2-fpm
    volumes:
      - ./apps/studenti-theme:/var/www/html/wp-content/themes/studently-theme
      - ./apps/studenti-plugin:/var/www/html/wp-content/plugins/studenti-functions
    # PHP-FPM, theme/plugin mounted from repo

  mariadb:
    image: mariadb:11
    volumes:
      - db_data:/var/lib/mysql
    # Database with persistent volume

  redis:
    image: redis:7-alpine
    # Object cache

  node-api:
    build: ./apps/studenti-node-api
    volumes:
      - ./apps/studenti-node-api:/app
    # Hot reload for Node API

  mailpit:
    image: axllent/mailpit
    ports:
      - "8025:8025"
    # Email catching UI

Jedan docker compose up i sve radi.

Kako servisi komuniciraju

                    ┌─────────────────────────────┐
                    │        Korisnik (Browser)     │
                    └──────────────┬────────────────┘
                                   │ HTTPS (:443)
                    ┌──────────────▼────────────────┐
                    │         Caddy (reverse proxy)  │
                    │   Automatski HTTPS, routing     │
                    └───────┬──────────────┬─────────┘
                            │              │
              studenti.local│              │api.studenti.local
                            │              │
                ┌───────────▼───┐   ┌──────▼──────────┐
                │  WordPress     │   │   Node API       │
                │  (PHP-FPM)     │   │   (Hono + TS)    │
                └──┬─────┬──────┘   └──────┬──────────┘
                   │     │                 │
          ┌────────▼┐  ┌─▼───────┐   ┌────▼─────┐
          │ MariaDB  │  │  Redis   │   │ ClamAV   │
          │ (baza)   │  │  (keš)   │   │ (virusi) │
          └──────────┘  └──────────┘   └──────────┘

                ┌─────────────────────────────┐
                │  Mailpit (lokalno) / SMTP2Go │
                │  (produkcija) — email        │
                └─────────────────────────────┘

Svaki servis je izolovan u svom kontejneru. Caddy prima sve zahteve i prosleđuje ih na pravi servis.

Caddy: Zašto ne nginx

Nginx je industrijski standard. Ali Caddy nudi ozbiljne prednosti koje daleko prevazilaze "lakšu konfiguraciju":

Automatski HTTPS — svuda

Lokalno: tls internal generiše sertifikat. U produkciji: automatski Let's Encrypt sa nula konfiguracije. Obnova sertifikata? Automatska. Nema crontab-a, nema certbot-a, nema zaboravljenih isteklih sertifikata u 3 ujutru.

HTTP/2 i HTTP/3 iz kutije

Caddy automatski omogućava HTTP/2 za sve HTTPS konekcije i HTTP/3 (QUIC) kad je dostupan. Nginx zahteva eksplicitnu konfiguraciju i rekompajliranje za HTTP/3.

Drastično jednostavnija konfiguracija

Caddy:

studenti.local {
    tls internal
    reverse_proxy wordpress:9000
}

api.studenti.local {
    tls internal
    reverse_proxy node-api:3005
}

Ekvivalent u nginx-u zahteva 30+ linija: server blok, listen direktive, ssl_certificate putanje, proxy_pass, proxy_set_header za svaki servis, i posebnu konfiguraciju za WebSocket upgrade.

Ugrađeni reverse proxy

Nema potrebe za dodatnim modulima. reverse_proxy je deo jezgra Caddy servera, sa automatskim health check-ovima, load balancing-om, i header rewriting-om.

Memorijski siguran

Caddy je napisan u Go-u (memorijski siguran jezik), za razliku od nginx-a koji je u C-u. Manje šansi za buffer overflow ranjivosti.

Za nginx, trebalo bi: generisati sertifikate, konfigurisati SSL parametre, podesiti proxy headere, restartovati posle svake promene. Caddy: 6 linija i gotovo.

Theme/Plugin mounting

Ključ za hot reload: mount iz repozitorijuma.

volumes:
  - ./apps/studenti-theme:/var/www/html/wp-content/themes/studently-theme
  - ./apps/studenti-plugin:/var/www/html/wp-content/plugins/studenti-functions

Promenim fajl u apps/studenti-theme/. WordPress odmah vidi promenu.

Git je source of truth. WordPress čita iz Git-a. Nikada ne editujem fajlove "u WordPress-u."

Mailpit: Email bez slanja

Svaki WordPress sajt šalje email-ove. Verifikacija, notifikacije, admin alerts.

U development-u, ne želite da šaljete prave email-ove. Želite da ih VIDITE.

Mailpit hvata sve email-ove i prikazuje u web UI na localhost:8025.

WordPress → wp_mail() → Mailpit → Vi vidite email u browseru

Nikakva konfiguracija u WordPress-u. Mailpit je SMTP server koji hvata sve.

Redis: 50-70% manje upita

WordPress voli database upite. Svaka stranica, desetine upita.

Redis object cache keširaj rezultate. Sledeći request, Redis vraća umesto MySQL-a.

// wp-config.php
define('WP_REDIS_HOST', 'redis');

Plugin: Redis Object Cache (free).

Rezultat: Drastično manje database load-a. Brže stranice.

Production vs. Local razlike

Aspekt Local Production
HTTPS mkcert self-signed Let's Encrypt (automatski)
Email Mailpit SMTP2Go
Cron wp-cron sidecar systemd timer
Baza MariaDB kontejner MariaDB na volume-u
Keš Redis (osnovni) Redis (optimizovan)

Production ima dodatne optimizacije:

  • OPcache enabled
  • 8GB MariaDB buffer pool
  • Redis persistence
  • fail2ban, UFW

WP-Cron: Zašto systemd

WordPress pseudo-cron: scheduled tasks se izvršavaju kada neko poseti sajt.

Problem: Nema poseta = nema cron-a. Scheduled posts se ne objave.

Rešenje 1 (local): Sidecar container koji poziva wp-cron.php.

Rešenje 2 (production): systemd timer.

# /etc/systemd/system/wp-cron.timer
[Timer]
OnCalendar=*:*:00
Persistent=true

[Install]
WantedBy=timers.target

Svaki minut, systemd poziva wp-cron.php. Scheduled posts se objave na vreme.

Database sync: Production → Local

Razvoj sa pravim podacima je bolji od dummy podataka.

pnpm dev:sync:prod:db

Šta radi:

  1. Export iz production (via Docker exec)
  2. Download via SCP
  3. Import u local MariaDB
  4. Search-replace (studenti.rs → studenti.local)
  5. Fix serialized data (Serbian characters)
  6. Clear caches

Serbian karakteri (š, ž, ć) mogu da pokvare PHP serialization. Script to popravlja.

Scripts: 20+ za infrastrukturu

infrastructure/scripts/
├── dev-start.sh           # Start local Docker
├── dev-stop.sh            # Stop Docker
├── dev-logs.sh            # View logs
├── db-import-local.sh     # Import SQL to local
├── sync-prod-to-local.sh  # Full production sync
├── staging-bootstrap.sh   # Setup new staging VPS
├── deploy-staging.sh      # Deploy to staging
├── staging-sync-db.sh     # Sync DB to staging
├── production-sync-db.sh  # Sync DB to production
└── ...

Svaka operacija je skriptovana. Ponovljiva. Dokumentovana.

Gap koji ostaje

Docker rešava mnogo toga. Ali neki WordPress DX problemi ostaju:

Nema hot reload za PHP. Morate refresh. Livereload pomaže za CSS, ali PHP je uvek refresh.

Debug je ručan. Xdebug + IDE konfiguracija. Funkcioniše, ali nije "out of the box."

Plugin conflicts. WordPress ekosistem problem, ne Docker problem.

Database heavy. Čak i sa Redis, WordPress voli upite.

Docker premošćuje gap. Ne eliminiše ga.

WordPress DX gap je realan. Dolazak iz modernog steka je bolan.

Ali Docker ga može premostiti:

  • Jedan command za start
  • HTTPS lokalno (Caddy + mkcert)
  • Theme/plugin hot reload (volume mounts)
  • Email catching (Mailpit)
  • Production parity

Setup zahteva vreme na početku. Ali posle toga, pnpm dev:docker:start i radite.