One-Pyeong Server — Running WordPress with Docker

Written by

in

Self-hosting deployment part 2. Infrastructure skeleton in 30 minutes.

← Previous: Part 1 — Why I Started a Blog on My Own Computer
→ Next: Part 3 — AI Should Read (robots.txt·llms.txt)


TL;DR

  • Launch WordPress + MariaDB + Redis three containers with a single docker-compose.yml file.
  • Only expose host port 127.0.0.1:8090. External routing handled by Cloudflare Tunnel (part 9).
  • Use Redis as an Object Cache for database query caching. Free and improves page response time by 30%.
  • Five traps: Korean file name SCP, healthcheck --connect, persisted password in named volume, WP_REDIS_HOST=redis, WORDPRESS_CONFIG_EXTRA X-Forwarded-Proto.

1. Why Docker — Host Installation vs Container

Comparison Direct Host (apt install) Docker Compose ⭐
Installation Simplicity apt install apache2 php mariadb-server, etc. docker compose up -d
Dependency Conflicts Conflicts like PHP 8.1 vs 8.3 arise Container Isolation
Mini-PC OS Reinstallation Start from scratch Just compose.yml + data backup
Updates System package manager (complexity) docker compose pull && up -d
Coexistence of Different Containers (WP, Umami, cloudflared) Difficult Natural

If the mini-PC is already running other services with Docker, the answer is Docker.


2. Structure on a Page

WordPress Docker Compose structure A visitor’s HTTPS request is terminated at Cloudflare Tunnel and forwarded as HTTP to 127.0.0.1:8090 on the mini-PC host, which is mapped to port 80 of the wordpress container. Inside the same docker network, the wordpress container reaches the mariadb container by the hostname db and the redis container by the hostname redis on port 6379. Each of the three containers has its own named volume. db and redis open no host ports. Outside (internet) Visitor browser (HTTPS) Cloudflare Tunnel (part 9) terminates HTTPS, forwards HTTP Mini-PC host only 127.0.0.1:8090 exposed 8090 → 80 Same docker network (talk by hostname) wordpress wordpress:php8.3-apache volume wordpress_data db mariadb:11 volume wordpress_db_data redis redis:7-alpine volume wordpress_redis_data WORDPRESS_DB_HOST: db waits: service_healthy WP_REDIS_HOST=redis 6379 (Object Cache) db and redis open no host ports
Figure 1. The three-container setup. Outside traffic comes in through Cloudflare Tunnel to a single door, 127.0.0.1:8090 on the host; db and redis are reachable only by hostname inside the same docker network.

The three containers communicate within the same Docker network using hostnames (db, redis). Only the host 127.0.0.1:8090 is exposed externally.


3. Key docker-compose.yml

services:
  wordpress:
    image: wordpress:php8.3-apache
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wp
      WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
      WORDPRESS_DB_NAME: wp
      WORDPRESS_CONFIG_EXTRA: |
        if (!empty($$_SERVER['HTTP_X_FORWARDED_PROTO']) && $$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
          $$_SERVER['HTTPS'] = 'on';
        }
    volumes:
      - wordpress_data:/var/www/html
    ports:
      - "127.0.0.1:8090:80"

  db:
    image: mariadb:11
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: wp
      MARIADB_USER: wp
      MARIADB_PASSWORD: ${DB_PASSWORD}
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - wordpress_db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect"]
      interval: 10s
      timeout: 5s
      retries: 6
      start_period: 30s

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - wordpress_redis_data:/data

volumes:
  wordpress_data:
  wordpress_db_data:
  wordpress_redis_data:

Separate .env:

DB_PASSWORD=<openssl rand -hex 24>
DB_ROOT_PASSWORD=<openssl rand -hex 24>

chmod 600 .env. Do not include in git.


4. Start + Verification

cd /home/user/wordpress
docker compose up -d
docker compose ps
curl -I http://localhost:8090

If the response is HTTP/1.1 302 (redirecting to WordPress installation page), it is successful.

Access http://localhost:8090 in a browser → 5-minute wizard → create admin account (do not use admin as the name, choose your own to avoid initial brute force).


5. Activate Redis Object Cache

After installing the redis-cache plugin:

