← Back to Integrations with 3rd parties

AWS App Runner

Deploy rtcstats-server on AWS App Runner in two stages: boot it, then make it keep your data.

You have a WebRTC app sending data to rtcstats-js. Now you need rtcstats-server somewhere your clients can actually reach. Not your laptop. Not a Docker container on a colleague's machine. A real URL with HTTPS that stays up.

AWS App Runner is one of the simplest paths to get there from inside AWS. It picks up your repo from GitHub, runs a managed Node.js runtime, and gives you a *.awsapprunner.com hostname. This guide walks the deploy in two stages: first it boots, then it does something useful. We won't pretend the first version is production-ready. It isn't. We'll be honest about what's missing as we go.

Before you start

You need:

  • An AWS account with permission to create App Runner services, IAM roles, and S3 buckets
  • A GitHub account, connected to App Runner via the GitHub connection flow in the App Runner console

That's it. No Docker installed locally. No Kubernetes. No Terraform.

Stage 1: get it running

Stage 1 is the smallest version of "running." You set one environment variable. App Runner's managed Node.js runtime does the rest. The result is a server that boots and accepts connections - but isn't actually useful yet. We'll fix that in Stage 2.

Step 1: choose your source

In the AWS console, open App Runner and click Create service, then pick Source code repository.

  • Provider: GitHub
  • Connection: your GitHub connection (create one if you don't have one yet)
  • Repository: rtcstats/rtcstats
  • Branch: main
  • Source directory: leave empty. The start command runs from the repo root and targets the workspace.
  • Deployment trigger: Manual. This is a server you deploy and forget. You don't want a git push to main to silently redeploy production.

Click Next.

Step 2: configure the service

  • Configuration source: Configure all settings here (no apprunner.yaml for now)
  • Runtime: Node.js 22 (the managed Node.js runtime)
  • Build command: leave empty for now. You'll add one later if you enable GeoIP enrichment.
  • Start command: npm start --workspace=packages/rtcstats-server
  • Port: 8080
  • CPU / Memory: 1 vCPU, 2 GB. You can scale up later from real load, not from anxiety.

The 8080 default comes from config/default.yaml:7 (httpPort: 8080). Match it on both sides or both will be wrong in different ways.

The workspace flag in the start command is what lets you start the server from the monorepo root.

Step 3: add a health check

Under Health check:

  • Protocol: HTTP
  • Path: /healthcheck

rtcstats-server exposes this endpoint at packages/rtcstats-server/rtcstats-server.js:94. It returns 200, no auth required. Use HTTP, not the default TCP check. TCP only confirms the port is open. HTTP confirms the app is actually serving.

Step 4: add one environment variable

Service-level environment variables, one row:

Key Value
NODE_ENV not-yet-production

That's the whole environment for Stage 1. Pick any value that reminds you this deploy isn't real yet. We use not-yet-production because it shows up in logs and reminds future-you what state this app is in.

Click Create & deploy and let it provision. After a few minutes you'll get a URL ending in .awsapprunner.com. Hit https://<service-id>.<region>.awsapprunner.com/healthcheck. You should see a blank 200.

That's Stage 1. The server is up. It's also, right now:

  • Not enriching anything with GeoIP (no MaxMind data)
  • Storing dump files on the App Runner instance's ephemeral disk
  • Not authenticating any clients (anyone with the URL can post data)

We're going to configure persistence in Stage 2. GeoIP enrichment is covered in How to enrich rtcstats-server with GeoIP data. Auth is covered at the end of this page - and it's the most important one before you point real traffic at this.

One App Runner-specific thing worth knowing: the platform enforces a per-request idle timeout. Long WebRTC sessions can hit it and reconnect mid-call. Validate against your real session lengths before you point production at this.

Stage 2: make it keep your data

Gathering dumps from your clients and storing them in object storage is what rtcstats-server is built around. Stage 2 wires up the storage half of that - a bucket the server can write each finished dump into, and access for the upload.

S3 is the native fit here: it's the original of the S3 API that rtcstats-server speaks, lives in the same console as your service, and the AWS SDK that rtcstats-server uses talks to it without any endpoint override. Pointing the server at a bucket is a few minutes of clicking and one environment variable.

Step 1: create an S3 bucket

In the AWS console, go to S3Create bucket.

  • Region: pick the same region as your App Runner service to avoid cross-region transfer.
  • Block all public access: ON. rtcstats dumps are private.
  • Versioning: off. Dumps are write-once.
  • Name: something you'll recognise, e.g. rtcstats-dumps-prod. The name is global across AWS S3, so generic names are taken.

Note the bucket name and region. You'll need both in Step 3.

Step 2: give App Runner access to the bucket

Two ways to do this. Prefer the first.

Instance role (recommended). App Runner can assume an IAM role on every request, and the AWS SDK inside rtcstats-server picks the credentials up automatically. No static keys to manage.

In IAMRolesCreate role:

  • Trusted entity: Custom trust policy
  • Trust principal: tasks.apprunner.amazonaws.com (the App Runner tasks service, distinct from the build-time access role)
  • Policy: grant s3:PutObject, s3:GetObject, and s3:ListBucket on the bucket ARN you just created
  • Name: something like rtcstats-server-instance-role so you can revoke it later without guessing

Back in App Runner, edit the service Security settings and set the Instance role to this role.

Static access keys (fallback). If you can't use an instance role, create an IAM user with the same S3 policy, generate an access key, and pass the credentials in NODE_CONFIG (Step 3 below). The instance role is the state-of-the-art on App Runner. Static keys are a fallback.

Step 3: configure rtcstats-server

rtcstats-server reads storage settings from the storage.s3 block in config/default.yaml. Override them at runtime via the NODE_CONFIG environment variable - it's inline JSON consumed by the node-config package at boot, and YAML in the repo maps one-for-one to keys here.

Replace the Stage 1 NODE_CONFIG (if you set one) with this. Mark it encrypted even when it carries no secret - the next maintainer shouldn't have to wonder.

Key Value
NODE_CONFIG see below

With an instance role, the config has no credentials block at all:

{
  "storage": {
    "s3": {
      "region": "us-east-1",
      "bucket": "rtcstats-dumps-prod"
    }
  }
}

With static keys, it looks more like the DigitalOcean version:

{
  "storage": {
    "s3": {
      "credentials": {
        "accessKeyId": "AKIAEXAMPLE",
        "secretAccessKey": "exampleSecretKeyReplaceMe"
      },
      "region": "us-east-1",
      "bucket": "rtcstats-dumps-prod"
    }
  }
}

Two things worth calling out:

  • region is the actual S3 region (us-east-1, eu-west-1, etc.), not a hard-coded default. Unlike DigitalOcean Spaces - where us-east-1 is the safe value regardless of where your Space lives - AWS S3 requires the real region.
  • endpoint is omitted entirely. The SDK derives it from region. The endpoint field in config/default.yaml is there for S3-compatible APIs (DO Spaces, Supabase Storage), not for AWS S3 proper.

forcePathStyle is left at the default (false). AWS S3 supports virtual-hosted style; only flip it on for backends that require path-style.

Step 4: verify dumps land in the bucket

Save and redeploy. Connect a client over the websocket (the default transport) and let it disconnect. Watch the App Runner runtime logs for a line like:

Connection with uuid <uuid> disconnected, starting to process data

Note the UUID. Then open your bucket in the S3 console - you should see an object whose key matches it. If the bucket stays empty, the most likely culprits are:

  • Instance role policy doesn't actually grant s3:PutObject on the bucket ARN, or grants it on a different ARN.
  • region in NODE_CONFIG doesn't match the bucket's region.
  • bucket name typo - rtcstats-server logs an S3 error in the runtime logs when this happens.
  • If you went with static keys: the IAM user's access key is inactive, or its policy doesn't cover the bucket.

Before this sees real traffic: lock down auth

Everything above gets you a working rtcstats-server you can poke at. You should make it more robust for production, partially by adding auth. This also doubles as our identifier mechanism.

The configuration in this guide does not set authorization.jwtSecret. From config/default.yaml:

JWT secret key to use for authorizing clients. If not set, no authorization is performed.

In plain English: anyone who finds your *.awsapprunner.com URL can post data to it. That's fine for a deploy you're poking at. It's not fine the moment real WebRTC traffic starts flowing.

For production, generate a JWT and authorize clients with it. The full setup is documented in the How to authenticate clients with rtcstats-server guide and also think about identifying users and sessions.

Set authorization.jwtSecret in NODE_CONFIG, sign tokens for your clients, and configure rtcstats-js to send them. That's the single change that turns this from "it deploys" into "it deploys safely."

What's next

Was this page helpful?