For an AI

Deploy

Resuma is a long-running Rust process (Axum), not a JavaScript serverless function. Any host that can run a Docker image — Fly.io, DigitalOcean, AWS App Runner, a VM behind Cloudflare, Railway, Render — works. Not Lambda, not Cloudflare Workers. This documentation site runs on Fly.

LIVE

Deploy runtime

Live process env — same vars you set on Fly, DigitalOcean, or Docker.

What you are shipping

  • One binary that listens on 0.0.0.0 and the platform PORT.
  • Built-in probes: GET /health (liveness) and GET /ready (readiness).
  • RESUMA_ENV=production — sanitized errors, disk rate limits, stricter origin checks.
  • Behind Fly / App Platform / nginx / Caddy: RESUMA_TRUST_PROXY=1 and RESUMA_TRUSTED_PROXY_CIDRS — without the CIDR list the process refuses to start.

Fastest scaffold: resuma new my-app --template production (Dockerfile + fly.toml). Then pick a host below.

Why there is no Lambda / Workers adapter

Qwik City (including the v2 beta) can ship qwik add aws-lambda because the app is a JavaScript module. Vite emits entry_aws-lambda.tsx that exports a handler(event, context). The same source becomes a Worker fetch handler, a Node listener, or a Netlify/Vercel edge function. Fifteen adapter pages is cheap when each one is a 50-line Vite plugin.

Resuma's entry is FlowApp::serve(): bind a TCP socket, run Tokio + Axum until SIGTERM, write rate-limits and Resuma OS queues to disk. There is no fetch(request) export to wrap. A Lambda/Workers adapter would be a lie — cold start a whole native binary per request, no durable process, /tmp wiped, cron/workers dead. Do not use the AWS Lambda Web Adapter for that reason.

Qwik City targetResuma equivalent
AWS Lambda adapterApp Runner or ECS Fargate + ALB (same Docker image)
Cloudflare Workers / PagesWorkers/Pages: no. Origin on Fly/VPS + Cloudflare DNS, or Cloudflare Containers. Static brochure: resuma build --static-export
Vercel / Netlify EdgeNo. Those run JS isolates. Use Fly, Cloud Run, or a VM.
Google Cloud RunYes — same Dockerfile, honor PORT, health /health
Node / Deno / Bun / Firebase / Azure SWAJS runtimes. Skip. Self-host the binary or the image instead.
Static / GitHub Pagesresuma build --static-export — no #[server] / #[submit]
Self-hostingDocker + Caddy/nginx, or fly.toml

Bind address — pick one style

Flow reads RESUMA_ADDR first. If that is unset, it uses HOST + PORT (default 127.0.0.1:3000). When PORT is set, Resuma binds that port exactly — platforms require it.

StyleWhenTrap
HOST=0.0.0.0 + platform PORTFly, DigitalOcean, Railway, Render (they inject PORT)Do not also set RESUMA_ADDR to a different port
RESUMA_ADDR=0.0.0.0:8080Fixed port in Docker / Fly internal_portMust match the platform HTTP port (DO default is 8080)

Environment checklist

VariableProduction
RESUMA_ENVproduction
HOST / PORT or RESUMA_ADDRListen on all interfaces; port = platform HTTP port
RESUMA_TRUST_PROXY1 behind a load balancer
RESUMA_TRUSTED_PROXY_CIDRSRequired with trust-proxy. Fly: fdaa::/16. Many private fabrics: 10.0.0.0/8. Local nginx on the same machine: 127.0.0.1/32
SITE_URLPublic origin, no trailing slash (sitemap, OG tags)
RESUMA_DATA_DIRWritable dir for the container user (uid 65532). Use /data.resuma under /app is not writable after USER 65532.
CARGO_MANIFEST_DIR/app so public/ resolves in the image
RESUMA_EXEC_API_KEYOnly if the app uses .workers() — never commit it

Full matrix: Environment variables. Check locally with resuma doctor.

Dockerfile (all of the hosts below)

Apps from crates.io do not need Node in the image — loader/core JS is embedded in the resuma crate. Add a Node stage only if you ship ClientComponent TypeScript (this docs site does).

