[Docker] WordPress with PHP‑FPM + Nginx, Redis (phpredis), and phpMyAdmin on Synology Docker
![[Docker] WordPress with PHP‑FPM + Nginx, Redis (phpredis), and phpMyAdmin on Synology Docker](/content/images/size/w1200/2025/09/wordpresswithall.png)
After hitting limits with a basic WordPress install (hard‑to‑tune PHP, fussy image operations, plugin/theme issues), I rebuilt the stack properly on Synology:
- WordPress (PHP‑FPM) behind Nginx
- Redis object cache via phpredis (built into the PHP image)
- MariaDB
- phpMyAdmin (DB only)
- Managed through Synology DSM → Container Manager → Projects
(CLI use is optional and shown at the end)
What you’ll do
- Prepare reverse proxy & DNS in DSM
- Create the folder layout on your NAS
- Drop in the config files (Dockerfile, compose, PHP/Nginx configs, env)
- Create/Start a Project in DSM (no terminal required)
- Finish WordPress setup and enable Redis object cache
1) Reverse proxy & DNS (DSM)
- Point DNS:
wordpress.yourdomain.com
→ your public IPphpmyadmin.yourdomain.com
→ your public IP
- In Control Panel → Login Portal → Reverse Proxy:
- Source:
https://wordpress.yourdomain.com
→ Destination:http://<NAS-LAN-IP>:10103
- Source:
https://phpmyadmin.yourdomain.com
→ Destination:http://<NAS-LAN-IP>:8097
- Source:
- Ensure your certificate covers both hostnames.
If your browser keeps jumping to DSM, clear HSTS/host cache and verify reverse proxy targets.
2) Folder layout (create this in File Station or SSH)
📌 EDIT THESE ROOT PATHS TO MATCH YOUR NAS.
Below I use/volume1/docker/wordpress-wody/
as an example. Choose your own base folder and keep it consistent.
/volume1/docker/wordpress-wody/ ← EDIT to your path
├─ build/ # custom WP (phpredis) image
│ └─ Dockerfile
├─ config/
│ ├─ nginx/
│ │ └─ nginx.conf
│ └─ php/
│ ├─ php-custom.ini
│ └─ www.conf
├─ mariadb/ # MariaDB data
├─ redis-data/ # Redis append-only file data
├─ wordpress/ # WordPress code (bind mount)
├─ docker-compose.yml
└─ stack.env
3) Config files
build/Dockerfile
(WordPress PHP‑FPM with phpredis)
# WordPress with PHP 8.3-FPM + phpredis extension
FROM wordpress:php8.3-fpm
# ── Install tools & phpredis, then clean up ──────────────
RUN set -ex \
&& apt-get update \
&& apt-get install -y --no-install-recommends git unzip ca-certificates \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& apt-get purge -y --auto-remove git unzip \
&& rm -rf /var/lib/apt/lists/*
# ── (Optional) additional PHP extensions ─────────────────
RUN set -ex \
&& apt-get update \
&& apt-get install -y --no-install-recommends libcurl4-openssl-dev \
&& docker-php-ext-install curl \
&& rm -rf /var/lib/apt/lists/*
.env
(place beside docker-compose.yml)
# ── Global ───────────────────────────────────────────────
TZ=Asia/Seoul
# ── MariaDB ──────────────────────────────────────────────
MYSQL_ROOT_PASSWORD=STRONG_ROOT_PASSWORD # ⚠️ CHANGE
MYSQL_DATABASE=wordpress # ⚠️ CHANGE if you like
MYSQL_USER=wpuser # ⚠️ CHANGE (non-root)
MYSQL_PASSWORD=STRONG_DB_PASSWORD # ⚠️ CHANGE
# ── WordPress ────────────────────────────────────────────
WORDPRESS_DB_HOST=db:3306
WORDPRESS_DB_USER=wpuser # must match MYSQL_USER
WORDPRESS_DB_PASSWORD=STRONG_DB_PASSWORD # must match MYSQL_PASSWORD
WORDPRESS_DB_NAME=wordpress # must match MYSQL_DATABASE
# ── phpMyAdmin ───────────────────────────────────────────
PMA_HOST=db
PMA_PORT=3306
UPLOAD_LIMIT=1024M
# ── Redis ────────────────────────────────────────────────
# Using defaults; see docker-compose notes for securing Redis in production
docker-compose.yml
📌 IMPORTANT: Update every path undervolumes:
to your actual folder.
I left loud comments# <<< EDIT MOUNT PATH
where you need to change them.
# ─────────────────────────────────────────────────────────────
# WordPress (PHP-FPM) + Nginx + MariaDB + phpMyAdmin + Redis
# Synology-friendly paths — edit the host paths (left side) only.
# ─────────────────────────────────────────────────────────────
services:
# ---------- DATA TIER ----------
db:
image: mariadb:11.4
container_name: wordpress-mariadb
command: >-
--transaction-isolation=READ-COMMITTED --binlog_format=ROW
--innodb_file_per_table=1 --character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
env_file: ./.env
volumes:
- /volume1/docker/wordpress-wody/mariadb:/var/lib/mysql # <<< EDIT: DB data (persistent)
restart: unless-stopped
networks: [wpnet]
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 10
# ---------- APP TIER ----------
wordpress:
# Uses your custom Dockerfile (e.g., phpredis enabled)
build: ./build # expects ./build/Dockerfile
image: wp-fpm-with-redis:latest
container_name: wordpress-app
env_file: ./.env
depends_on:
db:
condition: service_healthy
volumes:
- /volume1/docker/wordpress-wody/wordpress:/var/www/html # <<< EDIT: WP files (persistent)
- /volume1/docker/wordpress-wody/config/php/php-custom.ini:/usr/local/etc/php/conf.d/z-custom.ini:ro # <<< EDIT
- /volume1/docker/wordpress-wody/config/php/www.conf:/usr/local/etc/php-fpm.d/zzz-www.conf:ro # <<< EDIT
restart: unless-stopped
networks: [wpnet]
nginx:
image: nginx:alpine
container_name: wordpress-nginx
depends_on: [wordpress]
ports:
- "10103:80" # Expose HTTP (Reverse Proxy can point here)
volumes:
- /volume1/docker/wordpress-wody/wordpress:/var/www/html:ro # <<< EDIT: serve same WP files (read-only)
- /volume1/docker/wordpress-wody/config/nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro # <<< EDIT
restart: unless-stopped
networks: [wpnet]
phpmyadmin:
image: phpmyadmin:latest
container_name: wordpress-pma
env_file: ./.env
depends_on: [db]
ports:
- "8097:80" # Optional admin UI (protect via RP/auth)
restart: unless-stopped
networks: [wpnet]
redis:
image: redis:7-alpine
container_name: wordpress-redis
# ⚠️ PRODUCTION NOTE:
# Prefer not exposing Redis; keep it internal on wpnet.
# Use a password:
# command: ["redis-server","--appendonly","yes","--requirepass","STRONG_REDIS_PASSWORD"]
# and set WP_REDIS_PASSWORD in wp-config.php accordingly.
command: ["redis-server","--appendonly","yes","--bind","0.0.0.0","--protected-mode","no"] # Dev-friendly; not recommended for prod
volumes:
- /volume1/docker/wordpress-wody/redis-data:/data # <<< EDIT: Redis AOF data
restart: unless-stopped
networks: [wpnet]
networks:
wpnet:
driver: bridge
📝 Mount path reminder (again):
Replace every/volume1/docker/wordpress-wody/...
with your real folder locations on Synology.
config/nginx/nginx.conf
server {
listen 80;
server_name _; # TLS terminates at Synology RP; this is internal HTTP
root /var/www/html;
index index.php index.html;
# Allow large uploads & long edits (WordPress media, imports, etc.)
client_max_body_size 1000m;
client_body_timeout 1200s;
send_timeout 1200s;
# ── Front controller ───────────────────────────────────
location / {
try_files $uri $uri/ /index.php?$args;
}
# ── PHP via PHP-FPM in the "wordpress" service ────────
location ~ \.php$ {
try_files $uri =404;
include /etc/nginx/fastcgi_params;
fastcgi_pass wordpress:9000;
fastcgi_index index.php;
# Correct script resolution for WordPress
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
# Generous timeouts for slow plugins/exports
fastcgi_read_timeout 1200s;
fastcgi_send_timeout 1200s;
# Buffer tuning reduces "upstream sent too big header" issues
fastcgi_buffers 32 64k;
fastcgi_buffer_size 128k;
# If Synology RP sends X-Forwarded-Proto=https, forward it to PHP
fastcgi_param HTTPS $http_x_forwarded_proto;
}
# ── Static assets: cache for 30 days ──────────────────
location ~* \.(?:css|js|jpe?g|gif|png|svg|webp|ico|woff2?)$ {
try_files $uri =404;
expires 30d;
access_log off;
}
}
config/php/php-custom.ini
; ============================================================
; Custom PHP settings for WordPress on Synology (php-fpm)
; ============================================================
; === Resources ===
memory_limit = 512M
max_execution_time = 1200 ; long-running imports/exports
max_input_time = 600
max_input_vars = 4000
; === Uploads / POST limits (1GB) ===
upload_max_filesize = 1000M
post_max_size = 1000M
max_file_uploads = 50
; === OPcache ===
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 100000
; For development (hot code changes):
; opcache.validate_timestamps = 1
; opcache.revalidate_freq = 60
; For production (better performance):
opcache.validate_timestamps = 0
opcache.revalidate_freq = 0
; === Security / Stability ===
expose_php = 0
default_socket_timeout = 1200
realpath_cache_size = 256k
realpath_cache_ttl = 600
output_buffering = 4096
zlib.output_compression = 0
config/php/www.conf
[www]
user = www-data
group = www-data
; === Process manager (PHP-FPM pool) ===
pm = dynamic
pm.max_children = 50
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
; === Listener (Nginx connects via fastcgi_pass wordpress:9000) ===
listen = 0.0.0.0:9000
; Permissions for socket/TCP listener
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
; === Logs ===
access.log = /proc/self/fd/2
slowlog = /proc/self/fd/2
catch_workers_output = yes
; === Environment ===
; Let Docker provide HOSTNAME; avoid hardcoding here.
; clear_env=no ensures Docker env variables are visible to PHP.
clear_env = no
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp
; === Pool-level PHP overrides ===
php_admin_value[max_execution_time] = 1200
4) Create & run the Project in DSM (no terminal)
- Open Container Manager → Projects → Create.
- Project name: e.g.,
wordpress-stack
. - Path: point to the folder that contains your
docker-compose.yml
. - DSM automatically loads the compose file.
- Click Next, then Create (DSM will build
wordpress
from./build/
and start all services). - Confirm all containers show Running.
If a build fails, double‑check your mount paths in volumes:
and that every file exists.
5) First‑run checks
phpMyAdmin
- Visit
https://phpmyadmin.yourdomain.com
- Login with
MYSQL_USER
/MYSQL_PASSWORD
(fromstack.env
) - Confirm the
WORDPRESS_DB_NAME
exists and is accessible.
WordPress
- Visit
https://wordpress.yourdomain.com
- Complete the installer (site title + first admin user).
6) Turn on Redis object cache
Add to wp-config.php
(above “Happy publishing” line)
[www]
user = www-data
group = www-data
; === Process manager (PHP-FPM pool) ===
pm = dynamic
pm.max_children = 50
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
; === Listener (Nginx connects via fastcgi_pass wordpress:9000) ===
listen = 0.0.0.0:9000
; Permissions for socket/TCP listener
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
; === Logs ===
access.log = /proc/self/fd/2
slowlog = /proc/self/fd/2
catch_workers_output = yes
; === Environment ===
; Let Docker provide HOSTNAME; avoid hardcoding here.
; clear_env=no ensures Docker env variables are visible to PHP.
clear_env = no
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp
; === Pool-level PHP overrides ===
php_admin_value[max_execution_time] = 1200
Install & enable the plugin
- WP Admin → Plugins → Add New
- Search “Redis Object Cache” (by Till Krüss)
- Install → Activate
- Go to Settings/Tools → Redis → click Enable Object Cache
- Status should show Connected (client
phpredis
, hostredis:6379
)
If you see “Redis is unreachable”:
- Containers
wordpress
andredis
must be on the same network (wpnet
) - Service name must be
redis
(matchesWP_REDIS_HOST
) - If you required a password, add
WP_REDIS_PASSWORD
inwp-config.php
Why this setup is better
- Full PHP control via
php-custom.ini
+ FPM pool - Fewer timeouts on big image edits (increased PHP/FastCGI/NGINX limits)
- Faster page loads with OPcache + Redis object caching
- Separation of concerns: Nginx for static files, PHP‑FPM for PHP
Hardening & maintenance
- Use strong, unique passwords in
stack.env
(don’t commit them). - Prefer Redis auth in production and avoid publishing Redis outside Docker network.
- Back up
mariadb/
andwordpress/
regularly with Synology tasks. - If you deploy code often, switch OPcache to:
opcache.validate_timestamps = 1 opcache.revalidate_freq = 60
Troubleshooting quick hits
- DSM instead of site: clear HSTS/host cache, recheck reverse proxy target ports.
- White screen on large imports: raise
memory_limit
,post_max_size
,upload_max_filesize
, and tunepm.*
. - Redis drop‑in missing: after activating the plugin, click Enable Object Cache (creates
wp-content/object-cache.php
).
(Optional) Do it by console
Only if you prefer CLI.
# run in the folder with docker-compose.yml
docker compose build
docker compose up -d
docker compose ps
DSM and CLI can be mixed, but for simplicity stick to DSM Projects as the primary workflow. (It will not be shown in Project section in Container Manager)
You can now run WordPress on Synology the “right way” while keeping the workflow DSM‑centric.