ESC
> PressHaven alpha
Home › Docs ›Engine › Honeypot Engine

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.

  1. Assign. A new session asks Redis for a free ready pool. The assignment is a single atomic EVAL so two sessions can never receive the same exclusive pool.
  2. 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.
  3. 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.
  4. Reuse at the ceiling. Once the pool manager hits POOL_MAX or a resource guard, further sessions share existing ready pools round-robin instead of blocking.
  5. 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.
  6. 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.
Isolation model

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/.

ArtifactPurpose
catalog.pySingle source of truth: categories, 26 products, copy, variations and reviews.
art.py, build_assets.pyQPainter 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.phpStatic pages and journal posts authored into WordPress.
scripts/seed_storefront_content.phpVersion-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 .env and 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

VariableDefaultEffect
POOL_SPARES1Unowned pools kept ready on top of active assignments.
POOL_MAX10Hard cap on the total number of pools.
POOL_MAX_MEMORY_MB12288Memory budget all pools may reserve. 0 disables the budget.
POOL_MAX_CPUS12CPU budget all pools may reserve.
POOL_PARALLEL_BUILDS2Pools built at the same time while sessions wait.
POOL_RESERVE_MEM_MB, POOL_RESERVE_CPUScontainer limitsWhat each pool is charged against the budget. Lower toward measured use to fit more pools.
POOL_MIN_FREE_MEM_MB2560 / 1024Host free-memory floor for the WordPress / database layer.
POOL_MAX_LOAD_PER_CPU1.5Load guard; no new pool is built above it.
POOL_IDLE_TIMEOUT_SECONDS1800Idle window before scale-down. Values below 600 are raised to 600 because nginx caches a session's pool for 300 seconds.
POOL_WAITING_ACTIVE_SECONDS60How long a waiting session still counts as demand. Prevents abandoned sessions from consuming new pools.
POOL_FAST_CLONE0Experimental: clone the configured production database and skip slow per-pool wp-cli setup steps.
Size the reserve to measured use

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.