DB-유

Running a WordPress Container on Synology

· synology, docker, wordpress

Synology's one-click WordPress package is fine for a single personal blog. I moved to Docker when I wanted a specific PHP version and a site I could lift onto another machine without a migration plugin. The setup is the official wordpress image plus a mariadb container, data under /volume1/docker, one compose file. This is on DSM 6.1 with the Docker package.

You need SSH enabled, the docker shared folder, and a free port for WordPress (I use 8081, since DSM's nginx owns 80 and 443). A hostname and reverse proxy rule for https://blog.example.com are optional.

Folders first

sudo mkdir -p /volume1/docker/wordpress/html
sudo mkdir -p /volume1/docker/wordpress/db
sudo chown -R 33:33 /volume1/docker/wordpress/html
sudo chown -R 999:999 /volume1/docker/wordpress/db

UID 33 is www-data inside the Debian-based wordpress image; 999 is mysql inside mariadb.

The compose file

Save this as /volume1/docker/wordpress/docker-compose.yml:

version: '2'
services:
  db:
    image: mariadb:10.2
    container_name: wp-db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: root-secret
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wp-secret
    volumes:
      - /volume1/docker/wordpress/db:/var/lib/mysql

  wordpress:
    image: wordpress:4.8-apache
    container_name: wp-web
    restart: unless-stopped
    depends_on:
      - db
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wp-secret
    volumes:
      - /volume1/docker/wordpress/html:/var/www/html
    ports:
      - "8081:80"

The database is not published on any port; the two containers talk over the private network compose creates, and db resolves by service name.

Start it

cd /volume1/docker/wordpress
sudo docker-compose up -d
sudo docker-compose logs -f wordpress

On the first start the image copies WordPress into the empty html folder, which takes a minute. When the log shows apache2 -D FOREGROUND, open http://nas-ip:8081 and run the five-minute install. Both containers appear in the Docker GUI under Container, with the auto-restart flag already set from restart: unless-stopped.

Upload limit

The default PHP limit inside the image is 2 MB, which is useless for photos. It was the first thing I tripped over. Create a drop-in file in the bind-mounted folder; the image loads .htaccess from the document root, so this works without rebuilding:

# /volume1/docker/wordpress/html/.htaccess (append to the WordPress block)
php_value upload_max_filesize 64M
php_value post_max_size 64M
php_value memory_limit 256M

Alternatively add a php.ini snippet by mounting a file to /usr/local/etc/php/conf.d/uploads.ini in the compose file.

Behind the reverse proxy

Control Panel → Application Portal → Reverse Proxy → Create: Source HTTPS blog.example.com:443, Destination HTTP localhost:8081. Then, because WordPress builds absolute URLs, tell it the public address. Add to wp-config.php above the "That's all, stop editing" line:

define('WP_HOME', 'https://blog.example.com');
define('WP_SITEURL', 'https://blog.example.com');
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
    $_SERVER['HTTPS'] = 'on';
}

Without the HTTPS line WordPress sees plain HTTP from the proxy and falls into a redirect loop.

Dump the database nightly

Hyper Backup will pick up /volume1/docker/wordpress, but raw InnoDB files are only consistent if the container is stopped, and I don't trust a backup I can't restore from a plain SQL file. So I add a Task Scheduler entry (Control Panel → Task Scheduler → Create → Scheduled Task → User-defined script, user root, daily at 02:00):

mkdir -p /volume1/docker/wordpress/dump
docker exec wp-db sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction wordpress' \
  | gzip > /volume1/docker/wordpress/dump/wordpress-$(date +%F).sql.gz
find /volume1/docker/wordpress/dump -name '*.sql.gz' -mtime +14 -delete

Did it work?

sudo docker-compose ps
curl -sI http://localhost:8081/ | head -n 1
curl -sI https://blog.example.com/wp-login.php | grep -i location

docker-compose ps should show both containers as Up. The local curl returns HTTP/1.1 302 Found (redirecting to the public URL) or 200 OK, and the HTTPS request must not redirect back to HTTP. Log in to /wp-admin, upload a 10 MB image, and confirm it lands under html/wp-content/uploads.

Things that bit me

  • WordPress shows "Error establishing a database connection" right after up -d: MariaDB is still initializing on first boot; depends_on only waits for the container to start, not for the server to accept connections. Wait thirty seconds and reload.
  • The install page appears at http://nas-ip:8081 but images load from http://nas-ip:8081 after you moved it behind the proxy: WP_HOME/WP_SITEURL were not set; the values in the database still point at the old address. Setting the constants overrides them.
  • "The uploaded file exceeds the upload_max_filesize directive in php.ini": the .htaccess change was not picked up because AllowOverride was lost when you replaced the file wholesale. Check that the standard # BEGIN WordPress block is still present.
  • Docker GUI complains "Docker API has failed" when you open the container details: it dislikes containers with depends_on links created by compose. Use the CLI for those containers; they run fine.

Moving the blog to another NAS is now a copy of /volume1/docker/wordpress and a docker-compose up -d. That was the point of all this.