wp-splendid
Self-hosted WordPress stack defined entirely as a Compose deployment (Docker or Podman).
No application code lives in this repository — only the infrastructure configuration.
Internet → Nginx :8080 → PHP-FPM (wordpress:9000)
↓ ↓
MySQL Redis
| Service | Image | Role |
|---|---|---|
nginx |
nginx:alpine |
Reverse proxy, static cache, security headers, rate-limit |
wordpress |
wordpress:php8.5-fpm |
PHP-FPM only, never exposed to the host |
db |
mysql:lts |
Database, persisted under ./db/ |
redis |
redis:8-alpine |
WordPress object cache (W3TC / Redis Object Cache) |
All services share the internal bridge network wp-network. Redis and MySQL are not published on the host.
- Podman + podman-compose
- or Docker + Docker Compose
On Fedora / RHEL with SELinux enforcing, the volume mounts in docker-compose.yml use :Z / :z labels — no extra configuration is required.
# 1. Clone and enter the directory
git clone <url> wp-splendid && cd wp-splendid
# 2. Create the secrets file
cp .env.example .env
# Generate WordPress salts:
curl -s https://api.wordpress.org/secret-key/1.1/salt/
# Generate strong passwords:
openssl rand -base64 32
# 3. Start the stack
podman compose up -d # or: docker compose up -d
# 4. Open the site
xdg-open http://localhost:8080# Tail logs
podman compose logs -f nginx wordpress db redis
# Validate Compose syntax
podman compose config --quiet
# Shell into the WordPress container (for wp-cli, etc.)
podman compose exec wordpress bash
# MySQL client
podman compose exec db mysql -u${DB_USER} -p ${DB_NAME}
# Stop without deleting data
podman compose down
# Wipe all runtime data (DB, Redis snapshot, WordPress files)
scripts/cleanup.sh
scripts/cleanup.sh -y # skip confirmation.
├── docker-compose.yml # Service definitions, hardening, healthchecks
├── .env.example # Environment variable template
├── assets/
│ └── logo.svg # Project logo
├── scripts/
│ └── cleanup.sh # Stop the stack and wipe runtime data
├── nginx/
│ ├── default.conf # Virtual host (FastCGI, W3TC cache, rate-limit)
│ └── header.conf # Security headers (CSP, HSTS, frame, …)
├── db/
│ └── my.cnf # InnoDB / MySQL tuning
├── redis-data/ # Persisted Redis data (gitignored)
└── wordpress/ # WordPress core + plugins + themes (gitignored)
| Variable | Description |
|---|---|
DB_NAME |
MySQL database name |
DB_USER |
MySQL user |
DB_PASSWORD |
MySQL password (plaintext, hashed by MySQL) |
REDIS_PASSWORD |
Redis requirepass value |
WP_*_KEY / WP_*_SALT |
8 WordPress salts (see .env.example) |
Never commit .env — it is excluded via .gitignore.
Nginx looks for pre-rendered HTML files under wp-content/cache/page_enhanced/ before passing the request to PHP. The cache is bypassed for:
- POST requests
- non-empty query strings
- WordPress / WooCommerce / comment-author cookies
If you swap caching plugins, update the try_files chain in nginx/default.conf.
nginx/header.conf is mounted at /etc/nginx/conf.d/headers.conf and re-included in every location block. It provides:
X-Content-Type-Options,X-Frame-Options,X-XSS-Protection,X-Download-Options,X-Permitted-Cross-Domain-PoliciesContent-Security-Policy(tighten per your third-party domains)- HSTS — commented; enable only after HTTPS is in place
- CORS — disabled by default; enable per origin if needed
Not configured in this repository (HTTP only on port 8080). To enable HTTPS:
- Mount your certificates into the
nginxcontainer. - Add a
listen 443 ssl http2block innginx/default.conf. - Uncomment the
Strict-Transport-Securityheader innginx/header.conf. - Review the CSP if you add third-party HTTPS resources.
Container layer:
security_opt: no-new-privileges:trueon every servicecap_drop: [ALL]onnginxandredis, with a minimalcap_addsetmem_limitset per service (nginx 128m, wordpress 512m, db 1g, redis 256m)loggingdriver capped at 10 MB × 3 files to prevent log-disk fill- Healthchecks on
dbandredis;wordpresswaits on both viaservice_healthy
Application layer:
- PHP-FPM never exposed to the host — only reachable by Nginx on the internal network
- Redis and MySQL never exposed to the host
wp-login.phprate-limited to 5 req/min per IP (burst 3)- Author enumeration blocked (
?author=N→ 403) xmlrpc.phpandwp-config.phpdenied at the Nginx layerserver_tokens off— Nginx version hiddenMYSQL_RANDOM_ROOT_PASSWORD=1— root password is randomized and unknown- WordPress hardening constants:
DISALLOW_FILE_EDIT,WP_AUTO_UPDATE_CORE='minor',WP_DEBUG=false,WP_DEBUG_DISPLAY=false
This project is released under the WTFPL — see LICENSE.
Do What The Fuck You Want To Public License.