The first publish failed partway through the push with 413 Payload Too Large: one layer was bigger than the proxy in front of the registry would accept. Three changes, only one of which is that fix. Keep deployment out of a public repo. The registry, image name and credentials now come from repository variables and secrets rather than being written down here, and the docs describe how to run the thing rather than where one particular instance runs. PUBLIC_ORIGIN defaults to localhost. The 413 is a proxy limit, so the fix is pointing REGISTRY at a host the runner reaches directly; the workflow explains itself if that host is plain HTTP and the builder's daemon has not been told to allow it. Publish on version tags. A tag like 1.2.3 publishes :1.2.3, :1.2, :1 and :latest; a prerelease publishes only its exact version and leaves :latest alone. Pushes to main publish :main and :sha-<short> and no longer move :latest, so what is deployed moves when a release says so. Shrink the image from over 1.2GB to 353MB. The Playwright base image carries Firefox and WebKit, which this never launches. Installing just the browser it does launch onto a slim Node base drops two thirds of the weight, which is worth having on a Raspberry Pi even though it does not get any single layer under a proxy limit. That last change surfaced something worth naming: a headless launch resolves to Playwright's headless shell, not the full browser, so that is what every test so far has actually been running. The image now installs exactly that binary and pool.ts names the channel, so the two cannot drift apart. Verified in the container: Bluesky, Instagram, X and Threads all resolve identically on the slim image. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BGkRmLfiWuJHx6tQ12EELY
55 lines
2.0 KiB
TypeScript
55 lines
2.0 KiB
TypeScript
/**
|
|
* Every knob is an environment variable with a working default, so the
|
|
* container runs correctly with no configuration at all.
|
|
*/
|
|
|
|
function int(name: string, fallback: number): number {
|
|
const raw = process.env[name];
|
|
if (raw === undefined || raw === '') return fallback;
|
|
const parsed = Number.parseInt(raw, 10);
|
|
if (!Number.isFinite(parsed)) {
|
|
throw new Error(`${name} must be an integer, got ${JSON.stringify(raw)}`);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function str(name: string, fallback: string): string {
|
|
const raw = process.env[name];
|
|
return raw === undefined || raw === '' ? fallback : raw;
|
|
}
|
|
|
|
export const config = {
|
|
host: str('HOST', '0.0.0.0'),
|
|
port: int('PORT', 8080),
|
|
logLevel: str('LOG_LEVEL', 'info'),
|
|
|
|
/** Where the persistent Chromium profile lives. Survives restarts so that
|
|
* cookies and dismissed consent banners accumulate the way a real
|
|
* browser's do. */
|
|
profileDir: str('PROFILE_DIR', './profile'),
|
|
|
|
/** Concurrent page loads. Chromium is the memory ceiling on this box, so
|
|
* requests queue rather than opening an unbounded number of tabs. */
|
|
maxConcurrent: int('MAX_CONCURRENT', 2),
|
|
|
|
/** Budget for a single navigation, and for a whole resolve including
|
|
* extraction. */
|
|
navigationTimeoutMs: int('NAVIGATION_TIMEOUT_MS', 20_000),
|
|
resolveTimeoutMs: int('RESOLVE_TIMEOUT_MS', 30_000),
|
|
|
|
/** Resolved posts are cached in memory only. Reloading or hitting back
|
|
* must not drive Chromium again. */
|
|
cacheTtlMs: int('CACHE_TTL_MS', 60 * 60 * 1000),
|
|
cacheMax: int('CACHE_MAX', 200),
|
|
|
|
/** How long a signed /m/ media token stays valid. Long enough to finish
|
|
* watching a video, short enough that a leaked URL goes stale. */
|
|
mediaTokenTtlMs: int('MEDIA_TOKEN_TTL_MS', 6 * 60 * 60 * 1000),
|
|
|
|
/** Where this instance is reachable. Only used to print the rewrite rules
|
|
* on the index page with the right hostname in them. */
|
|
publicOrigin: str('PUBLIC_ORIGIN', 'http://localhost:8080'),
|
|
} as const;
|
|
|
|
export type Config = typeof config;
|