FROM rust:1.91-bookworm AS builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim
RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates \
    && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/target/release/my-app /app/server
COPY --from=builder /app/public /app/public
# Non-root cannot write /app/.resuma — #[server] actions then 500.
RUN mkdir -p /data && chown 65532:65532 /data
ENV RESUMA_ENV=production
ENV RESUMA_ADDR=0.0.0.0:8080
ENV RESUMA_DATA_DIR=/data
ENV CARGO_MANIFEST_DIR=/app
EXPOSE 8080
USER 65532:65532
CMD ["/app/server"]

Replace my-app with the binary name in Cargo.toml. Commit Cargo.lock. Depend on crates.io (or a git tag), not a sibling path = "../resuma" — remote builders will not see that folder.

  • COPY …/public — Flow serves {CARGO_MANIFEST_DIR}/public at runtime (icons, CSS, PWA). Without this copy, those URLs 404. Set CARGO_MANIFEST_DIR=/app so the path matches WORKDIR.
  • RESUMA_DATA_DIR=/data must be writable by uid 65532. A Fly volume is optional; the directory itself is not. Production rate limits write here. If it is missing, every #[server] action returns 500.
  • Put RESUMA_TRUST_PROXY and CIDRs on the platform (fly.toml), not only in the image — otherwise a local docker run exits.
  • Native extras belong in the image when the app needs them (e.g. ffmpeg, cmake). Keep the default image slim.
# .dockerignore
/target
.git
.github
docker build -t my-app .
    docker run --rm -p 8080:8080 \
    -e RESUMA_TRUSTED_PROXY_CIDRS=127.0.0.1/32 \
    -e RESUMA_TRUST_PROXY=1 \
  my-app
curl -fsS http://127.0.0.1:8080/health

Fly.io

Working Resuma apps on Fly use a fixed RESUMA_ADDR=0.0.0.0:8080 (not HOST+PORT), fdaa::/16, a writable /data, and flyctl deploy --remote-only --ha=false. Examples: youtubetotext, placaqr, underkb. This docs site: resuma-docs.fly.dev.

resuma new my-app --template production
cd my-app
fly launch --no-deploy --ha=false
# Edit fly.toml (snippet below), then:
fly deploy --ha=false
fly open
# fly.toml
app = "my-app"
primary_region = "iad"

[build]
  dockerfile = "Dockerfile"

[env]
  RESUMA_ENV = "production"
  RESUMA_TRUST_PROXY = "1"
  RESUMA_TRUSTED_PROXY_CIDRS = "fdaa::/16"
  RESUMA_ADDR = "0.0.0.0:8080"
  RESUMA_DATA_DIR = "/data"
  CARGO_MANIFEST_DIR = "/app"
  SITE_URL = "https://my-app.fly.dev"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0
  processes = ["app"]

  [[http_service.checks]]
    grace_period = "20s"
    path = "/health"
    interval = "15s"
    timeout = "2s"

[[vm]]
  size = "shared-cpu-1x"
  memory = "512mb"
  • --ha=false — Fly's default is two machines. One is enough and cheaper.
  • grace_period = "20s" — the first request after a Rust cold start is slow; without grace the check kills the machine.
  • min_machines_running = 0 + auto_stop_machines — scale to zero (typical SSR app). Workers / cron / SQLite that must keep running: min_machines_running = 1 and auto_stop_machines = false, plus a volume (see below).
  • Optional: RESUMA_CSP=0 if you embed YouTube or other third-party frames; RESUMA_BODY_LIMIT (bytes) for uploads.
  • Volume (survives restarts): fly volumes create resuma_data --size 1 and in fly.toml:
[mounts]
  source = "resuma_data"
  destination = "/data"

Without a volume, /data is still required so rate limits can write. It is just wiped on each new machine.

GitHub Actions (push to main)

Create a deploy token once, store it as the repo secret FLY_API_TOKEN:

fly tokens create deploy -x 999999h -a my-app
# .github/workflows/fly.yml
name: Fly Deploy
on:
  push:
    branches: [main]
  workflow_dispatch:
