wordpress: switch to dedicated MariaDB per site (was shared)

Reconsidered after the shared-MariaDB design's real cost became clear:
Kopia's generic backup (services/backup.sh) stops a service's
container to snapshot it, so a shared MariaDB instance would back up
-- and would have to be restored -- as one unit covering every site at
once. Restoring just one site's database to an earlier point meant
restoring the whole shared snapshot to a temporary location first and
manually extracting that site's data back out, not a direct restore.

Each site now gets its own dedicated MariaDB container embedded in its
own docker-compose.yml (same pattern as services/nextcloud.sh) instead
of registering a database on a shared instance:
- Removed _wordpress_ensure_shared_db() and the wordpress-db/
  wordpress_net shared resources entirely.
- Each site's compose file gets a `db` service (container
  <site>-db) on an explicitly-named per-site default network
  (<site>_net), so wp-cli's one-off container reliably joins the
  right network without depending on Docker Compose's implicit
  naming convention.
- DB creation goes through the mariadb image's own MYSQL_DATABASE/
  MYSQL_USER/MYSQL_PASSWORD env vars on first boot (same as
  nextcloud.sh) instead of an imperative `docker exec mysql -e
  "CREATE DATABASE..."` against a shared container.
- Root and site DB passwords are both reused across reruns (read from
  the existing .env), verified via a real update-mode rerun.

Tradeoff, stated in both the script's header comment and the generated
per-site README: more RAM per site (~100-150MB for a full MariaDB
container instead of a slice of one shared instance) in exchange for
independent backup/restore. Data was already fully isolated either way
(separate database + user, always required since WordPress's schema
uses generic table names) -- the shared-vs-dedicated choice was only
ever about the container/process, not the data.

Re-verified end-to-end against the fake docker shim: distinct ports,
distinct dedicated DB containers/networks per site, correct compose/
.env structure, credentials preserved across an update-mode rerun.

