Repository navigation
Prep for single-host deploy: S3-compatible image store, droplet stack, ffmpeg gating - #147
Merged
Merged
Conversation
…, ffmpeg gating Groundwork for moving the API off AWS (ALB + Fargate + RDS, ~$60/mo) onto one DigitalOcean droplet, without changing behaviour on the current ECS deployment — every new setting defaults to today's AWS path. Image storage (internal/s3, config): the bucket can now be any S3-compatible store. S3_ENDPOINT selects it (Cloudflare R2 in production), S3_REGION and S3_ACCESS_KEY_ID/SECRET give it its own credentials (falling back to AWS_*, which SES keeps using), and S3_PUBLIC_URL is the base stored image URLs are built from — so the database stops referencing the storage host. Key derivation understands both the new host and the legacy amazonaws.com shapes still present in old rows. The client is built once per process instead of per call. The MCP widget CSP allows the public image origin. Deploy stack (deploy/): docker-compose (Caddy + api + pgvector/pg16), Caddyfile (Cloudflare Origin CA cert, edge ranges as trusted proxies, SSE flushing, 32 MB bodies), scripts for host bootstrap, rolling a tag, and 6-hourly pg_dump → R2 backups with ntfy alerts, plus the migration runbook. CI (deploy.yml): the image is always pushed to GHCR; the DEPLOY_TARGET repository variable picks ECS (default, unchanged) or the droplet (scp stack files, run deploy.sh, verify the running digest and public /ping). Video import (internal/video): the only CPU-bound work in the API. ffmpeg now runs under nice -n 19 with a single decoder thread, and a sampler-wide slot serializes downloads/ffmpeg so a burst of imports cannot starve the API or Postgres on a shared 2-core host. The FrameSampler interface is untouched. Also: pgvector image for the dev compose (plain postgres could not CREATE EXTENSION vector), a .dockerignore, docs, and the vestigial configs/config.json removed. go mod tidy only relabels direct/indirect requires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The AWS bill is ~$60/mo and ~$45 of it is plumbing for a workload that averages 0.1% of a quarter-vCPU: five public IPv4s ($18.61), an ALB in front of one Fargate task ($16.74), and a t4g.micro RDS ($14.68). The whole thing fits on one $18 DigitalOcean droplet. This PR is the backward-compatible prep — nothing changes on the current ECS deployment (every new setting defaults to today's AWS path), so it can merge and deploy before the cutover.
Full plan + runbook:
deploy/README.md.What
Image storage → any S3-compatible bucket (
internal/s3,internal/config)S3_ENDPOINT(R2/Spaces/MinIO; empty = AWS S3),S3_REGION,S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY(fall back toAWS_*, which SES keeps using),S3_PUBLIC_URL— stored image URLs are built from it, so the DB stops referencing the storage host.S3KeyFromURL/RecipeImageKeyFromURLtake the config and understand both the public host and the legacyamazonaws.comshapes (old rows) — the amazonaws branch goes away in the cleanup PR after cutover.resourceDomainsgets the public image origin.Deploy stack (
deploy/) — compose (Caddy + api +pgvector/pgvector:pg16), Caddyfile (Cloudflare Origin CA cert, CF edge ranges as trusted proxies for real client IPs, SSE flushing, 32 MB bodies),host-setup.sh/deploy.sh/backup.sh(6-hourlypg_dump→ R2, ntfy on failure),.env.example, and the rehearsal/cutover/teardown runbook.CI (
deploy.yml) — image always pushed to GHCR; repository variableDEPLOY_TARGETselectsecs(default, unchanged flow) ordroplet(scp stack files →deploy.sh <sha>→ verify running digest + public/ping).Video import burst-proofing (
internal/video) — ffmpeg is the only CPU-bound work in the API. It now runsnice -n 19with-threads 1, and a sampler-wide slot serializes download+ffmpeg so simultaneous imports can't starve the API/Postgres on a shared host. Only the seconds of local work are gated — no slot is held during model calls.FrameSamplerinterface untouched.Also — dev compose uses
pgvector/pgvector:pg17(plainpostgres:17.2can'tCREATE EXTENSION vector),.dockerignore(the 40 MBapibinary and.gitwere in the build context), docs, vestigialconfigs/config.jsonremoved,go mod tidy(relabels only).Verification
go vet ./... && go test ./... -count=1green (offline); new tests: public-URL key round-trip incl. escaped segments, legacy URL fallback, CSP domains, ffmpeg argv, and a race-checked test proving a second video job waits for the first's ffmpeg (and gives up on ctx cancel).caddy validatepasses on the Caddyfile (with a throwaway cert);docker compose configpasses..dockerignore;nice+ffmpegconfirmed inside it.S3_*vars set,newS3Clientreceives the same region/creds as before andUploadRecipeImageToS3still returnsresult.Location.