Storefront data cache
Every cached read the reference storefront makes through the port becomes an entry of the Next.js Data Cache, keyed by its URL — product pages, listings, and also filtered listings and search. Those stay cached on purpose: sending every filter combination a crawler can produce to the kernel is exactly what the cache is there to avoid.
Since v0.3.3 that cache has a ceiling and lives in a volume, because without either it grew in the container’s writable layer until the disk — the database’s disk — was full.
Configure
Section titled “Configure”Set on the storefront service:
| Variable | Default | What it does |
|---|---|---|
FORGE_DATA_CACHE_DIR | .next/cache/forge-data-cache | The directory. The template fixes it at /var/cache/forge-storefront, where the storefront_cache volume is mounted. |
FORGE_DATA_CACHE_MAX_MB | 2048 | The ceiling, in MiB. The least recently used entries are evicted before each write that would cross it. |
FORGE_DATA_CACHE_MIN_FREE_PERCENT | 5 | Below this share of free disk — or below 1 GiB free, whichever is larger — the cache stops writing and says so once in the log. |
Empty means the default. The full list of variables is the configuration reference.
The ceiling is per process
Section titled “The ceiling is per process”⚠️ FORGE_DATA_CACHE_MAX_MB bounds one storefront process, not the box. Each process keeps its own index of
what it wrote and evicts against that. Run three storefronts — a second brand, a second theme, a replica — and
the cache can reach three times the ceiling: 3 × 2 GiB with the defaults.
- Size the disk for the sum, or lower the ceiling on each front.
- Give each front its own volume. Two processes writing one directory each count only their own entries, so together they can exceed the ceiling either of them believes it is keeping.
- The disk guard (
FORGE_DATA_CACHE_MIN_FREE_PERCENT) is the backstop that does see the whole disk: every process stops writing when the filesystem is nearly full.
The checkout image does not install this handler. The reads it caches (product names and photos in the summary, payment badges) stay in Next’s own cache inside its container.
force-dynamic does not keep a page out of it
Section titled “force-dynamic does not keep a page out of it”A route marked export const dynamic = 'force-dynamic' is rendered on every request, but its fetches can still
be written to the Data Cache. Next applies “don’t cache” under force-dynamic only to a fetch that states no
caching of its own; a fetch that declares next: { revalidate } — every cached read of
@forgeco/storefront-kit does — is cached and counted against the ceiling like any other. To keep a read out
of the cache, the fetch itself has to say so (cache: 'no-store').
How the kernel purges it
Section titled “How the kernel purges it”When a write changes what the storefront shows (a product published, a price changed, a block placed), the
kernel tells the storefront which cache tags to drop. Two variables on the kernel service turn that on:
FORGE_STOREFRONT_URL— the storefront origins, as the kernel reaches them. A comma-separated list: one origin, or several (http://storefront:3000,http://storefront-b:3000). Every listed front receives every purge.FORGE_REVALIDATE_SECRET— one secret, the same on the kernel and on every listed front.
So every front that keeps a cache of its own must be in that list, must answer the purge, and must hold the same secret. A front left out serves what it cached until the entry’s window passes — and a cached “not found” never recovers on its own.
The admin has its own FORGE_STOREFRONT_URL (also a list) for the purge it makes right after an operator saves;
the template sets it to the one storefront it ships, so add your other fronts there too.
What a front has to answer. POST <origin>/api/revalidate?tag=<tag>[&tag=<tag>…] with the header
x-revalidate-secret: <FORGE_REVALIDATE_SECRET>; drop each tag (revalidateTag) and answer 200. The reference
storefront has exactly that route; a storefront of your own needs it too.
How to check. The kernel says which case it is in, once, at boot:
storefront cache invalidation ON — 2 target(s): http://storefront:3000, http://storefront-b:3000storefront cache invalidation OFF — set FORGE_STOREFRONT_URL … + FORGE_REVALIDATE_SECRET to enable …⚠️ storefront cache invalidation MISCONFIGURED — FORGE_STOREFRONT_URL names 1 target(s) … but FORGE_REVALIDATE_SECRET is empty …docker compose logs kernel | grep 'storefront cache invalidation'A comma lost in an env file shows up here as one target fewer than you meant.
⚠️ Replicas behind one address are not all purged. A purge reaches the process that answers the request, so
of N replicas behind one origin, one is purged and the rest live on their window (FORGE_REVALIDATE_SECONDS,
one hour by default). List fronts that are different by name; replicas of one front need an edge purge.
What it says in the log
Section titled “What it says in the log”- On the first write of each process:
[forge-data-cache] writing to <dir>: <n> MiB in <k> entries, ceiling <t> MiB. - Disk nearly full, once:
[forge-data-cache] disk almost full (…): writes to <dir> are paused; … - When there is room again, once:
[forge-data-cache] disk has room again: writes to <dir> resumed. - A directory it cannot open:
[forge-data-cache] <dir> could not be opened (…); serving without a data cache.The storefront keeps serving, uncached.
How much it holds:
docker compose exec storefront du -sh /var/cache/forge-storefrontClearing it by hand
Section titled “Clearing it by hand”The cache is disposable: everything in it comes back from the kernel on the next visit. The first visit to each page is slower, and the kernel takes that wave of reads — prefer a quiet hour.
docker compose stop storefrontdocker compose run --rm --no-deps --entrypoint sh storefront -c 'rm -rf /var/cache/forge-storefront/*'docker compose start storefront# or, with the storefront stopped, remove the whole volume (it comes back empty on the next `up`):# docker volume rm <project>_storefront_cacheDo it after a long rollback: the volume survives a change of version, and the newer front serves what the older one cached.
A storefront of your own
Section titled “A storefront of your own”The handler ships in the kit, as @forgeco/storefront-kit/data-cache-handler. A storefront cut from the
reference already has the two pieces that wire it — keep them. A storefront written from scratch adds them:
// data-cache-handler.mjs, beside next.config.mjs — a file of the app, because Next imports it untranspiled// at server start, and the app's directory is the one that reaches `next` inside a standalone image.import { createRequire } from 'node:module';import { withPagesCache } from '@forgeco/storefront-kit/data-cache-handler';
const fileSystemCache = createRequire(import.meta.url)( 'next/dist/server/lib/incremental-cache/file-system-cache.js',);
export default withPagesCache(fileSystemCache.default ?? fileSystemCache);import { fileURLToPath } from 'node:url';
export default { cacheHandler: fileURLToPath(new URL('./data-cache-handler.mjs', import.meta.url)), // …};Then point FORGE_DATA_CACHE_DIR at a volume of the container. Without the handler, your front keeps Next’s
built-in cache: one file per entry, never deleted, in the container layer.
See also
Section titled “See also”- Volumes & backups —
storefront_cacheis disposable; never back it up. - Disk alarm — the kernel warns before the disk fills, whatever fills it.