Skip to content

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:

  1. Sets STORAGE_ROOT=/app/storage (one directory for all persistent data) and ASSET_BASE_URL=http://127.0.0.1:3000 (the internal origin the renderer fetches /api/media from)
  2. Creates the storage subfolders and seeds the static music library on first run
  3. Starts Express on :3001 (internal only)
  4. Starts Next.js on :3000 (exposed), with cwd /app/web so the Remotion bundler resolves remotion/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.watch rebuilds on web/src, web/remotion, server/src, packages/, and the package.json files
docker-compose build
docker-compose up
# open http://localhost:3000

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 (default 3333); keep it distinct from 3000.
  • 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/ and voiceover/ grow over time; schedule pruning.