Honeypot Engine
The WordPress layer, pool scaling, per-session stickiness, seeding, content sync and backups
The honeypot engine is the high-interaction deception environment. It is not a mock: every pool is a real WordPress/WooCommerce installation with a real database, real plugins and a real storefront. The proxy decides whether a session sees production or a pool; the engine makes the pool convincing enough that the session stays there.
The engine can run its deception at two different layers — separate decoy backends, or one eshop with a swapped database. This page describes the default reverse-proxy level with per-session pools; see Honeypot Layers for the alternative database level and how to choose.
Engine Layers
Pool Lifecycle
Assignment happens once per session, not per request and not per IP. This is per-session segmentation: visitors are told apart by their session identity (a signed cookie with a passive-fingerprint fallback), so several users behind one address — or one user who clears cookies — are each isolated on a pool of their own. The mapping is stored in Redis with a 24-hour TTL, and the dashboard's Segmentation Validation page proves it by running several independent sessions from one machine and showing each land on a different pool.
- Assign. A new session asks Redis for a free ready pool. The assignment is a single atomic
EVALso two sessions can never receive the same exclusive pool. - Provision a spare. Every assignment queues a wake-up on the pool-provision channel. The pool manager builds another shop/database pair so the next session finds a warm pool.
- Borrow under pressure. If none is free, the session borrows a ready pool round-robin and joins the waiting queue. When its own pool is ready, the binding moves.
- Reuse at the ceiling. Once the pool manager hits
POOL_MAXor a resource guard, further sessions share existing ready pools round-robin instead of blocking. - Stay sticky. Once a session is honeypot-bound, every later request goes to the same pool. Clearing cookies does not reset the binding because the fingerprint fallback recovers the session.
- Retire. After the idle timeout, assignments are dropped and pools above the spare count are removed. Pools declared statically in Compose are freed but never destroyed.
All honeypot containers share one flat network and can reach every honeypot database over TCP. Isolation therefore depends on each pool's MySQL user having a different password. MYSQL_PASSWORD_POOL_2 and MYSQL_PASSWORD_POOL_3 must differ from each other and from MYSQL_PASSWORD; testing/test_pool_isolation.sh verifies the wiring and the live credentials.
Seeding the Storefront
The catalog is generated rather than scraped. The brand, farms and reviewers are invented, and product
photography is procedural illustration produced by the seeding tools. Everything lives under
ids/eshop_seed/.
| Artifact | Purpose |
|---|---|
catalog.py | Single source of truth: categories, 26 products, copy, variations and reviews. |
art.py, build_assets.py | QPainter illustration engine and renderer for the storefront imagery. |
catalog.json, products/, site/ | Rendered, committed output so a fresh clone never needs to rebuild assets. |
pages.php, journal.php | Static pages and journal posts authored into WordPress. |
scripts/seed_storefront_content.php | Version-gated importer mounted into the production seed container. Bump FH_CATALOG_VERSION to re-author an existing install. |
To rebuild assets after editing catalog.py:
QT_QPA_PLATFORM=offscreen ../../dashboard/venv/bin/python build_assets.py
cp site/* ../production_eshop_files/wp-content/themes/omega-storefront/assets/fernhill/img/
Then run docker compose run --rm production_db_seed to author the content into WordPress.
Content Replication
Pools are not frozen at build time. honeypot_content_sync mirrors production posts, postmeta,
terms and attachments into every pool database on an interval controlled by
REPLICATION_INTERVAL_SECONDS (default 300 seconds).
- Copied: posts, postmeta, terms and attachments.
- Not copied: options, comments, nav-menu items and term meta. The storefront therefore keeps reviews in postmeta (
_fh_reviews), builds navigation in code, and derives store settings from option filters in a must-use plugin. - Deletes: pools never delete rows. After changing the catalog, clear a pool's old products or reseed it, then restart the sync service to mirror the new rows immediately.
To reflect an edit immediately instead of waiting for the next interval, use the dashboard's WordPress Studio page: it edits the real shop and runs this same replication script in its single-pool mode to fan the change out to every pool on demand.
Backups
The backup_service container snapshots the production database and the production
WordPress files into ids/backups/. It starts five minutes after boot, runs daily, and keeps
seven days by default (BACKUP_RETENTION_DAYS). Pool data is disposable; production data is not.
- Back up the production database before reseeding or applying migrations; pool data is disposable, production data is not.
- Back up
.envand the certificate material separately from the data, and store them encrypted. - Test a restore into a scratch stack before relying on a backup for recovery.
- Do not copy a live MySQL data directory while the container is running; use the backup service or a dump.
backups/manage_backups.sh (needs jq) lists, labels, moves and restores snapshots:
./backups/manage_backups.sh list
./backups/manage_backups.sh label 2026-08-09_02-00-03_production_database.sql.gz "before WooCommerce 9 upgrade"
./backups/manage_backups.sh export 2026-08-09_02-00-03_production_database.sql.gz ~/offsite/
./backups/manage_backups.sh import ~/offsite/2026-08-09_02-00-03_production_eshop.tar.gz wp
./backups/manage_backups.sh delete 2026-08-09_02-00-03_production_eshop.tar.gz --yes
Pool Configuration
| Variable | Default | Effect |
|---|---|---|
POOL_SPARES | 1 | Unowned pools kept ready on top of active assignments. |
POOL_MAX | 10 | Hard cap on the total number of pools. |
POOL_MAX_MEMORY_MB | 12288 | Memory budget all pools may reserve. 0 disables the budget. |
POOL_MAX_CPUS | 12 | CPU budget all pools may reserve. |
POOL_PARALLEL_BUILDS | 2 | Pools built at the same time while sessions wait. |
POOL_RESERVE_MEM_MB, POOL_RESERVE_CPUS | container limits | What each pool is charged against the budget. Lower toward measured use to fit more pools. |
POOL_MIN_FREE_MEM_MB | 2560 / 1024 | Host free-memory floor for the WordPress / database layer. |
POOL_MAX_LOAD_PER_CPU | 1.5 | Load guard; no new pool is built above it. |
POOL_IDLE_TIMEOUT_SECONDS | 1800 | Idle window before scale-down. Values below 600 are raised to 600 because nginx caches a session's pool for 300 seconds. |
POOL_WAITING_ACTIVE_SECONDS | 60 | How long a waiting session still counts as demand. Prevents abandoned sessions from consuming new pools. |
POOL_FAST_CLONE | 0 | Experimental: clone the configured production database and skip slow per-pool wp-cli setup steps. |
A pool is charged its full container limit (≈1536 MB / 1.5 CPU) against the budget by default, but
a recorded run measured each honeypot pool using only ≈420–430 MB once warm. Charging the limit stopped
scaling at roughly a quarter of the memory budget, so POOL_RESERVE_MEM_MB /
POOL_RESERVE_CPUS exist to lower the charge toward measured use (for example ≈768 MB /
0.75 CPU) and fit far more pools; the host free-memory and load guards still stop growth if the machine
really runs short.
See Operations for the router decision pipeline and the complete environment reference, Architecture for how the pool router sits in the request path, and Validation Results for the segmentation evidence that each session lands on its own pool.