jobs:
  deploy:
    name: Deploy app
    runs-on: ubuntu-latest
    concurrency: deploy-group
    steps:
      - uses: actions/checkout@v4
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy --remote-only --ha=false
        env:
          FLY_API_TOKEN: YOUR_FLY_API_TOKEN

In the real file use the GitHub secret expression secrets.FLY_API_TOKEN (the usual Actions secrets.* form). --remote-only builds on Fly. concurrency: deploy-group prevents overlapping deploys. App secrets (RESUMA_EXEC_API_KEY, API keys) stay in fly secrets, not in GitHub env.

DigitalOcean App Platform

App Platform's default HTTP port is 8080. It injects PORT. Bind 0.0.0.0, not localhost. Use a Dockerfile (Rust buildpacks are slower and easier to misconfigure).

# .do/app.yaml — create the app from this spec or paste it in the control panel
name: my-resuma-app
services:
  - name: web
    dockerfile_path: Dockerfile
    http_port: 8080
    health_check:
      http_path: /health
      initial_delay_seconds: 30
      period_seconds: 10
    envs:
      - key: RESUMA_ENV
        value: production
      - key: HOST
        value: "0.0.0.0"
      - key: RESUMA_TRUST_PROXY
        value: "1"
      - key: RESUMA_TRUSTED_PROXY_CIDRS
        value: "10.0.0.0/8"
      - key: SITE_URL
        value: "https://my-resuma-app.ondigitalocean.app"
    # Encrypted secrets (API keys) belong in the control panel, not this file.

Do not set RESUMA_ADDR=0.0.0.0:3000 in the Dockerfile if App Platform expects 8080 — RESUMA_ADDR wins over PORT and health checks fail.

App Platform disks are ephemeral. Rate-limit files and exec queues reset on each deploy. That is fine for a brochure site. For workers, SQLite, or durable graphs, use a Droplet + volume (or Fly volumes) instead.

DigitalOcean Droplet (Docker + Caddy)

# On the droplet, after docker build:
    docker run -d --name my-app --restart unless-stopped \
    -p 127.0.0.1:8080:8080 \
    -v /var/lib/resuma:/data/resuma \
    -e RESUMA_ENV=production \
    -e HOST=0.0.0.0 \
    -e PORT=8080 \
    -e RESUMA_TRUST_PROXY=1 \
    -e RESUMA_TRUSTED_PROXY_CIDRS=127.0.0.1/32 \
    -e RESUMA_DATA_DIR=/data/resuma \
    -e SITE_URL=https://example.com \
  my-app

# Caddyfile
example.com {
  reverse_proxy 127.0.0.1:8080
}

Railway, Render, and similar

Same image. They inject PORT (Render often uses 10000). Set HOST=0.0.0.0, RESUMA_ENV, trust-proxy + a private CIDR (10.0.0.0/8 is the usual starting point), SITE_URL, and an HTTP health path of /health.

# Railway / Render-style env (PORT comes from the platform)
HOST=0.0.0.0
RESUMA_ENV=production
RESUMA_TRUST_PROXY=1
RESUMA_TRUSTED_PROXY_CIDRS=10.0.0.0/8
SITE_URL=https://your-app.up.railway.app

AWS

Same Docker image. Resuma is a long-running process — not Lambda, Amplify Hosting, or API Gateway+Node. Pick a container or a VM.

ServiceFit
App RunnerClosest to Fly. Push the image to ECR, port 8080, HTTP health /health.
ECS Fargate + ALBProduction default. Mount EFS at /data if queues/SQLite must survive tasks.
Lightsail or EC2Same as a Droplet: Docker + Caddy/nginx on 127.0.0.1.
LambdaSkip. Cold starts and no durable process. Do not wrap Resuma in the Lambda Web Adapter.

App Runner (console or CLI) — after docker push to ECR:

# Service
Port: 8080
Health check: HTTP  /health  (not /)

