Skip to content

Prep for single-host deploy: S3-compatible image store, droplet stack, ffmpeg gating - #147

Merged
windoze95 merged 2 commits into
mainfrom
feat/droplet-migration-prep
Sep 13, 2026
Merged

windoze95 merged 2 commits into
mainfrom
feat/droplet-migration-prep

Conversation

@windoze95

Copy link
Copy Markdown
Owner

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 to AWS_*, which SES keeps using), S3_PUBLIC_URL — stored image URLs are built from it, so the DB stops referencing the storage host.
  • S3KeyFromURL/RecipeImageKeyFromURL take the config and understand both the public host and the legacy amazonaws.com shapes (old rows) — the amazonaws branch goes away in the cleanup PR after cutover.
  • S3 client built once per process (was per call → credential re-resolution on every upload).
  • R2 needs the SDK's CRC checksum trailers off; done only when an endpoint is set.
  • MCP widget CSP resourceDomains gets 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-hourly pg_dump → R2, ntfy on failure), .env.example, and the rehearsal/cutover/teardown runbook.

CI (deploy.yml) — image always pushed to GHCR; repository variable DEPLOY_TARGET selects ecs (default, unchanged flow) or droplet (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 runs nice -n 19 with -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. FrameSampler interface untouched.

Also — dev compose uses pgvector/pgvector:pg17 (plain postgres:17.2 can't CREATE EXTENSION vector), .dockerignore (the 40 MB api binary and .git were in the build context), docs, vestigial configs/config.json removed, go mod tidy (relabels only).

Verification

  • go vet ./... && go test ./... -count=1 green (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 validate passes on the Caddyfile (with a throwaway cert); docker compose config passes.
  • Image builds with the new .dockerignore; nice + ffmpeg confirmed inside it.
  • Behaviour on ECS: with no S3_* vars set, newS3Client receives the same region/creds as before and UploadRecipeImageToS3 still returns result.Location.

windoze95 and others added 2 commits September 12, 2026 22:47
…, 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>
@windoze95
windoze95 merged commit df419cd into main Sep 13, 2026
1 check passed
@windoze95
windoze95 deleted the feat/droplet-migration-prep branch September 13, 2026 03:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant