generated from bisco/codex-bootstrap
feat: use host bind mounts for persistent data
This commit is contained in:
@@ -22,3 +22,18 @@ LETSENCRYPT_STAGING=1
|
||||
LETSENCRYPT_RENEW_INTERVAL_SECONDS=43200
|
||||
LETSENCRYPT_RETRY_SECONDS=300
|
||||
TLS_RELOAD_INTERVAL_SECONDS=30
|
||||
|
||||
# Host-based persistent volumes. Relative paths are resolved from the project root.
|
||||
DB_DATA_PATH=./runtime/db
|
||||
WORDPRESS_DATA_PATH=./runtime/wordpress
|
||||
LETSENCRYPT_DATA_PATH=./runtime/letsencrypt
|
||||
CERTBOT_CHALLENGES_PATH=./runtime/certbot/www
|
||||
|
||||
# Expected container owners for host-based volumes.
|
||||
# MariaDB's official image normally uses mysql 999:999; WordPress uses www-data 33:33.
|
||||
MARIADB_VOLUME_UID=999
|
||||
MARIADB_VOLUME_GID=999
|
||||
WORDPRESS_VOLUME_UID=33
|
||||
WORDPRESS_VOLUME_GID=33
|
||||
CERTBOT_VOLUME_UID=0
|
||||
CERTBOT_VOLUME_GID=0
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
backups/
|
||||
runtime/
|
||||
tests/functional/test-results/
|
||||
tests/functional/playwright-report/
|
||||
|
||||
@@ -31,6 +31,7 @@ No host PHP, database, Node.js, or WordPress installation is required.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
./scripts/prepare-host-volumes.sh
|
||||
docker compose up --build -d
|
||||
docker compose --profile tools run --rm wp-cli /scripts/bootstrap.sh
|
||||
docker compose ps
|
||||
@@ -44,6 +45,20 @@ Open:
|
||||
Use the development credentials copied into `.env` only locally. Change them before
|
||||
sharing the environment.
|
||||
|
||||
Persistent data uses host-based bind mounts by default:
|
||||
|
||||
- `./runtime/db` for MariaDB;
|
||||
- `./runtime/wordpress` for WordPress core, uploads, themes, and mu-plugins copied at
|
||||
container startup;
|
||||
- `./runtime/letsencrypt` for certificates;
|
||||
- `./runtime/certbot/www` for ACME HTTP-01 challenges.
|
||||
|
||||
Run `./scripts/prepare-host-volumes.sh` before the first start, especially on Linux
|
||||
hosts. The script reads `.env`, creates the directories, and assigns the expected
|
||||
container owners (`999:999` for MariaDB, `33:33` for WordPress, `0:0` for Certbot).
|
||||
Override the `*_DATA_PATH` and `*_VOLUME_UID/GID` variables in `.env` if your runtime
|
||||
uses different host paths or image user IDs.
|
||||
|
||||
## Edit content
|
||||
|
||||
- **Appearance > Customize**: hero, manifesto, laboratory, teacher, lessons, contacts,
|
||||
@@ -109,10 +124,10 @@ docker compose config --quiet
|
||||
Back up the database and WordPress files at the same logical point in time:
|
||||
|
||||
```bash
|
||||
mkdir -p backups/wordpress-files
|
||||
docker compose stop wordpress
|
||||
mkdir -p backups
|
||||
docker compose exec -T db sh -c 'mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE"' > backups/database.sql
|
||||
docker compose cp wordpress:/var/www/html/. backups/wordpress-files/
|
||||
tar -C runtime -czf backups/wordpress-files.tgz wordpress
|
||||
docker compose start wordpress
|
||||
```
|
||||
|
||||
@@ -121,6 +136,7 @@ Do not commit backups or `.env`. Encrypt and test real backups off-host. See
|
||||
|
||||
## Rollback
|
||||
|
||||
Revert the application commit and rebuild containers. Preserve `db_data`,
|
||||
`wordpress_data`, and certificate volumes. Database restoration is a separate,
|
||||
destructive operation and requires a verified backup.
|
||||
Revert the application commit and rebuild containers. Preserve the host directories
|
||||
configured by `DB_DATA_PATH`, `WORDPRESS_DATA_PATH`, `LETSENCRYPT_DATA_PATH`, and
|
||||
`CERTBOT_CHALLENGES_PATH`. Database restoration is a separate, destructive operation
|
||||
and requires a verified backup.
|
||||
|
||||
+37
-13
@@ -12,7 +12,11 @@ services:
|
||||
entrypoint: ["/bin/sh", "/usr/local/bin/check-environment.sh"]
|
||||
command: ["mariadbd"]
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
- type: bind
|
||||
source: ${DB_DATA_PATH:-./runtime/db}
|
||||
target: /var/lib/mysql
|
||||
bind:
|
||||
create_host_path: true
|
||||
- ./db/check-environment.sh:/usr/local/bin/check-environment.sh:ro
|
||||
networks:
|
||||
- data
|
||||
@@ -55,7 +59,11 @@ services:
|
||||
define('DISALLOW_FILE_MODS', true);
|
||||
}
|
||||
volumes:
|
||||
- wordpress_data:/var/www/html
|
||||
- type: bind
|
||||
source: ${WORDPRESS_DATA_PATH:-./runtime/wordpress}
|
||||
target: /var/www/html
|
||||
bind:
|
||||
create_host_path: true
|
||||
networks:
|
||||
- web
|
||||
- data
|
||||
@@ -85,8 +93,18 @@ services:
|
||||
TLS_RELOAD_INTERVAL_SECONDS: ${TLS_RELOAD_INTERVAL_SECONDS:-30}
|
||||
TRUST_PROXY_HEADERS: ${TRUST_PROXY_HEADERS:-0}
|
||||
volumes:
|
||||
- letsencrypt_data:/etc/letsencrypt:ro
|
||||
- certbot_challenges:/var/www/certbot:ro
|
||||
- type: bind
|
||||
source: ${LETSENCRYPT_DATA_PATH:-./runtime/letsencrypt}
|
||||
target: /etc/letsencrypt
|
||||
read_only: true
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${CERTBOT_CHALLENGES_PATH:-./runtime/certbot/www}
|
||||
target: /var/www/certbot
|
||||
read_only: true
|
||||
bind:
|
||||
create_host_path: true
|
||||
networks:
|
||||
- web
|
||||
ports:
|
||||
@@ -119,8 +137,16 @@ services:
|
||||
entrypoint: ["/bin/sh", "/opt/certbot/renew.sh"]
|
||||
volumes:
|
||||
- ./certbot/renew.sh:/opt/certbot/renew.sh:ro
|
||||
- letsencrypt_data:/etc/letsencrypt
|
||||
- certbot_challenges:/var/www/certbot
|
||||
- type: bind
|
||||
source: ${LETSENCRYPT_DATA_PATH:-./runtime/letsencrypt}
|
||||
target: /etc/letsencrypt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${CERTBOT_CHALLENGES_PATH:-./runtime/certbot/www}
|
||||
target: /var/www/certbot
|
||||
bind:
|
||||
create_host_path: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/lib/letsencrypt
|
||||
@@ -159,7 +185,11 @@ services:
|
||||
entrypoint: ["/bin/sh"]
|
||||
command: ["/scripts/bootstrap.sh"]
|
||||
volumes:
|
||||
- wordpress_data:/var/www/html
|
||||
- type: bind
|
||||
source: ${WORDPRESS_DATA_PATH:-./runtime/wordpress}
|
||||
target: /var/www/html
|
||||
bind:
|
||||
create_host_path: true
|
||||
- ./wp-cli/bootstrap.sh:/scripts/bootstrap.sh:ro
|
||||
networks:
|
||||
- data
|
||||
@@ -175,12 +205,6 @@ services:
|
||||
- ALL
|
||||
pids_limit: 100
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
wordpress_data:
|
||||
letsencrypt_data:
|
||||
certbot_challenges:
|
||||
|
||||
networks:
|
||||
web:
|
||||
data:
|
||||
|
||||
@@ -18,8 +18,9 @@ sanitized theme modifications, while a must-use plugin owns Shows and Gallery cu
|
||||
post types so structured content is not lost when changing themes.
|
||||
|
||||
NGINX is the only public entry point. WP-CLI provides an idempotent opt-in bootstrap;
|
||||
Certbot provides opt-in direct TLS. Docker networks isolate the database and functional
|
||||
tests use separate volumes.
|
||||
Certbot provides opt-in direct TLS. Docker networks isolate the database. Runtime state
|
||||
uses host-based bind mounts with a preparation script for ownership/mode, while
|
||||
functional tests use separate Docker volumes.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -48,11 +49,12 @@ headers, and fail-closed TLS. Admin MFA and allowlisting remain external control
|
||||
## Operational impact
|
||||
|
||||
Database and WordPress files are coordinated state and must be backed up together.
|
||||
Host bind-mount paths must be permissioned for the image users before startup.
|
||||
Operators must monitor upstream security releases and rebuild pinned images. SMTP and
|
||||
off-host storage are not part of this minimal base.
|
||||
|
||||
## Rollback
|
||||
|
||||
Revert the implementation commit and rebuild. Preserve WordPress, database, and
|
||||
certificate volumes unless data deletion is intentional. Restore data only from a
|
||||
verified coordinated backup.
|
||||
Revert the implementation commit and rebuild. Preserve the configured WordPress,
|
||||
database, and certificate host directories unless data deletion is intentional. Restore
|
||||
data only from a verified coordinated backup.
|
||||
|
||||
@@ -16,8 +16,9 @@ activates the theme, configures the site, and creates realistic demo content. Ce
|
||||
another optional service, enabled only for direct deployments. It shares challenge and
|
||||
certificate volumes with NGINX but has no container-control access.
|
||||
|
||||
Persistent state lives in `db_data`, `wordpress_data`, and the certificate volumes.
|
||||
Functional tests replace the first two with isolated test volumes and reach NGINX via
|
||||
an internal `azionelab.org` network alias.
|
||||
Persistent state lives in host-based bind mounts configured by `DB_DATA_PATH`,
|
||||
`WORDPRESS_DATA_PATH`, `LETSENCRYPT_DATA_PATH`, and `CERTBOT_CHALLENGES_PATH`.
|
||||
Functional tests replace the database and WordPress mounts with isolated Docker test
|
||||
volumes and reach NGINX via an internal `azionelab.org` network alias.
|
||||
|
||||
See [ADR-0001](adr/0001-wordpress-single-page.md).
|
||||
|
||||
+18
-4
@@ -12,6 +12,19 @@ docker compose --profile tools run --rm wp-cli /scripts/bootstrap.sh
|
||||
NGINX binds to loopback ports 8080/8443. WordPress and MariaDB remain private. The
|
||||
bootstrap is safe to rerun and does not duplicate demo records.
|
||||
|
||||
The default persistent paths are host-based bind mounts under `./runtime`. Before the
|
||||
first start, run:
|
||||
|
||||
```bash
|
||||
./scripts/prepare-host-volumes.sh
|
||||
```
|
||||
|
||||
The script reads `.env`, creates the configured paths, and applies the expected
|
||||
container ownership. For Linux hosts this avoids the common problem where Docker
|
||||
creates missing bind-mount directories as `root:root` and WordPress or MariaDB later
|
||||
cannot write to them. If a platform uses different image UIDs/GIDs, override
|
||||
`MARIADB_VOLUME_UID/GID`, `WORDPRESS_VOLUME_UID/GID`, or `CERTBOT_VOLUME_UID/GID`.
|
||||
|
||||
## Production
|
||||
|
||||
Required controls:
|
||||
@@ -35,7 +48,8 @@ behavior, removing only the staging certificate volume when necessary.
|
||||
|
||||
## State and rollback
|
||||
|
||||
Database and WordPress file volumes must be backed up together. Code rollback is a
|
||||
container rebuild from a prior commit and does not require deleting volumes. Never use
|
||||
`docker compose down --volumes` where content must survive. Test database restoration
|
||||
in a disposable environment before any production restore.
|
||||
Database and WordPress file directories must be backed up together. Code rollback is a
|
||||
container rebuild from a prior commit and does not require deleting host data
|
||||
directories. Never delete the paths configured by `DB_DATA_PATH`, `WORDPRESS_DATA_PATH`,
|
||||
or `LETSENCRYPT_DATA_PATH` where content/certificates must survive. Test database
|
||||
restoration in a disposable environment before any production restore.
|
||||
|
||||
+10
-4
@@ -6,6 +6,7 @@
|
||||
docker compose up --build -d
|
||||
docker compose ps
|
||||
docker compose logs -f proxy wordpress db certbot
|
||||
./scripts/prepare-host-volumes.sh
|
||||
docker compose --profile tools run --rm wp-cli -c 'wp core version'
|
||||
docker compose down
|
||||
```
|
||||
@@ -24,9 +25,11 @@ image rebuilds are the update path.
|
||||
|
||||
## Backup and restore
|
||||
|
||||
Create database and `wordpress_data` backups in one maintenance window. Backups contain
|
||||
credentials, accounts, contact information, and uploaded media; encrypt them, restrict
|
||||
access, set retention, and store copies off-host.
|
||||
Create database and WordPress file backups in one maintenance window. The default host
|
||||
paths are `./runtime/db`, `./runtime/wordpress`, `./runtime/letsencrypt`, and
|
||||
`./runtime/certbot/www`, unless overridden in `.env`. Backups contain credentials,
|
||||
accounts, contact information, and uploaded media; encrypt them, restrict access, set
|
||||
retention, and store copies off-host.
|
||||
|
||||
A restore is destructive. Validate it on isolated volumes, then stop WordPress, restore
|
||||
the database and file volume together, restart, and verify the homepage, media,
|
||||
@@ -34,7 +37,10 @@ the database and file volume together, restart, and verify the homepage, media,
|
||||
|
||||
## Known risks
|
||||
|
||||
- Local named volumes are not backups.
|
||||
- Local host directories are not backups.
|
||||
- A host-based volume may be unreadable by the application if created with the wrong
|
||||
owner or mode; run `./scripts/prepare-host-volumes.sh` after changing paths or image
|
||||
user IDs.
|
||||
- SMTP is not configured; WordPress password-reset email needs an external mail service.
|
||||
- Admin MFA and network allowlisting are deployment concerns and are not bundled.
|
||||
- WordPress plugins expand the attack surface; install only reviewed, maintained,
|
||||
|
||||
+11
-3
@@ -14,10 +14,17 @@
|
||||
|
||||
## Images or theme are missing
|
||||
|
||||
1. Confirm `wordpress_data` is mounted in both WordPress and WP-CLI.
|
||||
1. Confirm `WORDPRESS_DATA_PATH` is mounted in both WordPress and WP-CLI.
|
||||
2. Run `docker compose --profile tools run --rm wp-cli -c 'wp theme status azionelab'`.
|
||||
3. Verify file ownership before changing permissions; never make the tree world-writable.
|
||||
|
||||
## A service cannot write to its volume
|
||||
|
||||
1. Stop the affected service.
|
||||
2. Confirm the relevant `.env` path points to the intended host directory.
|
||||
3. Run `./scripts/prepare-host-volumes.sh`.
|
||||
4. Start the service and inspect logs without printing secret values.
|
||||
|
||||
## Certificate issuance fails
|
||||
|
||||
Verify public DNS, inbound port 80, the operator email, and staging mode. Request a
|
||||
@@ -26,5 +33,6 @@ Avoid repeated production-CA retries while debugging.
|
||||
|
||||
## Rollback
|
||||
|
||||
Revert the deployment commit and rebuild while preserving all named volumes. Restore
|
||||
database/files only for a data rollback and only from a verified coordinated backup.
|
||||
Revert the deployment commit and rebuild while preserving all host data directories.
|
||||
Restore database/files only for a data rollback and only from a verified coordinated
|
||||
backup.
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
- Containers are not privileged and do not use host networking or the Docker socket.
|
||||
WordPress/Apache retains the capabilities needed by the official image internally,
|
||||
but no WordPress port is published.
|
||||
- Persistent state uses host-based bind mounts. Keep those paths outside the public web
|
||||
root, restrict host access, never make them world-writable, and run
|
||||
`./scripts/prepare-host-volumes.sh` when paths or image user IDs change.
|
||||
- WordPress/Apache access logs are disabled to avoid duplicate client metadata and
|
||||
healthcheck noise; NGINX remains the single request log and PHP warnings/errors stay
|
||||
visible.
|
||||
|
||||
+6
-5
@@ -14,11 +14,12 @@ docker compose config --quiet
|
||||
LETSENCRYPT_ENABLED=1 docker compose config --quiet
|
||||
```
|
||||
|
||||
The override replaces normal database and WordPress volumes, maps `azionelab.org` to
|
||||
the internal proxy, and never publishes an extra port. Browser tests cover content and
|
||||
section order, contact actions, mobile overflow/navigation, semantic landmarks, image
|
||||
alternatives, admin routing, security headers, blocked sensitive routes, and unknown
|
||||
virtual hosts.
|
||||
The normal stack uses host-based bind mounts under `./runtime` by default. The test
|
||||
override replaces database and WordPress state with isolated Docker volumes, maps
|
||||
`azionelab.org` to the internal proxy, and never publishes an extra port. Browser tests
|
||||
cover content and section order, contact actions, mobile overflow/navigation, semantic
|
||||
landmarks, image alternatives, admin routing, security headers, blocked sensitive
|
||||
routes, and unknown virtual hosts.
|
||||
|
||||
Subjective visual review and a real-device accessibility audit remain manual release
|
||||
checks.
|
||||
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
ENV_FILE="${ENV_FILE:-$ROOT_DIR/.env}"
|
||||
|
||||
DB_DATA_PATH="${DB_DATA_PATH:-./runtime/db}"
|
||||
WORDPRESS_DATA_PATH="${WORDPRESS_DATA_PATH:-./runtime/wordpress}"
|
||||
LETSENCRYPT_DATA_PATH="${LETSENCRYPT_DATA_PATH:-./runtime/letsencrypt}"
|
||||
CERTBOT_CHALLENGES_PATH="${CERTBOT_CHALLENGES_PATH:-./runtime/certbot/www}"
|
||||
|
||||
MARIADB_VOLUME_UID="${MARIADB_VOLUME_UID:-999}"
|
||||
MARIADB_VOLUME_GID="${MARIADB_VOLUME_GID:-999}"
|
||||
WORDPRESS_VOLUME_UID="${WORDPRESS_VOLUME_UID:-33}"
|
||||
WORDPRESS_VOLUME_GID="${WORDPRESS_VOLUME_GID:-33}"
|
||||
CERTBOT_VOLUME_UID="${CERTBOT_VOLUME_UID:-0}"
|
||||
CERTBOT_VOLUME_GID="${CERTBOT_VOLUME_GID:-0}"
|
||||
|
||||
load_env_file() {
|
||||
[ -f "$ENV_FILE" ] || return 0
|
||||
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
case "$line" in
|
||||
"" | \#*) continue ;;
|
||||
esac
|
||||
|
||||
key="${line%%=*}"
|
||||
value="${line#*=}"
|
||||
value="${value%\"}"
|
||||
value="${value#\"}"
|
||||
value="${value%\'}"
|
||||
value="${value#\'}"
|
||||
|
||||
case "$key" in
|
||||
DB_DATA_PATH) DB_DATA_PATH="$value" ;;
|
||||
WORDPRESS_DATA_PATH) WORDPRESS_DATA_PATH="$value" ;;
|
||||
LETSENCRYPT_DATA_PATH) LETSENCRYPT_DATA_PATH="$value" ;;
|
||||
CERTBOT_CHALLENGES_PATH) CERTBOT_CHALLENGES_PATH="$value" ;;
|
||||
MARIADB_VOLUME_UID) MARIADB_VOLUME_UID="$value" ;;
|
||||
MARIADB_VOLUME_GID) MARIADB_VOLUME_GID="$value" ;;
|
||||
WORDPRESS_VOLUME_UID) WORDPRESS_VOLUME_UID="$value" ;;
|
||||
WORDPRESS_VOLUME_GID) WORDPRESS_VOLUME_GID="$value" ;;
|
||||
CERTBOT_VOLUME_UID) CERTBOT_VOLUME_UID="$value" ;;
|
||||
CERTBOT_VOLUME_GID) CERTBOT_VOLUME_GID="$value" ;;
|
||||
esac
|
||||
done < "$ENV_FILE"
|
||||
}
|
||||
|
||||
resolve_path() {
|
||||
case "$1" in
|
||||
/*) printf '%s\n' "$1" ;;
|
||||
*) printf '%s/%s\n' "$ROOT_DIR" "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
apply_ownership() {
|
||||
path="$1"
|
||||
uid="$2"
|
||||
gid="$3"
|
||||
|
||||
if chown -R "$uid:$gid" "$path" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
echo "WARN: cannot chown $path to $uid:$gid." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
if sudo -n chown -R "$uid:$gid" "$path" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
echo "WARN: sudo cannot chown $path to $uid:$gid without a password." >&2
|
||||
echo "WARN: run this script with sudo if the service cannot write this path." >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "WARN: cannot chown $path to $uid:$gid; rerun as root or install sudo." >&2
|
||||
}
|
||||
|
||||
prepare_dir() {
|
||||
path="$1"
|
||||
uid="$2"
|
||||
gid="$3"
|
||||
mode="$4"
|
||||
|
||||
mkdir -p "$path"
|
||||
apply_ownership "$path" "$uid" "$gid"
|
||||
chmod "$mode" "$path"
|
||||
echo "Prepared $path as $uid:$gid mode $mode"
|
||||
}
|
||||
|
||||
load_env_file
|
||||
|
||||
prepare_dir "$(resolve_path "$DB_DATA_PATH")" "$MARIADB_VOLUME_UID" "$MARIADB_VOLUME_GID" 750
|
||||
prepare_dir "$(resolve_path "$WORDPRESS_DATA_PATH")" "$WORDPRESS_VOLUME_UID" "$WORDPRESS_VOLUME_GID" 755
|
||||
prepare_dir "$(resolve_path "$LETSENCRYPT_DATA_PATH")" "$CERTBOT_VOLUME_UID" "$CERTBOT_VOLUME_GID" 750
|
||||
prepare_dir "$(resolve_path "$CERTBOT_CHALLENGES_PATH")" "$CERTBOT_VOLUME_UID" "$CERTBOT_VOLUME_GID" 755
|
||||
Reference in New Issue
Block a user