docker-image-and-first-boot-seed

Self-seeding Docker image: code/data separation and first-boot bootstrap

The YouTeacher Games store is a WordPress/WooCommerce stack shipped as one image that seeds itself. The whole point of the design is a clean split: code lives in the image, data lives in volumes. Rebuild the image to ship code changes; the database and uploaded media are never touched. One and the same image serves local, dev, and prod — the differences between environments are all env-driven.

What the image bakes in (code)

The Dockerfile builds on wordpress:php8.3-apache and adds wp-cli, unzip, and a MySQL client. On top of WP core it bakes, all pinned to exact versions matched to the live store:

  • free WooCommerce plugins pulled from wordpress.org (WooCommerce itself plus the Stripe, PayPal, Mailgun, and Mailchimp integrations),
  • a pinned free theme (Astra),
  • a licensed premium plugin that is not on wordpress.org, bundled directly because it can't be fetched at build time,
  • the project's own code — a custom activator plugin and an Astra child theme.

It also bakes the WP rewrite .htaccess so pretty permalinks (/product/…, /activate/…) work the instant WP core lands in the web root, with no manual step, plus a small PHP ini raising upload/post size, memory, and execution-time limits for admin media and imports.

One subtle detail: the final WORKDIR is reset to /var/www/html. The stock WordPress entrypoint decides whether to copy WP core into the web root by checking for index.php in the current directory; leaving the working directory at wp-content (which has its own index.php) would make it wrongly skip the copy.

The seed (data as a first-boot artifact)

The image also carries a seed so a fresh deployment isn't empty: a SQL dump of the store's content (products, active plugins, test-mode payment config) and the media those products reference. Both are copied to /usr/local/share/ inside the image rather than mounted, because the deployment platform (Coolify) does not bind-mount repo files into service containers — so the database container's own init step can't reach them, and the WordPress container is what imports the seed on first boot.

The entrypoint: idempotent, env-driven bootstrap

store-entrypoint.sh wraps the stock WordPress entrypoint and runs a few steps that are all safe to repeat on every boot:

  1. Refresh code from the image. The base image declares /var/www/html as a volume, which can otherwise let stale code shadow an update. So on every boot the script re-copies plugins and themes from the image's /usr/src/wordpress into the live tree (via a temp-dir swap), making the image the source of truth for code — while leaving the data directories (uploads, the DB) alone.
  2. Wait, then seed an empty DB. A background task waits for WP core files to appear and for the database to actually accept connections (the platform brings the DB up asynchronously). If WordPress isn't installed yet, it imports the baked SQL seed.
  3. Restore baked media once. Because the uploads volume masks any media baked into the image, the script copies the seed media into the volume on first boot, fixes ownership, regenerates the thumbnail sizes the theme wants, and drops a marker file so this stays a one-shot, idempotent step.
  4. Rewrite the seeded site URL. The seed was captured under one URL; if the deployment's configured site-URL env var differs from what's stored, the script does a search-replace across all tables (skipping the guid column) and updates the siteurl/home options, then flushes caches. This is how the same seed adapts to a local, dev, or prod domain.
  5. Flush rewrite rules so plugin routes (e.g. /activate/, /download/) are registered, then hands off to docker-entrypoint.sh apache2-foreground.

The compose topology

docker-compose.yml runs two services: the WordPress container built from this Dockerfile, and MariaDB. There are exactly two named volumes — the database and wp-content/uploads — and /var/www/html is deliberately not a volume, which is what makes a rebuild ship new code without disturbing data.

Configuration comes entirely from deployment env vars: the DB connection, the per-deployment site URL (dev points at the dev domain, prod at the prod domain), credentials for the private R2 bucket the store signs download URLs against, a shared secret the packaging pipeline uses for its build callback, and — importantly — pinned WordPress auth salts. Because /var/www/html isn't a volume, WordPress would otherwise regenerate its salts on every rebuild and log every user out; pinning the salts as env vars keeps login sessions alive across deploys. Both services carry health checks so the platform waits for readiness.

Why this shape

The design answers a specific tension: WordPress mixes code and data in the same tree, and container platforms tend to persist that whole tree as a volume. By baking code into the image, keeping only DB and uploads as volumes, and making the entrypoint reconcile the two on every boot, a code change is just an image rebuild, and a brand-new environment bootstraps itself into a working store from a single seed.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

docker-image-and-first-boot-seed