Deployment
The platform ships as a single Docker image running two processes — the Express backend and the Next.js app — deployed to Azure Container Registry and run on Azure App Service (or any container host).
Runtime shape
docker-start.sh is the entrypoint:
- Sets
STORAGE_ROOT=/app/storage(one directory for all persistent data) andASSET_BASE_URL=http://127.0.0.1:3000(the internal origin the renderer fetches/api/mediafrom) - Creates the storage subfolders and seeds the static music library on first run
- Starts Express on
:3001(internal only) - Starts Next.js on
:3000(exposed), with cwd/app/webso the Remotion bundler resolvesremotion/index.ts
Next.js proxies /api/auth, /api/health, /api/email, /api/svg, /api/settings, and /api/brand to Express (EXPRESS_ORIGIN); everything else is served natively.
graph LR
NET[Internet] -->|:3000| NEXT[Next.js app]
NEXT -->|proxy auth/email/svg/settings/brand| EXP[Express :3001]
NEXT -->|/api/director · /api/media · /api/assets| NEXT
NEXT -->|render via @remotion/renderer| CHROME[headless Chromium]
NEXT --> STORE[(/app/storage)]
EXP --> STORE
Image
The multi-stage Dockerfile:
| Stage | What |
|---|---|
| builder | Installs deps, builds the @dexian/* packages + Express (server/dist/), and runs next build in web/ |
| runtime | Node 20 + Chromium + ffmpeg + fonts (incl. Noto Color Emoji); copies server/dist/, the built web/ (.next, node_modules, remotion/), and docker-start.sh |
Chromium is required by Remotion's headless renderer; the image bundles it plus the fonts renders need.
Storage — one Azure File share
All persistent data lives in one directory, /app/storage, mounted from a single Azure File share. There are no symlinks and no scattered v1 roots — the v2 editor uses STORAGE_ROOT, and the Express backend derives its *_DIR paths from the same root (server/src/paths.ts). Flat subfolders:
/app/storage/
├── projects/ saved documents (ResolvedIR)
├── voiceover/ narration MP3s
├── renders/ exported MP4s
├── uploads/ user uploads (logos, brand media)
├── audio/music static + uploaded music
├── images/ image assets
├── footage/ video clips
├── runs/ Express run artifacts (email, etc.)
├── brand/ brand-kit files
└── settings.json
The footage-index cache (/app/data) is regenerable and stays a local volume.
Compose (local prod-shape)
docker-compose.yml:
- Exposes
3000:3000 env_file: .env(Azure app settings supply these in prod)shm_size: 3gb(Chromium headless),mem_limit: 6g- Mounts
./storage → /app/storage develop.watchrebuilds onweb/src,web/remotion,server/src,packages/, and thepackage.jsonfiles
CI/CD — GitHub Actions
.github/workflows/deploy-acr.yml builds the multi-stage image and pushes to ACR (login via service principal; tags latest / short SHA / branch). App Service pulls the tag and restarts. Required repo secrets: ACR_LOGIN_SERVER, ACR_USERNAME, ACR_PASSWORD.
Production env vars
Set in App Service Configuration (same keys as the repo-root .env):
# Director (video)
AZURE_OPENAI_ENDPOINT AZURE_OPENAI_KEY AZURE_OPENAI_DEPLOYMENT AZURE_OPENAI_API_VERSION
AZURE_TTS_ENDPOINT AZURE_TTS_KEY AZURE_TTS_DEPLOYMENT AZURE_TTS_API_VERSION
# Auth + legacy features
JWT_SECRET ALLOWED_USERS
ACS_CONNECTION_STRING ACS_SENDER_ADDRESS # email send (optional)
LANGSMITH_API_KEY LANGSMITH_PROJECT # observability (optional)
# Runtime (usually defaulted by docker-start.sh)
STORAGE_ROOT=/app/storage
ASSET_BASE_URL=http://127.0.0.1:3000
NODE_ENV=production
Health check
Point App Service's health probe at /api/health (proxied to Express).
Notes
- Memory: 6 GB recommended — headless Chromium + ffmpeg encoding can spike past 4 GB at 1080p.
- Render port: the renderer serves its bundle on
REMOTION_RENDER_PORT(default3333); keep it distinct from3000. - Single instance: rendering runs in-process with an in-memory job map — there is no external queue, so run one instance (or add a queue) if you need concurrent renders across replicas.
- Disk:
renders/andvoiceover/grow over time; schedule pruning.