docs/vps-sizing-recommendations.md: updated to match -- WordPress
capacity recomputed for dedicated-per-site MariaDB (~580MB headroom at
4 sites, ~976MB at 2, vs. the shared design's ~700MB/~950MB).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TBtExJcqxnokyZZKmphdug
This commit is contained in:
Claude
2026-08-09 21:23:24 +00:00
parent e96a257d8f
commit a5d57050b3
3 changed files with 109 additions and 147 deletions
+1 -1
View File
@@ -68,7 +68,7 @@ a ready-to-copy Caddy config snippet to `~/docker/caddy-snippets/`.
|-------|---------| |-------|---------|
| `base` | `net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`; `glow` (terminal markdown reader, Charm apt repo); Docker CE + Compose plugin; `openssh-server` with GitHub/Launchpad SSH key import, optional password-auth lockdown, and SSH Host aliases; optional NetBird overlay network | | `base` | `net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`; `glow` (terminal markdown reader, Charm apt repo); Docker CE + Compose plugin; `openssh-server` with GitHub/Launchpad SSH key import, optional password-auth lockdown, and SSH Host aliases; optional NetBird overlay network |
| `homelab` | `caddy`, `crowdsec`, `authelia`, `coturn` (shared TURN/STUN relay — Asterisk, Mattermost Calls, and future WebRTC-capable services all register a dedicated credential against one instance instead of each running its own), `homeassistant`, `asterisk`, `pstn-trunk`, `sms-inbound`, `security-dashboard`, `sunshine` | | `homelab` | `caddy`, `crowdsec`, `authelia`, `coturn` (shared TURN/STUN relay — Asterisk, Mattermost Calls, and future WebRTC-capable services all register a dedicated credential against one instance instead of each running its own), `homeassistant`, `asterisk`, `pstn-trunk`, `sms-inbound`, `security-dashboard`, `sunshine` |
| `utilities` | `actualbudget`, `ai-gpu`, `ai-stack`, `archivebox`, `changedetection`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `homebox`, `iopaint`, `joplin`, `koha`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `n8n`, `nextcloud`, `ntfy`, `onlyoffice`, `paintplus`, `portainer`, `rustdesk`, `stirling-pdf`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy`, `wordpress` (multi-site, shared MariaDB — blogs, business sites, e-commerce via WooCommerce) | | `utilities` | `actualbudget`, `ai-gpu`, `ai-stack`, `archivebox`, `changedetection`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `homebox`, `iopaint`, `joplin`, `koha`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `n8n`, `nextcloud`, `ntfy`, `onlyoffice`, `paintplus`, `portainer`, `rustdesk`, `stirling-pdf`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy`, `wordpress` (multi-site, dedicated MariaDB per site — blogs, business sites, e-commerce via WooCommerce) |
| `media` | `arm`, `audiobookshelf`, `calibre-web`, `emby`, `immich`, `jellyfin`, `lyrion` | | `media` | `arm`, `audiobookshelf`, `calibre-web`, `emby`, `immich`, `jellyfin`, `lyrion` |
| `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` | | `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` |
| `gaming` | `drum-rhythm-game`, `js99er`, `kyber-launcher`, `kyber-server`, `minecraft`, `wolf`, `wolf-pair` | | `gaming` | `drum-rhythm-game`, `js99er`, `kyber-launcher`, `kyber-server`, `minecraft`, `wolf`, `wolf-pair` |
+28 -16
View File
@@ -140,14 +140,23 @@ again; it just isn't part of the current baseline.
**WordPress — confirmed, 2-4 sites, light traffic, ecommerce-capable.** **WordPress — confirmed, 2-4 sites, light traffic, ecommerce-capable.**
`services/wordpress.sh` (new): multi-site from the start, every site named, `services/wordpress.sh` (new): multi-site from the start, every site named,
one shared MariaDB container instead of a dedicated database container per each with its own **dedicated** MariaDB container (same pattern as
site (same resource-sharing idea as `coturn`, scoped to WordPress's own `services/nextcloud.sh`) — not a shared instance. Started as a shared-MariaDB
sites). Each site gets its own database + user within that shared design (same resource-sharing idea as `coturn`) but switched to dedicated
instance — required, not just tidy: WordPress's schema uses generic table per-site after weighing it against backup/restore: Kopia's generic backup
names (`wp_posts`, `wp_options`, etc.), so two installs sharing one database (`services/backup.sh`) stops a service's container to snapshot it, so a
with the same table prefix would collide. wp-cli automates the initial shared instance would back up — and would have to be restored — as one unit
install (title, admin account) so there's no per-site browser setup wizard, covering every site at once, not one site independently. Dedicated per-site
and PHP limits are pre-tuned (256M memory, 64M uploads) for WooCommerce costs more RAM (a full MariaDB container each, ~100-150MB, instead of one
instance amortized across sites) in exchange for real isolation: each
site's database backs up and restores completely independently. Separate
databases were always required regardless of which model — WordPress's
schema uses generic table names (`wp_posts`, `wp_options`, etc.), so two
installs sharing one database with the same table prefix would collide —
the shared-vs-dedicated choice was only ever about the container/process,
never about the data being mixed. wp-cli automates the initial install
(title, admin account) so there's no per-site browser setup wizard, and PHP
limits are pre-tuned (256M memory, 64M uploads) for WooCommerce
specifically since "possible ecommerce" was part of the ask. specifically since "possible ecommerce" was part of the ask.
**Explicitly out of scope for this box** (wrong fit, not "can't run"): **Explicitly out of scope for this box** (wrong fit, not "can't run"):
@@ -182,15 +191,18 @@ specifically since "possible ecommerce" was part of the ask.
| Traccar (JVM) | ~425MB | | Traccar (JVM) | ~425MB |
| NetBird client | ~35MB | | NetBird client | ~35MB |
| ntfy, actualbudget, mealie | ~340MB combined | | ntfy, actualbudget, mealie | ~340MB combined |
| Shared MariaDB (WordPress, one-time) | ~180MB | | WordPress × 4 sites (app ~80MB + dedicated MariaDB ~120MB each) | ~800MB |
| WordPress × 4 sites (~120MB each — sized for possible WooCommerce, not a light blog) | ~480MB | | **Total** | **~3.52GB** |
| **Total** | **~3.38GB** |
Leaves roughly **~700MB headroom (~17%)** out of 4GB — tighter than the Leaves roughly **~580MB headroom (~14%)** out of 4GB at 4 sites — tighter
25-30% rule of thumb above, but with the swapfile now automatic than the shared-MariaDB design would have been (~700MB), the real cost of
(`ensure_swapfile`, see above) there's real insurance against burst load per-site backup/restore isolation, and tighter than the 25-30% rule of
rather than relying on manual setup. At the low end of the site range (2 thumb above. With the swapfile now automatic (`ensure_swapfile`, see above)
instead of 4), headroom improves to roughly ~950MB (~23%). `wg-easy`, there's still real insurance against burst load. At the low end of the site
range (2 instead of 4), it's ~3.12GB used, ~976MB headroom (~24%) — the gap
between shared and dedicated MariaDB narrows a lot at low site counts,
since the shared model's one fixed instance cost is amortized across fewer
sites. `wg-easy`,
`homebox`, and `audiobookshelf` from earlier in this doc aren't included in `homebox`, and `audiobookshelf` from earlier in this doc aren't included in
this specific table — add them back in at ~25MB, ~125MB, and ~200MB this specific table — add them back in at ~25MB, ~125MB, and ~200MB
respectively if/when they're actually deployed alongside this baseline. respectively if/when they're actually deployed alongside this baseline.
+80 -130
View File
@@ -1,17 +1,23 @@
#!/bin/bash #!/bin/bash
# services/wordpress.sh — Self-hosted WordPress sites, multi-site, shared MariaDB. # services/wordpress.sh — Self-hosted WordPress sites, multi-site, dedicated MariaDB per site.
# Part of the modular post-install system (sourced by setup.sh). # Part of the modular post-install system (sourced by setup.sh).
# #
# Can also be run standalone on any machine: # Can also be run standalone on any machine:
# sudo bash wordpress.sh # sudo bash wordpress.sh
# (Docker must already be installed when run standalone) # (Docker must already be installed when run standalone)
# #
# Every WordPress site gets its own directory/containers, but all sites on # Every WordPress site gets its own directory, its own WordPress container,
# this box share ONE MariaDB container (chain-installed on first need, same # and its own dedicated MariaDB container (same pattern as services/
# "one shared thing instead of N heavy duplicates" idea as services/coturn.sh # nextcloud.sh) — deliberately NOT a shared MariaDB instance across sites.
# — just scoped to WordPress's own sites rather than shared across different # A shared instance would mean one site's database backup/restore is
# services). Each site gets its own database + credentials within that # entangled with every other site's: Kopia's generic backup (services/
# shared instance instead of a dedicated MariaDB container per site. # backup.sh) stops a service's container to get a consistent snapshot, so a
# shared instance backs up (and would have to be restored) as one unit
# covering every site at once, not one site independently. Costs more RAM
# per site (a full MariaDB container each, ~100-150MB, instead of one
# instance split across sites) in exchange for real backup/restore
# isolation — each site's database can be restored to any point in time
# without touching any other site's current data.
# #
# E-commerce (WooCommerce) is just a normal WordPress plugin — no separate # E-commerce (WooCommerce) is just a normal WordPress plugin — no separate
# infrastructure needed. PHP upload/memory limits are tuned upfront so it # infrastructure needed. PHP upload/memory limits are tuned upfront so it
@@ -202,91 +208,7 @@ CBLOCK
fi fi
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
register_service wordpress utilities "Self-hosted WordPress sites (multi-site, shared MariaDB) — blogs, business sites, e-commerce via WooCommerce" 8090 register_service wordpress utilities "Self-hosted WordPress sites (multi-site, dedicated MariaDB per site) — blogs, business sites, e-commerce via WooCommerce" 8090
# ── Shared MariaDB, chain-installed the first time any site needs it ────────
# Out-param (not `local`): WP_DB_ROOT_PASS — read after this returns.
_wordpress_ensure_shared_db() {
local DB_DIR="$DOCKER_DIR/wordpress-db"
WP_DB_ROOT_PASS=""
if [ -f "$DB_DIR/.env" ]; then
WP_DB_ROOT_PASS="$(grep '^MYSQL_ROOT_PASSWORD=' "$DB_DIR/.env" | cut -d= -f2-)"
[ -n "$WP_DB_ROOT_PASS" ] && return 0
fi
log_info "No shared WordPress database yet — setting one up (used by every WordPress site on this box)..."
mkdir -p "$DB_DIR/data"
ensure_docker_dir_ownership "$DB_DIR"
WP_DB_ROOT_PASS="$(generate_password 32)"
# Dedicated network so WordPress site containers can reach the shared DB
# without joining caddy_net (that one's for Caddy<->service HTTP traffic).
docker network inspect wordpress_net &>/dev/null || docker network create wordpress_net &>/dev/null
cat > "$DB_DIR/docker-compose.yml" << 'WPDBCOMPOSE'
name: wordpress-db
services:
wordpress-db:
image: mariadb:11
container_name: wordpress-db
hostname: wordpress-db
restart: unless-stopped
env_file: .env
volumes:
- ./data:/var/lib/mysql
networks:
- wordpress_net
networks:
wordpress_net:
external: true
WPDBCOMPOSE
cat > "$DB_DIR/.env" << WPDBENV
# Shared MariaDB for every WordPress site on this box — each site gets its
# own database + user within this one instance instead of a dedicated
# MariaDB container per site (same resource-sharing idea as coturn, just
# scoped to WordPress's own sites). Changing this breaks every site's DB
# connection until each site's .env is updated to match.
MYSQL_ROOT_PASSWORD=$WP_DB_ROOT_PASS
MARIADB_AUTO_UPGRADE=1
WPDBENV
chmod 600 "$DB_DIR/.env"
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$DB_DIR"
( cd "$DB_DIR" && docker compose up -d ) \
&& log_success "Shared WordPress database started" \
|| { log_warning "Failed to start the shared WordPress database — check: cd $DB_DIR && docker compose logs"; return 1; }
local _tries=0
until docker exec wordpress-db mysqladmin ping -uroot -p"$WP_DB_ROOT_PASS" --silent &>/dev/null || [ "$_tries" -ge 30 ]; do
sleep 1; _tries=$((_tries + 1))
done
write_readme "$DB_DIR" << WPDBREADME
# wordpress-db — shared MariaDB for every WordPress site
One MariaDB instance shared by every WordPress site on this box
(\`services/wordpress.sh\`) — each site gets its own database and user
within this instance instead of a dedicated MariaDB container per site.
Root credentials: \`.env\` (\`MYSQL_ROOT_PASSWORD\`, chmod 600).
## Manage
\`\`\`bash
docker compose up -d
docker compose down
docker compose logs -f
docker exec -it wordpress-db mysql -uroot -p
\`\`\`
Deleting this stops every WordPress site on the box — check
\`~/docker/wordpress-*\` for what depends on it before removing.
WPDBREADME
}
install_wordpress() { install_wordpress() {
require_docker || return 1 require_docker || return 1
@@ -302,10 +224,9 @@ install_wordpress() {
if [ "$DRY_RUN" = true ]; then if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would prompt for a site name (directory/container naming)" echo "[DRY-RUN] Would prompt for a site name (directory/container naming)"
echo "[DRY-RUN] Would ensure the shared wordpress-db MariaDB container exists"
echo "[DRY-RUN] (chain-installed on first WordPress site, reused by every other site)"
echo "[DRY-RUN] Would create this site's database + credentials in that shared instance"
echo "[DRY-RUN] Would create \$DOCKER_DIR/wordpress-<site> with docker-compose.yml + .env" echo "[DRY-RUN] Would create \$DOCKER_DIR/wordpress-<site> with docker-compose.yml + .env"
echo "[DRY-RUN] including a dedicated MariaDB container for this site alone (not shared"
echo "[DRY-RUN] with other sites — independent backup/restore per site)"
echo "[DRY-RUN] Would tune PHP memory_limit/upload_max_filesize for WooCommerce-readiness" echo "[DRY-RUN] Would tune PHP memory_limit/upload_max_filesize for WooCommerce-readiness"
echo "[DRY-RUN] Would auto-scan for a free host port for this site" echo "[DRY-RUN] Would auto-scan for a free host port for this site"
echo "[DRY-RUN] Would run wp-cli to install WordPress core non-interactively (site title," echo "[DRY-RUN] Would run wp-cli to install WordPress core non-interactively (site title,"
@@ -352,26 +273,23 @@ install_wordpress() {
esac esac
fi fi
# ── Shared MariaDB ──────────────────────────────────────────────────────── # ── This site's dedicated database credentials ───────────────────────────
_wordpress_ensure_shared_db || return 1 # The mariadb image creates MYSQL_DATABASE/MYSQL_USER itself from these
# env vars on first boot — no imperative CREATE DATABASE step needed,
# ── This site's database + credentials within the shared instance ─────── # same as services/nextcloud.sh. Reused across reruns (read from the
# existing .env if present) so a rerun never locks the site out of its
# own already-initialized database.
local DB_CONTAINER="${CONTAINER}-db"
local WP_NET="${CONTAINER}_net"
local WP_DB_NAME="wp_${SITE_NAME//-/_}" local WP_DB_NAME="wp_${SITE_NAME//-/_}"
local WP_DB_USER="wp_${SITE_NAME//-/_}" local WP_DB_USER="wp_${SITE_NAME//-/_}"
local WP_DB_PASS="" local WP_DB_PASS="" WP_DB_ROOT_PASS=""
[ -f "$DIR/.env" ] && WP_DB_PASS="$(grep '^WORDPRESS_DB_PASSWORD=' "$DIR/.env" | cut -d= -f2-)" if [ -f "$DIR/.env" ]; then
[ -n "$WP_DB_PASS" ] || WP_DB_PASS="$(generate_password 24)" WP_DB_PASS="$(grep '^WORDPRESS_DB_PASSWORD=' "$DIR/.env" | cut -d= -f2-)"
WP_DB_ROOT_PASS="$(grep '^MYSQL_ROOT_PASSWORD=' "$DIR/.env" | cut -d= -f2-)"
if docker exec wordpress-db mysql -uroot -p"$WP_DB_ROOT_PASS" -e \
"CREATE DATABASE IF NOT EXISTS \`$WP_DB_NAME\`; \
CREATE USER IF NOT EXISTS '$WP_DB_USER'@'%' IDENTIFIED BY '$WP_DB_PASS'; \
GRANT ALL PRIVILEGES ON \`$WP_DB_NAME\`.* TO '$WP_DB_USER'@'%'; \
FLUSH PRIVILEGES;" &>/dev/null; then
log_success "Database '$WP_DB_NAME' ready on the shared MariaDB instance"
else
log_warning "Could not create the database — is wordpress-db running? Check: docker logs wordpress-db"
return 1
fi fi
[ -n "$WP_DB_PASS" ] || WP_DB_PASS="$(generate_password 24)"
[ -n "$WP_DB_ROOT_PASS" ] || WP_DB_ROOT_PASS="$(generate_password 32)"
echo "" echo ""
local WP_SITE_TITLE="" WP_ADMIN_USER="" WP_ADMIN_EMAIL="" local WP_SITE_TITLE="" WP_ADMIN_USER="" WP_ADMIN_EMAIL=""
@@ -388,7 +306,7 @@ install_wordpress() {
WEB_PORT=$((WEB_PORT + 1)) WEB_PORT=$((WEB_PORT + 1))
done done
mkdir -p "$DIR/html" "$DIR/uploads-ini.d" mkdir -p "$DIR/html" "$DIR/db" "$DIR/uploads-ini.d"
ensure_docker_dir_ownership "$DIR" ensure_docker_dir_ownership "$DIR"
cd "$DIR" || return 1 cd "$DIR" || return 1
@@ -433,17 +351,30 @@ services:
hostname: $CONTAINER hostname: $CONTAINER
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
depends_on:
- db
volumes: volumes:
- ./html:/var/www/html - ./html:/var/www/html
- ./uploads-ini.d/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro - ./uploads-ini.d/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro
ports: ports:
- "${WEB_PORT}:80" - "${WEB_PORT}:80"
networks: networks:
- wordpress_net - default
${_CADDY_NET_LINE} ${_CADDY_NET_LINE}
db:
image: mariadb:11
container_name: $DB_CONTAINER
hostname: $DB_CONTAINER
restart: unless-stopped
env_file: .env
volumes:
- ./db:/var/lib/mysql
networks: networks:
wordpress_net: - default
external: true
networks:
default:
name: $WP_NET
${_CADDY_NET_SECTION} ${_CADDY_NET_SECTION}
WPCOMPOSE WPCOMPOSE
@@ -451,7 +382,15 @@ WPCOMPOSE
TZ=$TZ_VAL TZ=$TZ_VAL
CADDY_NET=$SITE_CADDY_NET CADDY_NET=$SITE_CADDY_NET
WORDPRESS_DB_HOST=wordpress-db # Dedicated MariaDB for this site alone (not shared with other WordPress
# sites) — independent backup/restore, at the cost of a full MariaDB
# container per site instead of one instance split across several.
MYSQL_ROOT_PASSWORD=$WP_DB_ROOT_PASS
MYSQL_DATABASE=$WP_DB_NAME
MYSQL_USER=$WP_DB_USER
MYSQL_PASSWORD=$WP_DB_PASS
WORDPRESS_DB_HOST=$DB_CONTAINER
WORDPRESS_DB_NAME=$WP_DB_NAME WORDPRESS_DB_NAME=$WP_DB_NAME
WORDPRESS_DB_USER=$WP_DB_USER WORDPRESS_DB_USER=$WP_DB_USER
WORDPRESS_DB_PASSWORD=$WP_DB_PASS WORDPRESS_DB_PASSWORD=$WP_DB_PASS
@@ -474,21 +413,27 @@ WPENV
write_readme "$DIR" << MD write_readme "$DIR" << MD
# WordPress — $SITE_NAME # WordPress — $SITE_NAME
Self-hosted WordPress site. Database lives on the shared \`wordpress-db\` Self-hosted WordPress site with its own **dedicated** MariaDB container
MariaDB instance (\`~/docker/wordpress-db\`) used by every WordPress site on (\`$DB_CONTAINER\`, in \`db/\` below) — not shared with any other WordPress
this box — not a dedicated database container for this site alone. site on this box. Costs more RAM per site than a shared database would, in
exchange for independent backup/restore: this site's database can be
restored to any point in time without touching any other site's data, since
each one is backed up (and would be restored) as its own separate Kopia
snapshot rather than being entangled with other sites in one shared
snapshot.
- Web UI: http://localhost:${WEB_PORT} - Web UI: http://localhost:${WEB_PORT}
- Admin user: \`$WP_ADMIN_USER\` - Admin user: \`$WP_ADMIN_USER\`
- Admin password: see \`WP_ADMIN_PASSWORD\` in \`.env\` - Admin password: see \`WP_ADMIN_PASSWORD\` in \`.env\`
- Site files: \`html/\` - Site files: \`html/\`
- Database files: \`db/\`
- PHP limits: \`uploads-ini.d/uploads.ini\` (256M memory, 64M uploads — - PHP limits: \`uploads-ini.d/uploads.ini\` (256M memory, 64M uploads —
raise further here if a specific import still hits a limit) raise further here if a specific import still hits a limit)
## Manage ## Manage
\`\`\`bash \`\`\`bash
cd $DIR cd $DIR
docker compose up -d # start docker compose up -d # start (both wordpress and its db)
docker compose down # stop docker compose down # stop
docker compose logs -f # logs docker compose logs -f # logs
docker compose pull && docker compose up -d # update docker compose pull && docker compose up -d # update
@@ -498,7 +443,7 @@ docker compose pull && docker compose up -d # update
Run any wp-cli command against this site without installing wp-cli on the Run any wp-cli command against this site without installing wp-cli on the
host: host:
\`\`\`bash \`\`\`bash
docker run --rm --network wordpress_net -v $DIR/html:/var/www/html \\ docker run --rm --network $WP_NET -v $DIR/html:/var/www/html \\
--env-file $DIR/.env wordpress:cli wp <command> --env-file $DIR/.env wordpress:cli wp <command>
\`\`\` \`\`\`
@@ -506,7 +451,7 @@ docker run --rm --network wordpress_net -v $DIR/html:/var/www/html \\
No separate infrastructure needed — WooCommerce is a normal WordPress No separate infrastructure needed — WooCommerce is a normal WordPress
plugin. Install it from Plugins → Add New in the WP admin, or via wp-cli: plugin. Install it from Plugins → Add New in the WP admin, or via wp-cli:
\`\`\`bash \`\`\`bash
docker run --rm --network wordpress_net -v $DIR/html:/var/www/html \\ docker run --rm --network $WP_NET -v $DIR/html:/var/www/html \\
--env-file $DIR/.env wordpress:cli wp plugin install woocommerce --activate --env-file $DIR/.env wordpress:cli wp plugin install woocommerce --activate
\`\`\` \`\`\`
The PHP limits above (256M memory, 64M uploads) were already sized with The PHP limits above (256M memory, 64M uploads) were already sized with
@@ -514,9 +459,14 @@ WooCommerce's own recommendations in mind, so product/image imports work
without hitting default-image limits on the first try. without hitting default-image limits on the first try.
## Backup ## Backup
Back up \`html/\` (site files/plugins/themes/media) and this site's \`services/backup.sh\` (Kopia) already covers this directory automatically —
database on \`wordpress-db\` (\`docker exec wordpress-db mysqldump -uroot -p it stops \`docker compose down\`, snapshots \`$DIR\`, and restarts, generically
$WP_DB_NAME > backup.sql\`) — \`.env\` holds the root password. for every \`~/docker/*\` directory with a \`docker-compose.yml\`, so both
\`html/\` and \`db/\` are captured together on every run with no per-site setup
needed. For an ad hoc logical dump instead:
\`\`\`bash
docker exec $DB_CONTAINER mysqldump -uroot -p"\$(grep MYSQL_ROOT_PASSWORD .env | cut -d= -f2-)" $WP_DB_NAME > backup.sql
\`\`\`
MD MD
local START_WP="" local START_WP=""
@@ -531,9 +481,9 @@ MD
sleep 1; _tries=$((_tries + 1)) sleep 1; _tries=$((_tries + 1))
done done
if docker run --rm --network wordpress_net \ if docker run --rm --network "$WP_NET" \
-v "$DIR/html:/var/www/html" \ -v "$DIR/html:/var/www/html" \
-e WORDPRESS_DB_HOST=wordpress-db \ -e WORDPRESS_DB_HOST="$DB_CONTAINER" \
-e WORDPRESS_DB_NAME="$WP_DB_NAME" \ -e WORDPRESS_DB_NAME="$WP_DB_NAME" \
-e WORDPRESS_DB_USER="$WP_DB_USER" \ -e WORDPRESS_DB_USER="$WP_DB_USER" \
-e WORDPRESS_DB_PASSWORD="$WP_DB_PASS" \ -e WORDPRESS_DB_PASSWORD="$WP_DB_PASS" \
@@ -549,8 +499,8 @@ MD
else else
log_warning "wp-cli install didn't complete (WordPress may not have been ready yet, or was" log_warning "wp-cli install didn't complete (WordPress may not have been ready yet, or was"
log_warning "already installed). Finish setup in the browser, or retry manually:" log_warning "already installed). Finish setup in the browser, or retry manually:"
log_warning " docker run --rm --network wordpress_net -v $DIR/html:/var/www/html \\" log_warning " docker run --rm --network $WP_NET -v $DIR/html:/var/www/html \\"
log_warning " -e WORDPRESS_DB_HOST=wordpress-db -e WORDPRESS_DB_NAME=$WP_DB_NAME \\" log_warning " -e WORDPRESS_DB_HOST=$DB_CONTAINER -e WORDPRESS_DB_NAME=$WP_DB_NAME \\"
log_warning " -e WORDPRESS_DB_USER=$WP_DB_USER -e WORDPRESS_DB_PASSWORD=$WP_DB_PASS \\" log_warning " -e WORDPRESS_DB_USER=$WP_DB_USER -e WORDPRESS_DB_PASSWORD=$WP_DB_PASS \\"
log_warning " wordpress:cli core install --url=http://localhost:${WEB_PORT} \\" log_warning " wordpress:cli core install --url=http://localhost:${WEB_PORT} \\"
log_warning " --title=\"$WP_SITE_TITLE\" --admin_user=$WP_ADMIN_USER \\" log_warning " --title=\"$WP_SITE_TITLE\" --admin_user=$WP_ADMIN_USER \\"