User story
As a team member, I want the public-facing tools (carousel generator, YouTube captions extractor, landing page) deployed to a free hosting platform, so that external users can access them without running the app locally.
As a developer making future changes, I want a recorded architecture decision explaining why the platform was chosen, so that the tradeoffs are understood and the decision can be revisited if requirements change.
Background
The following routes are ready to deploy publicly once Issues #76, #77, #78, #79, and #80 are merged:
/ — landing page
/about — about page
/carousel — carousel generator (subscriber-gated)
/get-youtube-captions — YouTube captions extractor (subscriber-gated)
The critical deployment constraint is Puppeteer. The carousel generator (/api/generate-carousel, /api/extract-captions) launches a headless Chromium browser via Puppeteer. This rules out standard serverless platforms (Vercel, Netlify) unless the carousel generation is offloaded to a separate worker service. A container host running Dockerfile.public (see below) is the path of least resistance.
The existing Dockerfile must not be used for public deployment. It includes Python 3.12, PyTorch, whisperx, pyannote, ffmpeg, and other ML dependencies needed only for the internal video pipeline. Issue #83 is restructuring that Dockerfile as the NVIDIA/Linux internal pipeline image. This issue creates a completely separate Dockerfile.public — a slim Node.js + Chromium image with no ML stack — specifically for the public deployment. The two Dockerfiles are independent artifacts serving different purposes.
An ADR must be written and decided before any deployment work begins.
Acceptance criteria
ADR
Given the ADR has been written
When a developer reads docs/adr/ADR-001-deployment-platform.md
Then they understand the evaluated options, the decision drivers, the chosen platform, and the tradeoffs accepted
Deployment
Given the ADR decision has been made and the platform is configured
When a user visits the public domain
Then the landing page loads and the carousel and captions tools are accessible behind the subscriber email gate
Given the deployment is live
When a user submits a carousel generation request
Then it completes successfully (Puppeteer runs and returns base64 images)
Given the deployment is live
When the RESEND_API_KEY, SUBSCRIBER_COOKIE_SECRET, RESEND_FROM_EMAIL, RESEND_SEGMENT_ID, and RESEND_TOPICS env vars are set in the platform dashboard
Then the subscriber gate works end-to-end
Slim Dockerfile
Given Dockerfile.public is created
When it is built with docker build -f Dockerfile.public .
Then the image contains Node.js and Chromium but NOT Python, PyTorch, ffmpeg, or any ML model dependencies
Out of scope
Technical context
Public feature runtime requirements
| Feature |
Requires |
| Landing, about |
Node.js, Next.js SSR |
/carousel, /get-youtube-captions |
Node.js, Next.js SSR |
/api/generate-carousel |
Node.js, Chromium (Puppeteer), Sharp |
/api/extract-captions, /api/transcribe |
Node.js, outbound HTTP (YouTube) |
/api/auth/subscribe |
Node.js, outbound HTTP (Resend) |
Minimum container spec: ~512 MB RAM (Puppeteer peak usage), 1 shared CPU, persistent (not serverless).
Platforms to evaluate in the ADR
| Platform |
Docker support |
Puppeteer |
Free tier |
Cold starts |
Notes |
| Fly.io |
✅ |
✅ |
✅ (3 shared VMs, 256 MB each) |
None |
May need to bump to 512 MB; worldwide edge |
| Railway |
✅ |
✅ |
✅ ($5/month credit) |
None |
Good DX; credit may run out |
| Render |
✅ |
✅ |
✅ (web service, spins down after 15 min) |
Yes (cold start on first request) |
Free tier has 15-min sleep |
| Google Cloud Run |
✅ |
✅ |
✅ (2M req/month, 400K GB-sec) |
Configurable min-instances |
Generous free tier; min-instances=0 causes cold starts |
| Koyeb |
✅ |
✅ |
✅ (2 nano instances, 512 MB) |
None |
Less established |
| Vercel (free) |
❌ |
❌ |
✅ |
N/A |
Serverless; Puppeteer requires separate worker — significant extra work |
| Netlify (free) |
❌ |
❌ |
✅ |
N/A |
Same constraint as Vercel |
Environment variables required on the platform
From .env.example (after Issue #80):
RESEND_API_KEY
RESEND_FROM_EMAIL
RESEND_SEGMENT_ID
RESEND_TOPICS
SUBSCRIBER_COOKIE_SECRET
Do NOT set ANTHROPIC_API_KEY or INTERNAL_API_SECRET on the public deployment. Auto-carousel (the only feature requiring ANTHROPIC_API_KEY) is removed in Issue #81.
Dockerfile.public
Write Dockerfile.public from scratch as a minimal image — do not derive it from the internal Dockerfile, which is being restructured by Issue #83 for the NVIDIA/Linux pipeline path. The only shared content is the Chromium system library block (apt-get dependencies for headless Chrome), which can be copied but is stable and well-understood.
Dockerfile.public must include:
- Node.js 20.x
- Chromium + Puppeteer system libraries:
chromium libasound2 libatk-bridge2.0-0 libatspi2.0-0 libcups2 libdrm2 libgbm1 libgtk-3-0 libnspr4 libnss3 libx11-xcb1 libxcomposite1 libxdamage1 libxfixes3 libxkbcommon0 libxrandr2
PUPPETEER_SKIP_DOWNLOAD=true and PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
- npm install +
next build
Dockerfile.public must NOT include:
- Python or any pip dependencies
- PyTorch, whisperx, pyannote, faster-whisper, rembg
- ffmpeg
- Any Remotion or video pipeline dependencies
Implementation details
-
Write ADR at docs/adr/ADR-001-deployment-platform.md using the format:
- Title, Status (Proposed → Accepted), Date
- Context (Puppeteer constraint, free tier preference, single-container requirement)
- Decision drivers (free tier, no cold starts on carousel, Puppeteer support, DX)
- Considered options (table above with pros/cons)
- Decision (chosen platform + rationale)
- Consequences (what gets easier, what gets harder, what to revisit if traffic grows)
-
Create Dockerfile.public — slim Node.js + Chromium image written from scratch (see spec above)
-
Configure platform — create app/service on chosen platform; point to Dockerfile.public; set all required env vars from the platform dashboard
-
Verify deployment — smoke test: load landing page, submit an email on /carousel, generate a carousel, download an image
-
Update README.md or create docs/DEPLOYMENT.md with the platform URL, how to redeploy, and where env vars are managed
Additional test scenarios
- Carousel generation completes within the platform's request timeout (verify timeout is ≥ 60 s; Puppeteer can take 30–45 s on a cold container)
- Subscriber email gate works end-to-end on the live domain (email → Resend contact created → cookie set → tool accessible)
/editor and /camera routes are inaccessible on the public deployment (they write to local filesystem — they will 500 or 404 without INTERNAL_API_SECRET, which is acceptable)
Hard constraints
- Platform must be free tier (no credit card required beyond signup, or has a perpetual free tier)
- Puppeteer must run server-side without a separate external browser service
INTERNAL_API_SECRET and ANTHROPIC_API_KEY must NOT be set in the public deployment environment
- The ADR must be written and committed before any platform configuration begins
Dockerfile.public must be written independently of the internal Dockerfile — do not import or extend it
Dependency issues
User story
As a team member, I want the public-facing tools (carousel generator, YouTube captions extractor, landing page) deployed to a free hosting platform, so that external users can access them without running the app locally.
As a developer making future changes, I want a recorded architecture decision explaining why the platform was chosen, so that the tradeoffs are understood and the decision can be revisited if requirements change.
Background
The following routes are ready to deploy publicly once Issues #76, #77, #78, #79, and #80 are merged:
/— landing page/about— about page/carousel— carousel generator (subscriber-gated)/get-youtube-captions— YouTube captions extractor (subscriber-gated)The critical deployment constraint is Puppeteer. The carousel generator (
/api/generate-carousel,/api/extract-captions) launches a headless Chromium browser via Puppeteer. This rules out standard serverless platforms (Vercel, Netlify) unless the carousel generation is offloaded to a separate worker service. A container host runningDockerfile.public(see below) is the path of least resistance.The existing
Dockerfilemust not be used for public deployment. It includes Python 3.12, PyTorch, whisperx, pyannote, ffmpeg, and other ML dependencies needed only for the internal video pipeline. Issue #83 is restructuring that Dockerfile as the NVIDIA/Linux internal pipeline image. This issue creates a completely separateDockerfile.public— a slim Node.js + Chromium image with no ML stack — specifically for the public deployment. The two Dockerfiles are independent artifacts serving different purposes.An ADR must be written and decided before any deployment work begins.
Acceptance criteria
ADR
Given the ADR has been written
When a developer reads
docs/adr/ADR-001-deployment-platform.mdThen they understand the evaluated options, the decision drivers, the chosen platform, and the tradeoffs accepted
Deployment
Given the ADR decision has been made and the platform is configured
When a user visits the public domain
Then the landing page loads and the carousel and captions tools are accessible behind the subscriber email gate
Given the deployment is live
When a user submits a carousel generation request
Then it completes successfully (Puppeteer runs and returns base64 images)
Given the deployment is live
When the RESEND_API_KEY, SUBSCRIBER_COOKIE_SECRET, RESEND_FROM_EMAIL, RESEND_SEGMENT_ID, and RESEND_TOPICS env vars are set in the platform dashboard
Then the subscriber gate works end-to-end
Slim Dockerfile
Given
Dockerfile.publicis createdWhen it is built with
docker build -f Dockerfile.public .Then the image contains Node.js and Chromium but NOT Python, PyTorch, ffmpeg, or any ML model dependencies
Out of scope
Dockerfileordocker-compose.yml— those are owned by Issue refactor(pipeline): replace docker-compose with native macOS execution to enable Metal and VideoToolbox #83Technical context
Public feature runtime requirements
/carousel,/get-youtube-captions/api/generate-carousel/api/extract-captions,/api/transcribe/api/auth/subscribeMinimum container spec: ~512 MB RAM (Puppeteer peak usage), 1 shared CPU, persistent (not serverless).
Platforms to evaluate in the ADR
Environment variables required on the platform
From
.env.example(after Issue #80):Do NOT set
ANTHROPIC_API_KEYorINTERNAL_API_SECRETon the public deployment. Auto-carousel (the only feature requiringANTHROPIC_API_KEY) is removed in Issue #81.Dockerfile.public
Write
Dockerfile.publicfrom scratch as a minimal image — do not derive it from the internalDockerfile, which is being restructured by Issue #83 for the NVIDIA/Linux pipeline path. The only shared content is the Chromium system library block (apt-get dependencies for headless Chrome), which can be copied but is stable and well-understood.Dockerfile.publicmust include:chromium libasound2 libatk-bridge2.0-0 libatspi2.0-0 libcups2 libdrm2 libgbm1 libgtk-3-0 libnspr4 libnss3 libx11-xcb1 libxcomposite1 libxdamage1 libxfixes3 libxkbcommon0 libxrandr2PUPPETEER_SKIP_DOWNLOAD=trueandPUPPETEER_EXECUTABLE_PATH=/usr/bin/chromiumnext buildDockerfile.publicmust NOT include:Implementation details
Write ADR at
docs/adr/ADR-001-deployment-platform.mdusing the format:Create
Dockerfile.public— slim Node.js + Chromium image written from scratch (see spec above)Configure platform — create app/service on chosen platform; point to
Dockerfile.public; set all required env vars from the platform dashboardVerify deployment — smoke test: load landing page, submit an email on
/carousel, generate a carousel, download an imageUpdate
README.mdor createdocs/DEPLOYMENT.mdwith the platform URL, how to redeploy, and where env vars are managedAdditional test scenarios
/editorand/cameraroutes are inaccessible on the public deployment (they write to local filesystem — they will 500 or 404 withoutINTERNAL_API_SECRET, which is acceptable)Hard constraints
INTERNAL_API_SECRETandANTHROPIC_API_KEYmust NOT be set in the public deployment environmentDockerfile.publicmust be written independently of the internalDockerfile— do not import or extend itDependency issues
Dockerfile.publicis a separate artifact from the internalDockerfilethat refactor(pipeline): replace docker-compose with native macOS execution to enable Metal and VideoToolbox #83 restructures; however, if refactor(pipeline): replace docker-compose with native macOS execution to enable Metal and VideoToolbox #83 lands first, note that the internalDockerfilewill have changed andDockerfile.publicmust still be written independently