From Zero to AI-Powered Blog: Docker, WP-CLI, and Natural Language Publishing with OpenClaw and Hermes
Most WordPress tutorials start with a one-click install on shared hosting. This one starts with SSH, a blank server, and the assumption that you would rather own your infrastructure than rent convenience.
Why This Stack
There is a failure mode that every self-hoster eventually hits: a perfectly good blog disappears because a billing cycle gets missed, a platform sunsets, or a “simple” upgrade breaks the database. I lost ten years of writing that way. Once.
So when I rebuilt, I made a deliberate set of choices:
- Docker — the entire WordPress and database stack lives in two containers. Move it to any server in minutes.
- Nginx on the host — handles TLS termination, serves existing sites, and proxies WordPress cleanly. No port conflicts.
- Let’s Encrypt — free, auto-renewing SSL. No annual cert anxiety.
- Cloudflare — CDN, DDoS protection, and orange-cloud proxying with zero additional infrastructure.
- WP-CLI — command-line management for everything. No clicking through the admin UI to do bulk operations.
- OpenClaw and Hermes — AI agents that can draft and publish posts through natural language conversation.
The result is a blog I can fully control from an SSH terminal, or from a Telegram message.
The Architecture
Internet
|
v
Cloudflare (CDN + DDoS + Orange Cloud)
| HTTPS
v
Nginx on host (port 443, TLS termination)
| HTTP proxy
v
wp_app container (127.0.0.1:8082 -> container:80)
| internal Docker network
v
wp_db container (MariaDB, port 3306, internal only)
The key insight: WordPress is never exposed to the internet directly. Nginx is the only public-facing process. WordPress listens on 127.0.0.1:8082 — unreachable from outside the server regardless of firewall rules.
Part 1 — Deploying WordPress in Docker
The docker-compose.yml
mkdir -p /opt/wordpress
services:
db:
image: mariadb:11
container_name: wp_db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: your-root-password
MYSQL_DATABASE: wordpress
MYSQL_USER: wpuser
MYSQL_PASSWORD: your-db-password
volumes:
- wp_db_data:/var/lib/mysql
networks:
- wordpress_net
wordpress:
image: wordpress:latest
container_name: wp_app
restart: unless-stopped
ports:
- "127.0.0.1:8082:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: your-db-password
volumes:
- wp_content:/var/www/html/wp-content
- /opt/wordpress/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro
depends_on:
- db
networks:
- wordpress_net
volumes:
wp_db_data:
wp_content:
networks:
wordpress_net:
driver: bridge
Why MariaDB 11 instead of MySQL 8? About 30% lighter on RAM and fully wire-compatible with WordPress.
Why bind WordPress to 127.0.0.1:8082? If you bind to 0.0.0.0:8082, WordPress is directly reachable on port 8082, bypassing nginx and Cloudflare entirely. Binding to localhost means the only path in is through nginx.
Custom PHP Upload Limit
The default WordPress upload limit is 2MB. Override it with a mounted config file:
cat > /opt/wordpress/uploads.ini << EOF
upload_max_filesize = 20M
post_max_size = 20M
EOF
This file is bind-mounted into the container at /usr/local/etc/php/conf.d/uploads.ini. To change the limit later, edit the file and restart wp_app — no rebuild needed.
Start the Stack
cd /opt/wordpress
docker compose up -d
# Verify
docker ps | grep wp_
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8082/
# 302 = redirect to install page, correct
Part 2 — Nginx Reverse Proxy and SSL
The Port Conflict Situation
Port 80 and 443 are already occupied by an existing Nginx serving other sites. The solution: add a new server block in a new config file under /etc/nginx/conf.d/. Never touch existing config files.
Create the vHost
mkdir -p /var/www/certbot
cat > /etc/nginx/conf.d/yourdomain.conf << 'EOF'
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
try_files $uri =404;
}
location / {
proxy_pass http://127.0.0.1:8082;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
client_max_body_size 64M;
}
}
EOF
nginx -t && nginx -s reload
DNS Setup in Cloudflare
Before issuing an SSL certificate, DNS must resolve to your server:
- Add
Arecord pointing to your server IP — grey cloud (DNS only) - Add
CNAMEwwwpointing to apex domain — grey cloud (DNS only) - SSL/TLS mode: Full
Grey cloud is required during cert issuance. Certbot’s HTTP-01 challenge must reach your server directly — Cloudflare’s orange cloud will intercept it and cause verification to fail.
Issue the Certificate
dnf install -y certbot python3-certbot-nginx
certbot --nginx \
-d yourdomain.com \
-d www.yourdomain.com \
--non-interactive \
--agree-tos \
-m [email protected] \
--redirect
Certbot automatically modifies the nginx vHost to add HTTPS, configures the redirect from HTTP, and sets up a systemd timer for auto-renewal.
After the cert is issued: switch Cloudflare to orange cloud, change SSL/TLS mode to Full (strict).
Part 3 — Managing WordPress with WP-CLI
WP-CLI is the command-line interface for WordPress. It gives you complete control — posts, pages, users, plugins, themes, options, database — without opening a browser.
Install WP-CLI in the Container
The official WordPress Docker image does not include WP-CLI. One-time install:
docker exec wp_app bash -c \
'curl -sO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
&& chmod +x wp-cli.phar \
&& mv wp-cli.phar /usr/local/bin/wp'
Every WP-CLI command from now on follows this pattern:
docker exec wp_app wp --allow-root --path=/var/www/html <command>
Publishing a Post — The Direct Three-Step Flow
The fastest way to publish from a Markdown file:
# Step 1: Convert Markdown to HTML
python3 -c "
import markdown, sys
with open('post.md', encoding='utf-8') as f:
body = f.read()
print(markdown.markdown(body, extensions=['tables','fenced_code','nl2br']))
" > /tmp/post.html
# Step 2: Copy the HTML file into the container
docker cp /tmp/post.html wp_app:/tmp/post.html
# Step 3: Publish with WP-CLI
docker exec wp_app wp post create /tmp/post.html \
--post_title="Your Post Title" \
--post_status=publish \
--post_category=3 \
--allow-root --path=/var/www/html \
--porcelain
# Returns the new post ID
That is the entire pipeline. No admin UI, no copy-pasting, no formatting by hand.
To publish with a specific historical date — useful when migrating old content and you want to preserve the original timeline:
docker exec wp_app wp post create /tmp/post.html \
--post_title="Old Post Title" \
--post_status=publish \
--post_date="2024-09-30 08:00:00" \
--post_date_gmt="2024-09-30 08:00:00" \
--post_category=3 \
--allow-root --path=/var/www/html \
--porcelain
WP-CLI Commands for Blog Management
Posts — reading and listing:
# List all published posts with ID, title, and date
docker exec wp_app wp post list \
--post_status=publish \
--fields=ID,post_title,post_date \
--format=table \
--allow-root --path=/var/www/html
# Get a specific post's full content
docker exec wp_app wp post get 42 \
--fields=ID,post_title,post_status,post_date,post_content \
--allow-root --path=/var/www/html
# Count posts by status
docker exec wp_app wp post list --post_status=publish --format=count --allow-root --path=/var/www/html
docker exec wp_app wp post list --post_status=draft --format=count --allow-root --path=/var/www/html
Posts — editing:
# Update a post's title
docker exec wp_app wp post update 42 \
--post_title="New Title" \
--allow-root --path=/var/www/html
# Update post content from a file
docker cp /tmp/updated.html wp_app:/tmp/updated.html
docker exec wp_app wp post update 42 \
--post_content="$(docker exec wp_app cat /tmp/updated.html)" \
--allow-root --path=/var/www/html
# Change a draft to published
docker exec wp_app wp post update 42 \
--post_status=publish \
--allow-root --path=/var/www/html
# Move a post back to draft
docker exec wp_app wp post update 42 \
--post_status=draft \
--allow-root --path=/var/www/html
Posts — deleting:
# Move to trash
docker exec wp_app wp post delete 42 \
--allow-root --path=/var/www/html
# Delete permanently (skip trash)
docker exec wp_app wp post delete 42 \
--force \
--allow-root --path=/var/www/html
# Delete all posts in a category (get IDs first, then delete)
docker exec wp_app wp post list \
--post_status=publish \
--category=5 \
--format=ids \
--allow-root --path=/var/www/html \
| xargs -I{} docker exec wp_app wp post delete {} --force --allow-root --path=/var/www/html
Categories and tags:
# List all categories with post counts
docker exec wp_app wp term list category \
--fields=term_id,name,slug,count \
--format=table \
--allow-root --path=/var/www/html
# Create a new category
docker exec wp_app wp term create category "Travel" \
--allow-root --path=/var/www/html
# Rename an existing category
docker exec wp_app wp term update category 8 \
--name="My Thoughts" \
--slug="my-thoughts" \
--allow-root --path=/var/www/html
# Assign a category to a post
docker exec wp_app wp post term set 42 category "My Works" \
--allow-root --path=/var/www/html
# List all tags
docker exec wp_app wp term list post_tag \
--fields=term_id,name,count \
--format=table \
--allow-root --path=/var/www/html
Users:
# List all users
docker exec wp_app wp user list \
--fields=ID,user_login,display_name,roles \
--format=table \
--allow-root --path=/var/www/html
# Change a user's password
docker exec wp_app wp user update 1 \
--user_pass="new-secure-password" \
--allow-root --path=/var/www/html
# Create a new author account
docker exec wp_app wp user create author [email protected] \
--role=author \
--allow-root --path=/var/www/html
Plugins and themes:
# List installed plugins and their status
docker exec wp_app wp plugin list --format=table --allow-root --path=/var/www/html
# Install and activate a plugin
docker exec wp_app wp plugin install wp-super-cache --activate \
--allow-root --path=/var/www/html
# Deactivate a plugin
docker exec wp_app wp plugin deactivate wp-super-cache \
--allow-root --path=/var/www/html
# Update all plugins at once
docker exec wp_app wp plugin update --all \
--allow-root --path=/var/www/html
# List available themes
docker exec wp_app wp theme list --format=table --allow-root --path=/var/www/html
Site settings:
# Check the site URL and home URL (useful after domain changes)
docker exec wp_app wp option get siteurl --allow-root --path=/var/www/html
docker exec wp_app wp option get home --allow-root --path=/var/www/html
# Update site title and tagline
docker exec wp_app wp option update blogname "My Blog" \
--allow-root --path=/var/www/html
docker exec wp_app wp option update blogdescription "Thoughts on technology and life" \
--allow-root --path=/var/www/html
# Set reading settings — show latest posts or a static page
docker exec wp_app wp option update show_on_front "posts" \
--allow-root --path=/var/www/html
Database and maintenance:
# Export the database (run on the host server)
docker exec wp_db mysqldump -u wpuser -pyour-db-password wordpress > /opt/wordpress/backup.sql
# Search and replace URLs in the database (useful after domain migration)
docker exec wp_app wp search-replace \
'http://old-domain.com' 'https://new-domain.com' \
--allow-root --path=/var/www/html
# Run database optimization
docker exec wp_app wp db optimize --allow-root --path=/var/www/html
# Check WordPress installation for errors
docker exec wp_app wp doctor check --all --allow-root --path=/var/www/html
Cache:
# Flush the object cache (Memcached, Redis, etc.)
docker exec wp_app wp cache flush --allow-root --path=/var/www/html
# If using WP Super Cache
docker exec wp_app wp super-cache flush --allow-root --path=/var/www/html
Part 4 — AI Agents: Natural Language Publishing with OpenClaw and Hermes
This is where the stack stops being infrastructure and starts being a workflow.
Both OpenClaw and Hermes Agent run on the same server as WordPress. They have shell access. Anything you can do with WP-CLI, they can do on your behalf — triggered by a chat message.
OpenClaw: Conversational Publishing
OpenClaw is a self-hosted AI gateway that connects to messaging channels (Telegram, WeChat, Feishu) and extends its capabilities through Skills — Markdown files that define what the agent can do and how.
A WordPress publishing skill looks like this:
---
name: wordpress
description: Create, update, list, and delete WordPress posts. Turn ideas into published posts.
---
# WordPress Publishing Skill
## Publish a new post
1. Write content to /tmp/wp_draft.html
2. Copy into container: docker cp /tmp/wp_draft.html wp_app:/tmp/wp_draft.html
3. Publish:
docker exec wp_app wp post create /tmp/wp_draft.html \
--post_title="<title>" \
--post_status=publish \
--post_category=<cat_id> \
--allow-root --path=/var/www/html --porcelain
## List recent posts
docker exec wp_app wp post list \
--post_status=publish \
--fields=ID,post_title,post_date \
--format=table \
--allow-root --path=/var/www/html
## Update a post
docker exec wp_app wp post update <ID> \
--post_title="<new title>" \
--allow-root --path=/var/www/html
With this skill active, a Telegram conversation becomes a publishing interface:
You: Write a 500-word post about why self-hosted infrastructure beats SaaS for long-term reliability. Publish it under “My Works”.
OpenClaw: Drafting now…
(30 seconds later)
Published. Post ID 97. Live at https://yourdomain.com/?p=97You: List my last 5 posts.
OpenClaw: Here are your most recent posts:
ID 97 | Why Self-Hosted Beats SaaS | 2026-04-26
ID 96 | From Zero to AI-Powered Blog | 2026-04-26
…You: Move post 95 back to draft.
OpenClaw: Done. Post 95 is now a draft.
You never open a browser.
Hermes Agent: Research-to-Publication Pipeline
Hermes is a persistent agent framework for longer, multi-step workflows. Where OpenClaw handles quick conversational requests, Hermes handles research-intensive tasks that take 5–15 minutes.
A typical Hermes-powered workflow:
Input:
"Research LFP vs NMC battery performance in high-temperature climates.
Write a 1200-word post comparing real-world data. Audience: EV buyers.
Publish to WordPress when done."
Hermes pipeline:
Step 1: Web search — battery chemistry, thermal performance data
Step 2: Synthesize into outline
Step 3: Write full draft in Markdown
Step 4: Convert to HTML
Step 5: docker cp + wp post create
Step 6: Return post URL
Because Hermes runs as a persistent server-side process, it handles multi-step research without timeout concerns and can chain actions across tools.
What Changes When AI Agents Enter the Loop
| Stage | Manual | With AI Agent |
|---|---|---|
| Idea capture | Open laptop, navigate to wp-admin | Send a Telegram message |
| Content creation | Write in editor | Agent generates |
| HTML formatting | Manual | Auto-converted |
| Metadata | Click through UI | Agent infers from content |
| Publish | Click button | Automatic |
| Total time | 20–45 min | 2–5 min |
The time savings matter less than the friction reduction. At 2am, you are not opening a laptop and logging into WordPress. You are sending a message. That message either becomes a post, or the idea disappears. The agent is the difference.
Things I Would Do Differently
Set up WP-CLI from day one. It should be part of the initial deployment script. Installing it retroactively means the container diverges from its base image, which complicates future upgrades.
Automate database backups immediately. restart: unless-stopped is uptime management, not disaster recovery. A nightly mysqldump to remote storage should be in place before publishing anything worth keeping:
# Add to crontab on the host server
0 3 * * * docker exec wp_db mysqldump -u wpuser -pyour-db-password wordpress | gzip > /opt/wordpress/backups/$(date +\%Y\%m\%d).sql.gz
Monitor disk usage. Docker volumes grow silently. Set a weekly check:
# Add to crontab
0 9 * * 1 df -h / | awk 'NR==2 {print $5, $4}' | mail -s "Server disk usage" [email protected]
Add a WordPress Application Password. WP-CLI via docker exec works when the agent runs on the same server. Application Passwords (Settings → Users → Application Passwords) allow agents on external machines to publish via the REST API without shell access — more flexible for distributed setups.
Closing
The stack described here is not remarkable in any individual part. Docker, nginx, Let’s Encrypt, WordPress, WP-CLI — all standard tools with years of documentation. What is less common is wiring them together with AI agents that speak human language.
The infrastructure runs unattended. The agents handle the operational layer. The only thing left requiring deliberate human input is deciding what to say.
That is the point.
Built on a self-hosted VPS. Docker 28.4.0, MariaDB 11, WordPress latest, WP-CLI 2.8.x. SSL by Let’s Encrypt. CDN and DDoS protection by Cloudflare. AI publishing via OpenClaw and Hermes Agent.
了解 ~/jintaoblog/朝夕见闻志⚡️ 的更多信息
订阅后即可通过电子邮件收到最新文章。
