Developer Guide
Restore the production video-export container
Runbook for restoring VideoExportContainer on the Storys.ai production Worker
VideoExportContainer production restore
This runbook covers issue #9. It is a deployment plan, not a declaration that the production binding is currently restored. Production is intentionally Docker-free until a Docker-capable build path has been reviewed and exercised.
Current state and scope
The implementation is still versioned in the repository:
containers/video-export/Dockerfilebuilds the Node 22 Debian image and installs Bun 1.3.12,node-av, and its FFmpeg payload.src/lib/containers/video-export-container.tsdefines theVideoExportContainerDurable Object (port 8080, five-minute idle timeout).src/lib/workflows/sequence-export-workflow.tscalls the container atPOST /export, validatesx-export-metaandcontent-length, and streams the MP4 into R2.src/server.tsmust re-export the Durable Object class so Wrangler includes it in the Worker bundle.
Commit de06b84a removed only the production Container declaration, the
VIDEO_EXPORT_CONTAINER Durable Object binding, and migration v2, because
the production deployment machine had no Docker. The default and env.test
blocks must stay Docker-free. Local development uses VIDEO_EXPORT_DEV_URL
(bun dev:all sets it to http://localhost:8080), and the existing PR-preview
workflow patches the default block and builds a container on an Ubuntu runner.
Preconditions and ownership
Before changing production configuration, the deployment owner must confirm:
- The selected runner has a functioning Docker engine, can build Linux
node:22-bookworm-slimimages, and has outbound access tobun.sh, npm package sources, and the FFmpeg download used bynode-av. - The Cloudflare token can deploy Worker
storys, apply remote D1 migrations, update the production Durable Object/container application, and write the configured R2 bindings. In Cloudflare terms this normally means Workers Scripts edit, D1 edit, R2 edit, and Workers Containers edit permissions on the Builtopia account; verify the exact account-token labels before use. - The account resources are unchanged from the checked-in production config:
Worker
storys, D1storys-prd(122ee867-0478-4c9e-bc84-d718b2f4c1fa), R2storys-public-assets, and R2storys-storage. - The change has passed independent QA. This agent does not have production deployment credentials and must not claim the container is restored.
Build and image smoke test
Run these checks from the repository root at the exact commit that will be deployed. The Dockerfile performs its own frozen install; the root install still catches lockfile or workspace drift before a deployment is attempted.
git rev-parse HEAD
bun install --frozen-lockfile
IMAGE="storys-video-export:$(git rev-parse --short HEAD)"
docker build --pull -t "$IMAGE" ./containers/video-export
docker run --rm -d --name storys-video-export-smoke -p 8787:8080 "$IMAGE"
trap 'docker rm -f storys-video-export-smoke >/dev/null 2>&1 || true' EXIT
curl --fail --silent --show-error http://127.0.0.1:8787/ping
curl --fail --silent --show-error -X POST \
http://127.0.0.1:8787/export \
-H 'content-type: application/json' \
-o /tmp/storys-video-export-smoke.mp4 \
-D /tmp/storys-video-export-smoke.headers \
-d '{"scenes":[{"orderIndex":0,"videoUrl":"https://media.w3.org/2010/05/sintel/trailer.mp4"},{"orderIndex":1,"videoUrl":"https://media.w3.org/2010/05/sintel/trailer.mp4"}],"musicUrl":null,"musicLoudnessGainDb":null}'
grep -F 'content-type: video/mp4' /tmp/storys-video-export-smoke.headers
grep -F 'x-export-meta:' /tmp/storys-video-export-smoke.headers
grep -Eiq '^content-length:[[:space:]]*[1-9][0-9]*([[:space:]]|$)' /tmp/storys-video-export-smoke.headers
test -s /tmp/storys-video-export-smoke.mp4The two copies of the same AVC clip deliberately exercise the supported
transmux path. A mixed codec or mixed-resolution fixture is expected to fail;
that is an application limitation, not a container liveness failure. Do not
publish the image to a separate registry as part of this plan: the
containers[] entry below makes wrangler deploy build and publish the image
through Cloudflare's deployment path.
Production-only Wrangler wiring
After the Docker smoke test and independent review, restore these fields under
env.production in wrangler.jsonc. Do not add them to the default or
env.test blocks:
"containers": [
{
"class_name": "VideoExportContainer",
"image": "./containers/video-export/Dockerfile",
"instance_type": "standard",
"max_instances": 3
}
],
"durable_objects": {
"bindings": [
{ "name": "REALTIME", "class_name": "RealtimeChannel" },
{
"name": "VIDEO_EXPORT_CONTAINER",
"class_name": "VideoExportContainer"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["RealtimeChannel"]
},
{
"tag": "v2",
"new_sqlite_classes": ["VideoExportContainer"]
}
]durable_objects and migrations are not inherited by an environment block,
so the production block must contain both the existing v1 RealtimeChannel
entry and the new v2 class entry. v2 is a Durable Object class migration,
not a D1 SQL migration; it must be applied by the Worker deployment and must
not be removed or renumbered later.
Deployment order
The sequence below is intentionally ordered so a failed image build cannot leave a Worker pointing at an unavailable binding:
Review the MR and confirm the Docker-capable runner, account resources, and token permissions. Keep production deployment ownership with fucheng.
Merge the reviewed configuration to
main.In a clean checkout of that merge, install dependencies and generate Worker types:
bun install --frozen-lockfile bun cf:typegenBuild with the production environment selected. This is required because
@cloudflare/vite-pluginbakes the selected environment intodist/server/wrangler.json; a build withoutCLOUDFLARE_ENV=productionwould select the local placeholder D1 binding:CLOUDFLARE_ENV=production bun run buildFlatten the nested Drizzle migrations, then apply remote D1 migrations to the production binding before deploying the Worker. The canonical project command is equivalent to the two commands shown here:
bun scripts/flatten-migrations.ts bunx wrangler d1 migrations apply DB --env=production --remoteA migration failure must stop the deployment. Wrangler backs up the D1 database and rolls back a failed SQL migration; investigate the error before retrying.
Deploy the Worker and production container from the same build output:
bunx wrangler deploy --env=productionThe repository's
bun run cf:deploy:prdwraps steps 3–6 (type generation, production build, migration flatten/apply, and deploy) for an approved manual run. The deploy must complete the Container image build before the version is treated as live.Record the deployed version and inspect its state. These are the verified Wrangler commands:
bunx wrangler deployments list --name storys --json bunx wrangler deployments status --name storys --jsonConfirm the new version is active, the Worker is
storys, and the production binding view containsVIDEO_EXPORT_CONTAINER. Confirm the Cloudflare Containers application is healthy and/pingreturns200 okbefore attempting an API export.
Production API smoke test
Use a disposable, authenticated API key and a sequence owned by that key's team. The selected videos must form a uniform AVC/transmux-compatible cut. Do not use a customer sequence for the first smoke test.
set -euo pipefail
BASE_URL="${STORYS_PRODUCTION_URL:?set the production hostname for this deploy}"
AUTH_HEADER="Authorization: Bearer ${API_KEY:?set the disposable API key}"
EXPORT_PATH="$BASE_URL/api/v1/sequences/$SEQUENCE_ID/exports"
START_JSON="$(mktemp)"
GET_JSON="$(mktemp)"
START_HEADERS="$(mktemp)"
MP4_HEADERS="$(mktemp)"
MP4_FILE="$(mktemp --suffix=.mp4)"
trap 'rm -f "$START_JSON" "$GET_JSON" "$START_HEADERS" "$MP4_HEADERS" "$MP4_FILE"' EXIT
POST_STATUS="$(curl --fail --silent --show-error -X POST \
"$EXPORT_PATH" \
-H "$AUTH_HEADER" \
-H 'content-type: application/json' \
-d '{}' \
-D "$START_HEADERS" \
-o "$START_JSON" \
-w '%{http_code}')"
test "$POST_STATUS" = 202
jq -e '.export.status == "processing" and (.export.id | type == "string")' "$START_JSON" >/dev/null
EXPORT_ID="$(jq -er '.export.id' "$START_JSON")"
DEADLINE="$(( $(date +%s) + 1800 ))"
while :; do
GET_STATUS="$(curl --fail --silent --show-error \
"$EXPORT_PATH" \
-H "$AUTH_HEADER" \
-o "$GET_JSON" \
-w '%{http_code}')"
test "$GET_STATUS" = 200
EXPORT_JSON="$(jq -cer --arg export_id "$EXPORT_ID" \
'[.exports[] | select(.id == $export_id)] | if length == 1 then .[0] else error("export not found") end' \
"$GET_JSON")"
case "$(jq -r '.status' <<<"$EXPORT_JSON")" in
processing)
if [ "$(date +%s)" -ge "$DEADLINE" ]; then
echo 'export did not become ready within 30 minutes' >&2
exit 1
fi
sleep 10
;;
ready)
jq -e '.url | type == "string" and length > 0' <<<"$EXPORT_JSON" >/dev/null
jq -e '.durationSeconds | type == "number" and . > 0' <<<"$EXPORT_JSON" >/dev/null
EXPORT_URL="$(jq -er '.url' <<<"$EXPORT_JSON")"
curl --fail --silent --show-error -L \
"$EXPORT_URL" \
-D "$MP4_HEADERS" \
-o "$MP4_FILE"
grep -Eiq '^content-type:[[:space:]]*video/mp4([;[:space:]]|$)' "$MP4_HEADERS"
test -s "$MP4_FILE"
break
;;
failed)
jq -e '.error | type == "string" and length > 0' <<<"$EXPORT_JSON" >/dev/null
echo 'export workflow failed; use a known uniform AVC sequence and inspect the error above' >&2
jq -r '.error' <<<"$EXPORT_JSON" >&2
exit 1
;;
*)
echo "unexpected export status: $(jq -r '.status' <<<"$EXPORT_JSON")" >&2
exit 1
;;
esac
doneThe POST must return 202 and a processing export. Poll the GET endpoint
until the same export is ready with a non-null URL and positive
durationSeconds; download that URL and verify it is a non-empty MP4. A
failed export with an incompatible fixture does not validate the deployment;
repeat with a known uniform AVC sequence. A 5xx, missing
VIDEO_EXPORT_CONTAINER error, missing x-export-meta, or missing
content-length is an infrastructure/contract failure and blocks sign-off.
Local, test, and preview behavior
- Default/local and
env.testremain withoutcontainers[], theVIDEO_EXPORT_CONTAINERbinding, and migrationv2. This keeps unit tests, e2e, andbun devhermetic and Docker-free. bun dev:allstartscontainers/video-exportwith Bun and setsVIDEO_EXPORT_DEV_URL=http://localhost:8080; this is the local export path.- The PR-preview workflow patches the default config with the Container,
binding, and both migrations, then runs on
ubuntu-latestwith Docker. Its per-PR Worker, D1, workflow names, and Container application must remain isolated from production.
Rollback and failure handling
Before activation
- A failed Docker build, missing
node-av/FFmpeg payload, failed typecheck, or failed D1 migration stops the sequence beforewrangler deploy. - If the Container application cannot be created or becomes unhealthy, keep the previous Worker version serving and fix the runner/configuration. Do not remove the production D1 or R2 resources as a recovery shortcut.
After a Worker version is active
Capture the active and previous version IDs:
bunx wrangler deployments list --name storys --jsonRoll back only the Worker version if the new release causes a regression:
bunx wrangler rollback <previous-version-id> \ --name storys --yes \ --message 'Rollback video-export restore after smoke-test failure'Keep the production source configuration's
v2migration andVIDEO_EXPORT_CONTAINERdeclaration in subsequent deploys. There is no safe reverse migration for a Durable Object class; removing or renumberingv2does not undo the registered class and can strand future deployments. Rolling the Worker code back may temporarily leave the binding unused, which is safe. Do not delete the Container application while in-flight export workflows may still reference it.Re-run the
/pingand authenticated export smoke tests after the rollback, then record the failed version, rollback version, error, and follow-up MR.
The D1 migration and Worker rollback are separate operations: a successful D1
migration is not reversed by wrangler rollback. D1 schema changes must remain
backwards-compatible with the previous Worker version. The v2 DO class
registration is retained permanently once applied.
Handoff and completion criteria
This issue is complete only when the runbook is merged, an independent QA review has confirmed the commands and production-only wiring, and fucheng has performed the Docker-capable deployment plus the live smoke tests. The agent implementing this plan should then report the commit/MR and explicitly request the deployment handoff; it must not claim production restoration without the live binding and export evidence.