# Runtime env
RESUMA_ENV=production
RESUMA_ADDR=0.0.0.0:8080
RESUMA_TRUST_PROXY=1
RESUMA_TRUSTED_PROXY_CIDRS=10.0.0.0/8
RESUMA_DATA_DIR=/data
CARGO_MANIFEST_DIR=/app
SITE_URL=https://xxxxx.awsapprunner.com

App Runner injects PORT (default 8080). Keep RESUMA_ADDR on that same port. Give the health check 20s+ to allow a Rust boot. Disk is ephemeral unless you move to ECS+EFS.

ALB / App Runner hop is a private address — 10.0.0.0/8 is the usual RESUMA_TRUSTED_PROXY_CIDRS. If rate-limit IPs look wrong, tighten to your VPC CIDR.

Cloudflare

Resuma does not run on Cloudflare Workers or Pages. Workers are V8 isolates (JS/WASM); Pages is static files. resuma build --static-export can feed Pages for a brochure site with no #[server] / #[submit]. A real app needs a Linux container or a VM, then Cloudflare in front if you want.

DNS / proxy in front of Fly or a VPS (usual path)

Keep the origin on Fly, App Runner, or a Droplet. Point the domain at Cloudflare. Orange-cloud (proxied) means Cloudflare overwrites X-Forwarded-For — set RESUMA_TRUST_PROXY=1 and put Cloudflare's published IP ranges into RESUMA_TRUSTED_PROXY_CIDRS (comma-separated). Grey-cloud (DNS only) leaves TLS to the origin; then you do not need those ranges.

Simpler: Cloudflare Tunnel (cloudflared) on the same machine as the binary, proxy to http://127.0.0.1:8080, and set RESUMA_TRUSTED_PROXY_CIDRS=127.0.0.1/32.

Cloudflare Containers

Paid Workers plan. The same Dockerfile runs as a container; a small Worker fetchs it. defaultPort must be 8080 to match RESUMA_ADDR. Instances sleep when idle (sleepAfter) — /data is wiped, like Fly with no volume. Docs: developers.cloudflare.com/containers.

// src/index.js — Worker that forwards to the Resuma container
import { Container, getContainer } from "@cloudflare/containers";

export class Resuma extends Container {
  defaultPort = 8080;
  sleepAfter = "10m";
}

export default {
  async fetch(request, env) {
    return getContainer(env.RESUMA).fetch(request);
  },
};
{
  "name": "my-resuma-app",
  "main": "src/index.js",
  "compatibility_date": "2026-09-02",
  "containers": [
    { "class_name": "Resuma", "image": "./Dockerfile", "max_instances": 2 }
  ],
  "durable_objects": {
    "bindings": [{ "class_name": "Resuma", "name": "RESUMA" }]
  },
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Resuma"] }]
}

Set production env on the container (Wrangler secrets / envVars on the class): RESUMA_ENV, RESUMA_ADDR=0.0.0.0:8080, RESUMA_DATA_DIR=/data, CARGO_MANIFEST_DIR=/app, SITE_URL, and trust-proxy CIDRs for the Cloudflare hop. Then npx wrangler deploy.

GCP, Azure, and the rest

Cloud Run, Azure Container Apps, Google Compute, Azure VM: same image, listen on 0.0.0.0, honor PORT when the platform injects it, health /health, writable /data, RESUMA_TRUST_PROXY=1 + the load-balancer CIDR. If the host can run the Fly Dockerfile, it can run Resuma.

If it does not boot

SymptomCause
Process exits mentioning RESUMA_TRUSTED_PROXY_CIDRSRESUMA_TRUST_PROXY=1 without CIDRs
Health check timeout / 502Bound to 127.0.0.1, or RESUMA_ADDR port ≠ platform HTTP port
Docker build cannot find resumaPath dependency; pin crates.io or a git tag
#[server] / actions return 500RESUMA_DATA_DIR missing or not writable by the non-root user
Static files / PWA icons 404Forgot COPY public or CARGO_MANIFEST_DIR=/app
Two machines / surprise billOmitted --ha=false on fly launch / fly deploy
Health check kills a booting appMissing grace_period (~20s for a Rust binary)

← Getting started · Environment variables · Ops & production →