wp config set WP_REDIS_HOST redis --allow-root
wp config set WP_REDIS_PORT 6379 --allow-root
wp config set WP_REDIS_PREFIX byeolgorae: --allow-root
wp config set WP_CACHE true --raw --allow-root
wp redis enable --allow-root

WP_REDIS_HOST=redis is key. It is the Docker hostname. Using 127.0.0.1 results in failure as it only looks within its own container.

WP_REDIS_HOST set to 127.0.0.1 versus redis Left: with WP_REDIS_HOST set to 127.0.0.1, the wordpress container looks for port 6379 on itself, where there is no Redis, so the connection is refused and the redis container is never reached. Right: with WP_REDIS_HOST set to redis, the docker network resolves the hostname redis to the redis container, the connection on port 6379 succeeds, and the Object Cache works. WP_REDIS_HOST=127.0.0.1 fails WP_REDIS_HOST=redis works wordpress container WordPress (PHP) 127.0.0.1:6379 no Redis in here 127.0.0.1 = this container itself it never points to another container redis container never reached Connection refused 127.0.0.1:6379 wordpress container WordPress (PHP) the docker network resolves hostname redis redis container :6379 connected, Object Cache works
Figure 2. Inside a container, 127.0.0.1 means that container itself. Redis lives in a different container, so it has to be called by the hostname redis (Trap 4).

6. Traps — Real Experiences Faced

Trap 1. Korean and Box Characters in File SCP

While writing docker-compose.yml with minipc_write_file, the SSH escape broke due to a combination of Korean comments and special characters (bash: -c: unexpected end of file while looking for matching quotation mark).

→ Write plain text in c: mp on Windows → scp → mini-PC /tmp/ → circumvent by sudo cp.

Trap 2. mariadb healthcheck unsupported --innodb-initialized

Following the web guide with ["CMD", "healthcheck.sh", "--connect", "--innodb-initialized"] led to an unknown option error.

→ Use only --connect + extend start_period: 30s.

Trap 3. “Database Error” after changing .env password

After changing the DB password and running docker compose up -d, a 500 error appeared. Reason: The old password was retained in the MariaDB DB file stored in the named volume. The new password failed authentication.

→ Run docker compose down -v (the -v removes volumes) → restart with up -d. Do not do this if operational data exists; only at initial setup stage.

Trap 4. Redis Connection refused 127.0.0.1:6379

After setting WP_REDIS_HOST=127.0.0.1, it resulted in the container trying to access itself. Redis is in another container.

→ Set WP_REDIS_HOST=redis (Docker network hostname).

Trap 5. HTTPS Reverse Proxy Infinite Redirect

With Cloudflare Tunnel in front, it forwards HTTP to the mini-PC after terminating HTTPS. WordPress identifies itself as HTTP → redirects saying it must go to “HTTPS” → infinite loop.

→ Handle processing for X-Forwarded-Proto in WORDPRESS_CONFIG_EXTRA (see above yml).


FAQ

Q. How much memory is used?
WP (php-fpm or apache) ~200MB, MariaDB ~150MB, Redis ~30MB. Total ~400MB. Plenty of room with 8GB RAM on the mini-PC.

Q. What about disk space?
Image combined ~1GB. DB size varies (100 articles + images = ~500MB).

Q. Can I use PHP 8.2 instead of 8.3?
WordPress 6.x supports both 8.2 and 8.3. 8.3 is faster (JIT stabilization). It is recommended to use the latest.

Q. What if I use mysql instead of mariadb?
mariadb is compatible with mysql + free licensing + no Oracle dependency. Both will work. mariadb is recommended for single-user operations.

Q. How to backup?
Details covered in part 4 (backup, cache, images). Daily cron at 03:00 runs mariadb-dump → gzip → stored in Obsidian Vault.


Next Part Preview

Part 3 — For AI to Read: robots.txt and llms.txt. Specify allowances for 14 types of AI crawlers instead of discrimination against bots. Content exposure strategy for the GEO era.


Summary in One Line

Launching three containers (WordPress, MariaDB, Redis) through a single docker-compose.yml file in 30 minutes. Only exposing host 127.0.0.1:8090; external routing delegated to Cloudflare Tunnel. Traps include Korean SCP, healthcheck issues, password persistence in volumes, Redis host misconfiguration, and X-Forwarded-Proto problems.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *