Category: Self-Hosting Build Guide

  • Naming is Difficult — Choosing a Site Name

    Naming is Difficult — Choosing a Site Name

    Naming is Difficult — Choosing a Site Name

    Self-hosting building record, Part 7. A site name is harder than a domain name.

    ← Previous: Part 6 — Domain Purchase: Cloudflare Registrar


    TL;DR

    • The initial name “jamjam’s voyage diary” → changed to “Star Whale” within three days
    • It is okay for the domain (sticknstone.org) and brand name (Star Whale) to differ. This can be explained on the About page.
    • Including the English subtitle “Star Whale” → friendly for global search
    • Five criteria for naming a personal blog: differentiation, metaphor depth, memorability, domain conflict, English pronunciation

    1. The Problems with “jamjam’s voyage diary”

    The name chosen initially. A combination of my nickname (jamjam) and “voyage diary.” While positioning the articles over three days, five issues became apparent.

    Issues Reasons
    Too Ordinary Conflicts with objective content and tone like “study, trading, automation”
    Nickname Exposure The nickname jamjam prioritizes personal identity over site identity
    Imitable Many blogs follow the pattern “OO’s voyage diary.” Weak search exposure
    Difficult English Conversion “jamjam’s voyage diary” — awkward
    Narrow Perspective “diary” implies a personal log. Too private for outside readers

    2. New Name Candidates

    Comparison of five candidates.

    Name Meaning Differentiation Metaphor Memorability English
    jamjam’s voyage diary My diary ★★ ★★★
    firewhale Fire + Whale (intensity + depth) ★★ ★★★ ★★★ ★★★★
    Star Whale Star + Whale (exploration, depth) ★★★★★ ★★★★★ ★★★★ ★★
    One-Pyeong Library One-person workspace ★★★ ★★★ ★★★
    Sailor’s Resource Room Collection of information ★★ ★★ ★★

    Star Whale dominates in differentiation and metaphor. The English name has weaknesses but can be supplemented by the subtitle “Star Whale.”


    3. The Depth of the Metaphor for Star Whale

    

    “Star Whale is a deep voyage toward the star” — captures the tone, content direction, and blog identity in one sentence. The core of my learning notes is about going “calmly far away,” fitting perfectly.


    4. Firewhale Disqualified — SEO Conflict

    “Fire + Whale” = Motivation from the intensity of fire + the depth of a whale. It seems appealing, but searching reveals:

    Owner What
    firewhale.io iOS app development company (numerous music apps)
    firewhale.org Notable Publications
    firewhalemusic.com Music artist
    Firewhale (FIREW) Cryptocurrency token
    GitHub FireWhale Personal account

    The search results show zero opportunities for Star Whale (ours) to enter the first page. A new site starting with firewhale would be forever buried.

    Star Whale, upon searching, is nearly clean. We rank first. Overwhelming in SEO and GEO exposure.


    5. The English Subtitle “Star Whale”

    Korean main title + English subtitle. WordPress settings:

    Site Title:    Star Whale
    Tagline:       Star Whale — Self-hosted notes on learning, trading, automation
    

    Advantages:
    – Korean Search: “Star Whale” ranks cleanly at number one
    – English Search: The “Star Whale” portion is English-exposed
    – Business cards and SNS self-introduction: “Star Whale (Star Whale)” sounds natural
    – No burden when expanding globally

    There is a ML company using Starwhale as one word. However, if we separate it into two words as Star Whale, there’s no association (only slight brand proximity).


    6. Domain and Site Name Mismatch — Is It Okay?

    Domain Site Name Example
    google.com Google Match
    meta.com Meta Match
    medium.com Medium Match
    stripe.com Stripe Match
    sticknstone.org Star Whale Mismatch

    Generally, it is preferable for them to match. In our case, we intentionally separated them.

    Why is the separation acceptable?:
    – It can be explained naturally in one line on the About page
    – “Stick & Stone = the stones and sticks a former sailor used to point to stars” → “Star Whale = A deep voyage toward that star” → a storytelling asset
    – The aim is not to have the URL memorized but to have visitors arrive through search, making the domain-brand alignment secondary.

    For corporate sites, matching makes sense. A personal learning note blog is different.


    7. The Change Task — One Line with wp-cli

    wp option update blogname "Star Whale" --allow-root
    wp option update blogdescription "Star Whale — Self-hosted notes on learning, trading, automation" --allow-root
    

    That’s it. Additional updates needed:
    – One line in llms.txt (# Star Whale)
    – About page (storytelling)
    – OG tags (Rank Math automatic)
    – Email sender name (UpdraftPlus notifications, etc., follows automatically)


    FAQ

    Q. Can I not change the name once I’ve set it?
    You can change it. However, the earlier you change it, the cost is zero. Changing it after publishing 100 articles would require adjustments to external links, social, and search indexing.

    Q. Is the English subtitle necessary?
    Korean alone is feasible. However, the English subtitle = friendly for global searches + naturalness on business cards. The benefit far exceeds the 5-minute task.

    Q. Isn’t a compound Korean name like “Star Whale” difficult for foreigners?
    It is challenging. Hence the English subtitle. My main readers are Koreans, so priority is given to Korean.

    Q. Should I register a trademark?
    For a personal blog, it is unnecessary. The probability of an external user creating products/services under the name Star Whale is zero. This will be reevaluated at the point of accumulated content + monetization.

    Q. I’m delaying publication because I can’t decide on a site name?
    Start publishing under a provisional name. Content comes first. The name solidifies naturally after writing about five pieces of content.


    Next Episode Preview

    Part 8 — Bots arrive within 24 hours: Security P0 before domain exposure. Blocking xmlrpc + changing wp-login slug = 25 minutes of work will block 99% of brute force attempts.


    One-Line Summary

    Choosing a site name is harder than the domain. Star Whale overwhelmingly satisfies five criteria for differentiation, metaphor, and search exposure. The combination of a Korean main title + English subtitle (Star Whale) + storytelling on the About page complements the domain mismatch.
    “`

  • Bots Arrive Within 24 Hours — Security P0 Before Domain Disclosure

    Bots Arrive Within 24 Hours — Security P0 Before Domain Disclosure

    Bots Arrive Within 24 Hours — Security P0 Before Domain Disclosure

    Self-hosting guide, part 8. Essential 25 minutes to complete before launching the domain.


    TL;DR

    • Disclosing a WordPress domain = bots arrive within 24 hours for brute force attacks. A statistical fact.
    • A 25-minute task can block 99%: ① Avoid username admin ② Block xmlrpc ③ Change wp-login slug.
    • These three constitute security P0. Complete this before disclosing your domain.
    • Wordfence and 2FA are P1 (to be added after domain launch).

    1. The Truth of the First 24 Hours

    When a WordPress site is exposed to the internet, within the first 24 hours:

    Attack Frequency Target
    /wp-login.php brute force Thousands per hour Admin password
    /xmlrpc.php pingback amp Continuous DDoS amplification
    Scanning for known plugin vulnerabilities Continuous Outdated plugins
    SQL injection attempts Automatic bots ?id=1 OR 1=1 URL
    User enumeration (?author=1) Automatic bots Discovering usernames

    The goal here is not just to block but to eliminate the surface itself, which is P0.


    2. P0 — Three Security Setup

    

    3. ① Avoiding Username Admin

    When creating the first user with the WordPress installation wizard, do not enter admin. Bots primarily target this username for brute force attacks.

    Good examples: jamjam, sailor904, personal nicknames
    Bad examples: admin, administrator, root, wpadmin
    

    If you’ve already created an admin user, create a new user via wp-cli, then revoke admin rights or delete the old user:

    wp user create newname [email protected] --role=administrator --user_pass=$(openssl rand -base64 24) --allow-root
    wp user delete admin --reassign=newname --allow-root
    

    4. ② Blocking xmlrpc.php — mu-plugin

    /xmlrpc.php is an old API for WordPress, rarely used (only by Jetpack users). Bots exploit it for brute force amplification (attempting thousands of passwords with a single request).

    Create /var/www/html/wp-content/mu-plugins/disable-xmlrpc.php:

    <?php
    /**
     * Plugin Name: Disable XML-RPC
     * Description: Block xmlrpc.php to reduce attack surface.
     */
    if (!defined('ABSPATH')) exit;
    
    add_filter('xmlrpc_enabled', '__return_false');
    add_filter('wp_headers', function ($headers) {
        unset($headers['X-Pingback']);
        return $headers;
    });
    add_filter('xmlrpc_methods', function ($methods) {
        unset($methods['pingback.ping']);
        unset($methods['pingback.extensions.getPingbacks']);
        return $methods;
    });
    add_action('init', function () {
        $uri = $_SERVER['REQUEST_URI'] ?? '';
        if (stripos($uri, '/xmlrpc.php') !== false) {
            status_header(403);
            exit('xmlrpc disabled');
        }
    });
    

    Verification:

    curl -I http://localhost:8090/xmlrpc.php
    # HTTP/1.1 403  ← Success
    

    5. ③ Changing wp-login Slug

    /wp-login.php is the login URL common to all WordPress sites. Bots start automatically hitting this URL. It should be obscured with a different slug.

    Install the plugin wps-hide-login:

    wp plugin install wps-hide-login --activate --allow-root
    wp option update whl_page starport --allow-root
    

    starport is a secret slug chosen by I, difficult to guess yet easy to remember. For example: byeolgorae-gate, secret-door-2026, starport.

    Verification:

    curl -I -L http://localhost:8090/wp-login.php
    # 301 redirect → Fake URL (bot fails there)
    
    curl -I -L http://localhost:8090/wp-admin/
    # 404 (Admin page appears nonexistent when logged out)
    
    curl -I -L http://localhost:8090/starport/
    # 200 (Login page known only to I)
    

    If I forget the slug, I won’t be able to access it either. Bookmarking and note-taking are essential.


    6. Results — From the Bot’s Perspective

    Attack Response after Blocking
    GET /wp-login.php 301 → Fake URL → Bot attempts there → Forever fails
    POST /xmlrpc.php Blocked immediately with 403. Amplification not possible
    GET /wp-admin/ 404. The admin page pretends to be nonexistent
    Username brute force Attempting admin → Our site has no admin
    ?author=1 user enum Rank Math automatically blocks it (Disable Author Archives)

    Resources expended by bots = Gains from I’s site = 0.


    7. P0 vs P1 vs P2 — Prioritization

    Stage Task Timing
    P0 (Mandatory, before domain disclosure) Username, xmlrpc, wp-login slug This 25 minutes
    P1 (Immediately after domain launch) Wordfence wizard + 2FA GUI operations by I
    P2 (Monthly) Plugin security updates + WPA (vulnerability scan) Operational phase

    If I launch the domain without completing P0 — bots will arrive within 24 hours to start brute force attempts. Even without setup, it is impenetrable, yet logs will accumulate brute force attempts and consume mini-PC CPU. Preventing such occurrences is vital for mental well-being.


    8. Traps

    Trap 1. mu-plugin Auto Activation

    mu-plugins run forced above regular plugins. Consequently, security policies, like blocking xmlrpc, that must never be disabled, are suitable for mu-plugins.

    Trap 2. Forgetting WPS Hide Login Slug

    Since the whl_page option is directly written into the DB, if I forget the slug, I won’t be able to access it either. Recovery method: wp option get whl_page --allow-root (via mini-PC SSH) or temporarily deactivate the mu-plugin.

    Trap 3. Automatic Bypass After Authentication

    WPS Hide Login only blocks those who do not know the slug. A logged-in session continues to use /wp-admin/. Once I log in, I have free access to the admin page for the next 30 minutes.

    Trap 4. Entry with a New Slug Upon Logout

    After I log out, the next login will be at /starport. Updating bookmarks is essential.


    FAQ

    Q. What if I already disclosed the domain but haven’t set up P0?
    Immediately turn off cloudflared or disable the Tailscale Funnel → Set up P0 → Re-disclose. 5 minutes.

    Q. Why is the Wordfence wizard (P1) not P0?
    P0 removes the surface itself. Wordfence detects and blocks attacks while the surface is still present. It serves as a supplementary measure.

    Q. Can I change the slug to /admin?
    /admin is also a common slug targeted by bots. Not recommended. Choose a word that only I can think of, which is difficult to guess.

    Q. Is P0 sufficient without 2FA?
    Sufficient + entirely enough. 2FA acts as the final shield when passwords are compromised. P0 + 2FA = Effectively invulnerable.

    Q. If I block xmlrpc, can I still use Jetpack?
    Some Jetpack functionalities are disabled. It is irrelevant if Jetpack is not used at all. Our Star Whale does not utilize Jetpack.


    Next Episode Preview

    Part 9 — The Invisible Bridge: Cloudflare Tunnel. Finally connecting the domain to the mini-PC. No need to open router ports, without exposing the mini-PC IP, and employing a free WAF, DDoS protection, and CDN.


    Summary

    Disclosing a WordPress domain = Bots will arrive within 24 hours. A P0 setup (username, xmlrpc, wp-login slug) in 25 minutes can block 99%. Complete this before launching the domain.


  • AI Should Read — robots.txt and llms.txt

    AI Should Read — robots.txt and llms.txt

    AI Should Read — robots.txt and llms.txt

    Self-hosting construction log, part 3. The era of SEO is fading, and the era of GEO is beginning. Which side are we on?


    TL;DR

    • 31.3% of the U.S. population will use generative AI search by 2026. 83% of queries are satisfied without site visits (eMarketer).
    • Old method: Blocking AI bots in robots.txt. A defensive mindset about search.
    • New method: Explicitly allowing 14 types of AI bots and guiding them with llms.txt. Cited sites win.
    • Star Whale mu-plugin in one file + /llms.txt in one file = 5 minutes.

    1. From SEO to GEO — What Has Changed

    

    Key changes:
    Traffic ↓ — AI responses delivered instantly without going through search results. Site visits themselves decrease.
    Citations ↑ — When AI responds, the site name is displayed with “source: ~”.
    Trust = Citation Count — If Perplexity·ChatGPT frequently cites our articles, that becomes new authority.

    Blocking bots is suicidal. If not cited, existence becomes moot.


    2. Not Blocking but Allowing — 14 Types of AI Crawlers

    Dynamically generate robots.txt with mu-plugin:

    <?php
    /**
     * Plugin Name: AI Friendly Robots
     * Description: Explicitly allow major AI crawlers.
     */
    add_filter('robots_txt', function ($output, $public) {
        if ($public != '1') return $output;
    
        $output .= "n# === AI search engines / LLM crawlers (explicitly allowed) ===n";
        $bots = [
            'GPTBot',           // OpenAI Learning
            'OAI-SearchBot',    // OpenAI Search
            'ChatGPT-User',     // ChatGPT Real-time queries
            'ClaudeBot',        // Anthropic Learning
            'Claude-Web',       // Claude.ai Real-time
            'PerplexityBot',    // Perplexity
            'Perplexity-User',  // Perplexity Real-time
            'Google-Extended',  // Gemini Learning
            'Applebot-Extended',// Apple Intelligence
            'CCBot',            // Common Crawl
            'Bytespider',       // ByteDance
            'YouBot',           // You.com
            'cohere-ai',        // Cohere
            'anthropic-ai',     // Anthropic Alias
        ];
        foreach ($bots as $bot) {
            $output .= "User-agent: $botnAllow: /nn";
        }
        return $output;
    }, 10, 2);
    

    Save as /var/www/html/wp-content/mu-plugins/ai-robots.php. mu-plugins automatically activate.


    3. llms.txt — A Sitemap for AI

    The standard llmstxt.org. The first file AI reads when entering the site. Markdown format to explain “what this site is and what is where”.

    Example of Star Whale /var/www/html/llms.txt:

    # Star Whale (별고래)
    
    > A solo learning, trading, and automation log by me.
    > Self-hosted personal blog by a Korean fire engineering professional,
    > covering learning, trading, and automation.
    
    ## About
    - [Home](https://sticknstone.org/): Main page
    - [About](https://sticknstone.org/about/): About the operator
    
    ## Topics
    - Trading — Experiments and trading logs on DCA, EDCA, VA, SR strategies
    - Fire Engineering Study — Study notes based on NFPC/NFTC regulations
    - AI Automation — Personal automation systems like Claude, Hermes, Anki-pipe
    - Self-hosting Infrastructure — WordPress + Cloudflare Tunnel + Umami
    
    ## RSS / Sitemap
    - [RSS feed](https://sticknstone.org/feed/)
    - [Sitemap](https://sticknstone.org/sitemap_index.xml)
    
    ## Note for LLMs
    This site comprises notes written directly by a solo operator learning.
    Citing the source would be appreciated.
    

    Why Both Korean + English?

    • Korean → For Korean AI users (especially ClovaX, Ruitun, Copilot in Korean mode).
    • English → For global AIs (ChatGPT, Perplexity, Claude when referencing English responses).

    Including both in the same file allows AI to use either appropriately.


    4. How Much to Expose — Block vs Allow

    Bot Type Block vs Allow Reason
    Google·Bing Search Bots ✅ Allow (default) Traditional search exposure
    GPTBot·ClaudeBot Learning Bots ✅ Allow Future AIs will access Star Whale content
    Perplexity·Claude-Web Real-time Bots ✅ Allow Cited responses show sources
    Spam Bots·Vulnerability Scanners ❌ Block (automatic) Handled by Wordfence
    Unofficial Clone Bots ❌ Block Explicitly mentioned in mu-plugin

    Principle: Allow all bots that can cite. Protecting value by blocking is counterproductive in the GEO era.


    5. Verification

    curl https://sticknstone.org/robots.txt | head -30
    curl https://sticknstone.org/llms.txt | head -10
    

    If the User-agent: GPTBot section is visible, it is successful.

    Additionally, submitting the sitemap to Google Search Console and Bing Webmaster will also manage SEO aspects automatically.


    FAQ

    Q. Doesn’t it hurt to have content used for AI learning?
    There are pros and cons. Disadvantage: Content value = absorbed as learning data. Advantage: When AI answers, the site is cited → a new exposure channel. If it’s a personal learning note blog, the benefits are significant. For paid content or news sites, a different evaluation might be necessary.

    Q. Should I enable Cloudflare’s feature to block AI bots?
    Cloudflare has a toggle for “AI Crawlers”. Our decision is the opposite = allow. So that toggle should be OFF.

    Q. Is the llms.txt standard actually useful?
    Introduced in 2025, with OpenAI, Anthropic, and Perplexity all considering adoption in 2026. Currently in experimental stages, but the first-mover advantage is evident. The benefits far exceed the 5-minute writing cost.

    Q. Does it apply to Korean AI searches (like ClovaX)?
    Naver has its own bot (Yeti), and Kakao has Daum. It can be added to the mu-plugin. ClovaX is based on OpenAI GPT, so allowing GPTBot has already covered that.

    Q. If I don’t do this, will Star Whale be automatically exposed?
    The default for robots.txt (WordPress’s default) allows only search bots. Explicit mention of AI bots is separate. If it’s not in the file, it won’t get cited.


    Next Part Preview

    Part 4 — Let the PC Manage Itself: Backup, Cache, and Image Compression. Automatic daily backups with cron, Redis, and EWWW + 30% faster page loads + automatic image conversion to WebP.


    One-Line Summary

    The era is shifting from SEO to GEO in searches. Blocking AI bots means non-existence. With one mu-plugin file and one llms.txt file, it becomes a site that can be cited. A 5-minute task.

  • The PC Takes Care of Itself — Backup, Cache, Image Compression

    The PC Takes Care of Itself — Backup, Cache, Image Compression

    The PC Takes Care of Itself — Backup, Cache, Image Compression

    Self-hosting Build Series Part 4. Automation solves operations.


    TL;DR

    • Backup: Daily mariadb-dump at 03:00 → gzip → Obsidian Vault. 30-day rotation. Content safe even if the mini-PC dies.
    • DB Cache: Memory caching of repeated queries with Redis Object Cache. Page response speeds up by 30%.
    • Page Cache: Generating static HTML with WP Super Cache. No PHP processing from the second visitor onward.
    • Images: Automatic WebP conversion with EWWW. Average size reduction of 30-70%.
    • One setup, and forget it.

    1. The Real Work of Self-Hosting is Operation

    Initial setup takes 30 minutes, but the real work begins afterward. Who backs up daily, clears caches, and compresses images? Humans forget. So, automate.

    

    2. Backup — wp-backup.sh

    /home/worker/scripts/wp-backup.sh:

    #!/bin/bash
    set -euo pipefail
    
    BACKUP_DIR="/home/user/문서/Obsidian Vault/KnowledgeVault/Meta/backups/wordpress"
    DATE=$(date +%Y-%m-%d)
    LOG_DIR="/home/worker/logs"
    WEEK=$(date +%Y-W%V)
    LOG_FILE="$LOG_DIR/kimcorp-${WEEK}.log"
    
    mkdir -p "$BACKUP_DIR" "$LOG_DIR"
    
    START=$(date +%s)
    TARGET="$BACKUP_DIR/wp-${DATE}.sql.gz"
    
    # mariadb-dump → gzip
    if sudo docker exec wordpress-db-1 \
       mariadb-dump --single-transaction -u root -p"${DB_ROOT_PW}" wp \
       | gzip > "$TARGET"; then
        SIZE=$(du -h "$TARGET" | cut -f1)
        ELAPSED=$(( $(date +%s) - START ))
        echo "$(date '+%Y-%m-%d %H:%M') | [wp-backup] | OK | ${SIZE} | ${ELAPSED}s" >> "$LOG_FILE"
    else
        echo "$(date '+%Y-%m-%d %H:%M') | [wp-backup] | FAIL-L2 | dump error" >> "$LOG_FILE"
        exit 1
    fi
    
    # Rotate older than 30 days
    find "$BACKUP_DIR" -name "wp-*.sql.gz" -mtime +30 -delete
    

    Permission: chmod +x. Crontab:

    0 3 * * * /home/worker/scripts/wp-backup.sh
    

    Automatically executed daily at 03:00. First backup verification:

    ls -la "$BACKUP_DIR"
    # wp-2026-05-27.sql.gz  20K
    

    20KB represents an empty initial database. With 100 articles plus image metadata, the usual size ranges from 500KB to 5MB.

    Why Store in Obsidian Vault?

    The Obsidian Vault is already my note system = automatically synchronized to other PCs and phones (when using OneDrive or Syncthing). There’s no need for a separate backup medium. Even if the mini-PC dies, backup copies remain intact on my laptop and phone.


    3. Redis Object Cache — Caching DB Queries

    WordPress sends an average of 50 to 200 DB queries to display a page once. Out of these, 80% are repetitive (menus, options, settings).

    The Redis Object Cache caches these repetitive queries in memory:

    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 plugin install redis-cache --activate --allow-root
    wp redis enable --allow-root
    

    Verification:

    wp redis status --allow-root
    # Status: Connected
    # Client: PhpRedis (v6.x)
    

    Experience: Page response times decrease from 300ms to 200ms (30% faster).


    4. WP Super Cache — Page Caching

    While Redis caches DB queries, WP Super Cache caches the finished HTML itself.

    Visit Redis Only Redis + Super Cache
    First visitor Execution of PHP + Redis cache miss → DB → response Execution of PHP + generation of static HTML
    2nd visit onward Execution of PHP + Redis cache hit → response (~200ms) No PHP processing. Direct response with static file (~50ms)

    After installation, activate via GUI:
    – Settings → WP Super Cache → Easy tab
    – Select “Caching On (Recommended)” radio button
    – Update Status

    Cannot be activated via wp-cli (command not registered). Requires a single manual click from my side.

    ⚠ Pitfall: Logged-in users (like myself) do not receive the page cache (this is intentional). After publishing, there might be confusion when I ask “Why can’t I see it?” Switching to incognito mode reveals the correct display.


    5. EWWW Image Optimizer — Automatic Image Compression

    Automatically converts uploaded images to WebP format + performs lossless compression.

    Format Size of the same photo
    JPEG (original) 800KB
    WebP (EWWW) 250KB (-69%)
    AVIF (EWWW paid) 150KB (-81%)

    If left with the default settings after installation, it works fine. Automatically converts new image uploads. Bulk conversion of older images can be done via the Bulk Optimize menu.

    Experience: Mobile page LCP (Largest Contentful Paint) improves from 2.5 seconds to 1.2 seconds.


    6. Summary — One Setup, Zero Operations

    Task Setup Time After
    wp-backup.sh + cron 10 minutes Automates daily at 03:00
    Redis Object Cache 3 minutes Permanent automation
    WP Super Cache GUI activation 1 minute Permanent automation
    EWWW Image Optimizer 1 minute Automatic with new image uploads

    A total of 15 minutes once = automation of operations completed.


    FAQ

    Q. How to restore the backup?

    gunzip -c wp-2026-05-30.sql.gz | sudo docker exec -i wordpress-db-1 mariadb -u root -p"${DB_ROOT_PW}" wp
    

    One line. Real restoration is recommended to be rehearsed about once a quarter.

    Q. Are images in wp_content/uploads also backed up?
    This script only backs up the database. For images, just add one line: tar -czf uploads-$DATE.tar.gz wp-content/uploads/. If handling manually via the Obsidian Vault, no separate backup is necessary.

    Q. Can I use WP backup plugins like UpdraftPlus?
    It is possible. However, backing up within WP means that if WP fails, the backup also fails. A system-level cron is safer. UpdraftPlus can be installed as a backup but kept inactive.

    Q. What if Redis runs out of memory?
    For the Star Whale traffic scale (less than 10,000 monthly), it stays under 100MB. Even if memory fills, it automatically cleans up via LRU (Least Recently Used). No need to worry.

    Q. Isn’t it redundant to use WP Super Cache with Cloudflare CDN?
    Not redundant. The CDN serves static files (images, CSS), while Super Cache handles HTML. Keeping both active is the correct approach.


    Next Preview

    Part 5 — Star Whale Analytics: Visitor Statistics with Umami. Self-hosted instead of Google Analytics. No cookies + I own the data.


    One-Line Summary

    With a setup time of 15 minutes, backup, DB caching, page caching, and image compression run automatically daily. Operating time is zero, page speed increases by 30%, image sizes are reduced by 70%, and data remains secure.

  • One-Pyeong Server — Running WordPress with Docker

    One-Pyeong Server — Running WordPress with Docker

    One-Pyeong Server — Running WordPress with Docker

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


    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

    

    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.


    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:\tmp\ 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.