[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

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

  1. Prepare reverse proxy & DNS in DSM
  2. Create the folder layout on your NAS
  3. Drop in the config files (Dockerfile, compose, PHP/Nginx configs, env)
  4. Create/Start a Project in DSM (no terminal required)
  5. Finish WordPress setup and enable Redis object cache

1) Reverse proxy & DNS (DSM)

  • Point DNS:
    • wordpress.yourdomain.com → your public IP
    • phpmyadmin.yourdomain.com → your public IP
  • In Control Panel → Login Portal → Reverse Proxy:
    • Source: https://wordpress.yourdomain.comDestination: http://<NAS-LAN-IP>:10103
    • Source: https://phpmyadmin.yourdomain.comDestination: http://<NAS-LAN-IP>:8097
  • 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 under volumes: 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)

  1. Open Container Manager → Projects → Create.
  2. Project name: e.g., wordpress-stack.
  3. Path: point to the folder that contains your docker-compose.yml.
  4. DSM automatically loads the compose file.
  5. Click Next, then Create (DSM will build wordpress from ./build/ and start all services).
  6. 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 (from stack.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

  1. WP Admin → Plugins → Add New
  2. Search “Redis Object Cache” (by Till Krüss)
  3. InstallActivate
  4. Go to Settings/Tools → Redis → click Enable Object Cache
  5. Status should show Connected (client phpredis, host redis:6379)

If you see “Redis is unreachable”:

  • Containers wordpress and redis must be on the same network (wpnet)
  • Service name must be redis (matches WP_REDIS_HOST)
  • If you required a password, add WP_REDIS_PASSWORD in wp-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/ and wordpress/ 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 tune pm.*.
  • 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.