> ## Documentation Index
> Fetch the complete documentation index at: https://docs.talosjs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy an API

> Ship the app module as a single Bun container image built from the generated Dockerfile — one image for staging and production, configured entirely by environment variables at deploy time.

The **app module** is your API: the server that registers controllers, runs middleware, and answers HTTP. [`app:create`](/cli/commands/app-create) scaffolds everything you need to ship it as a container — a multi-stage `Dockerfile`, a `.dockerignore`, a `docker-compose.yml` for its backing stores, and (optionally) CI/CD pipelines. This page walks through building that image, pushing it to a registry, and running it in production.

The guiding principle is **one image, many environments**. A single image serves both staging and production; they differ only by the environment variables (`APP_ENV`, `DATABASE_URL`, the `*_REDIS_URL`s, `JWT_SECRET`, `CORS_*`, …) injected at deploy time. Never bake secrets or per-environment values into the image.

## What `app:create` generates

| File                 | Location                                             | Role                                                                                 |
| -------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `Dockerfile`         | `modules/app/Dockerfile`                             | Multi-stage Bun build producing a `production` target.                               |
| `.dockerignore`      | project root                                         | Keeps `node_modules`, `dist`, tests, secrets, and CI files out of the build context. |
| `docker-compose.yml` | `modules/app/docker-compose.yml`                     | Postgres + Redis for local development.                                              |
| CI/CD pipelines      | `.github/`, `.gitlab/`, or `bitbucket-pipelines.yml` | Build → push → deploy, if you opt in during create.                                  |

## The Dockerfile

The generated `Dockerfile` is a multi-stage build on the official [`oven/bun`](https://hub.docker.com/r/oven/bun) image. Each stage has one job:

| Stage        | Purpose                                                                                                                   |
| ------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `base`       | Pins the Bun version, sets `WORKDIR /app`, disables Husky (`HUSKY=0`).                                                    |
| `deps`       | Full install (incl. devDependencies), cached on the lockfile.                                                             |
| `prod-deps`  | Production-only install, in the same base image so prebuilt native addons match the runtime platform.                     |
| `build`      | Runs `bun run build` to emit `dist/index.js` with `@module/*` aliases inlined.                                            |
| `production` | Copies prod `node_modules` + the compiled bundle + runtime YAML, drops to the non-root `bun` user, and starts the server. |

The production image is **not fully standalone**. At startup `@talosjs/app` reads `modules/shared/src/roles.yml` from the working directory, and native dependencies load their `.node` addons from `node_modules`. That is why the image ships the bundle **plus** production `node_modules` **plus** the runtime data files (`roles.yml`, `modules/shared/.env.yml`).

<Note>
  The build context **must be the repository root**, not `modules/app/`. The
  Dockerfile copies `package.json`, `bun.lock`, and `modules/shared/…` by paths
  relative to the repo root.
</Note>

## Build the image

Build the `production` target from the repository root, pointing at the app module's Dockerfile:

```bash theme={null}
docker build \
  -f modules/app/Dockerfile \
  --target production \
  -t my-app:latest \
  .
```

Pin the Bun version at build time if you need to override the default:

```bash theme={null}
docker build -f modules/app/Dockerfile --target production \
  --build-arg BUN_VERSION=1.3.12 -t my-app:latest .
```

The `production` stage:

* listens on `PORT` (defaults to `3500` in the image; override it with an env var),
* binds `HOST_NAME=0.0.0.0` so it is reachable outside the container,
* runs as the non-root `bun` user,
* ships a `HEALTHCHECK` that polls `/healthcheck` — any HTTP response (200/401/404) counts as healthy; only a refused connection fails.

Smoke-test it locally against your dev Postgres and Redis:

```bash theme={null}
docker run --rm -p 3500:3500 \
  -e APP_ENV=staging \
  -e HOST_NAME=0.0.0.0 \
  -e PORT=3500 \
  -e DATABASE_URL="postgresql://talos:talos@host.docker.internal:5432/talos" \
  -e CACHE_REDIS_URL="redis://host.docker.internal:6379" \
  my-app:latest
```

## Push to a container registry

Tag the image for your registry and push. The generated CI/CD pipelines default to **GitHub Container Registry (GHCR)**, but any OCI registry works the same way — authenticate, tag, push.

<Tabs>
  <Tab title="GitHub (GHCR)">
    ```bash theme={null}
    echo "$GITHUB_TOKEN" | docker login ghcr.io -u <username> --password-stdin

    docker tag my-app:latest ghcr.io/<owner>/<repo>:latest
    docker push ghcr.io/<owner>/<repo>:latest
    ```

    The token needs the `write:packages` scope. This is exactly what the
    generated `.github/workflows/ci.yml` does with `docker/login-action` and
    `docker/build-push-action`.
  </Tab>

  <Tab title="Docker Hub">
    ```bash theme={null}
    docker login -u <username>

    docker tag my-app:latest <username>/my-app:latest
    docker push <username>/my-app:latest
    ```
  </Tab>

  <Tab title="AWS ECR">
    ```bash theme={null}
    aws ecr get-login-password --region <region> \
      | docker login --username AWS --password-stdin <account>.dkr.ecr.<region>.amazonaws.com

    docker tag my-app:latest <account>.dkr.ecr.<region>.amazonaws.com/my-app:latest
    docker push <account>.dkr.ecr.<region>.amazonaws.com/my-app:latest
    ```

    Create the repository first with `aws ecr create-repository --repository-name my-app`.
  </Tab>

  <Tab title="Google Artifact Registry">
    ```bash theme={null}
    gcloud auth configure-docker <region>-docker.pkg.dev

    docker tag my-app:latest <region>-docker.pkg.dev/<project>/<repo>/my-app:latest
    docker push <region>-docker.pkg.dev/<project>/<repo>/my-app:latest
    ```
  </Tab>
</Tabs>

<Tip>
  Tag by commit SHA (`ghcr.io/<owner>/<repo>:sha-abc1234`) as the immutable
  deploy target and move `:latest` to track the newest build. The generated
  GitHub pipeline does exactly this, so every deploy references a specific,
  reproducible image.
</Tip>

## Run it in production

The image is stateless; its Postgres and Redis live outside it. Point it at your managed or self-hosted datastores via environment variables. A production Compose file that pulls the pushed image and runs it alongside a `.env` file works well on a single host:

```yaml docker-compose.production.yml theme={null}
services:
  app:
    image: ${APP_IMAGE:-ghcr.io/owner/repo:latest}
    restart: unless-stopped
    ports:
      - "80:3500"
    env_file: .env
    environment:
      - HOST_NAME=0.0.0.0
      - PORT=3500
```

```bash theme={null}
# On the host: pull the new image and swap in with zero downtime.
docker login ghcr.io -u <username> --password-stdin <<< "$GITHUB_TOKEN"
docker pull ghcr.io/<owner>/<repo>:latest

# Run migrations before switching traffic, so a bad migration fails early.
docker run --rm --env-file .env ghcr.io/<owner>/<repo>:latest bun run db:migrate

# --wait blocks until the container reports healthy via the Dockerfile HEALTHCHECK.
APP_IMAGE=ghcr.io/<owner>/<repo>:latest docker compose \
  -f docker-compose.production.yml up -d --no-deps --wait app
```

This mirrors the generated `production.yml` GitHub workflow, which SSHes to the host, runs migrations, performs a health-gated `docker compose up --wait`, and keeps the previous image tagged `:rollback` so a failed health check can roll straight back.

<Warning>
  The generated `modules/app/docker-compose.yml` defines **Postgres and Redis
  only** — it is for local development. In production, run Postgres and Redis as
  managed services (or a separate, backed-up Compose stack) and give the app
  their URLs through the environment. Do not point production at the dev
  compose stack.
</Warning>

### Platform options

The same image runs anywhere that runs OCI containers. Pick per your ops appetite:

| Target                                                    | Notes                                                                                                                                                      |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Single VM + Docker Compose                                | The generated `production.yml` flow. Simple, SSH-based, with built-in rollback.                                                                            |
| Kubernetes                                                | Use the image in a `Deployment`; map the Dockerfile `HEALTHCHECK` to a liveness/readiness probe on `/healthcheck`; inject config via `Secret`/`ConfigMap`. |
| AWS ECS / Fargate, Google Cloud Run, Azure Container Apps | Managed container runtimes — supply the image URL and environment variables; set the container port to `PORT`.                                             |
| Fly.io, Render, Railway                                   | Point them at the repo/Dockerfile or the pushed image; set env vars in their dashboards.                                                                   |

## Configure the environment

Every per-environment value is an environment variable read through `AppEnv`. In development they live in `modules/shared/.env.yml`; in production you inject the same keys through your platform's secret manager. See [Configuration](/getting-started/configuration) for how the YAML keys flatten to env vars (`app.port` → `PORT`, `database.url` → `DATABASE_URL`, …).

The essentials to set at deploy time:

| Variable                                                                         | Purpose                                                                           |
| -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `APP_ENV`                                                                        | Environment name (`staging`, `production`, …). Drives `isProduction` and friends. |
| `HOST_NAME`                                                                      | Bind address — must be `0.0.0.0` in a container (the image sets this).            |
| `PORT`                                                                           | Listen port (image default `3500`).                                               |
| `DATABASE_URL`                                                                   | Postgres connection string.                                                       |
| `CACHE_REDIS_URL`, `PUBSUB_REDIS_URL`, `RATE_LIMIT_REDIS_URL`, `QUEUE_REDIS_URL` | Redis endpoints for each subsystem.                                               |
| `JWT_SECRET`                                                                     | Signing secret — **rotate away from the scaffold default**.                       |
| `CORS_*`                                                                         | Cross-origin policy (see below).                                                  |

<Warning>
  Never commit real `.env.yml` values. `.dockerignore` already excludes `.env`
  and `.env.*` (keeping only `.env.yml`), but production credentials belong in
  your platform's secret store, injected at runtime — not in the image and not
  in git.
</Warning>

### CORS

Your API almost always answers a browser front-end (a [spa](/deployment/spa)) on another origin, so configure CORS explicitly for each deployed environment. The [`CorsMiddleware`](/security/cors) is already wired into the app's `cors` slot; its policy comes entirely from `CORS_*` environment variables:

```bash theme={null}
CORS_ORIGINS=https://app.example.com, https://admin.example.com
CORS_METHODS=GET, POST, PUT, PATCH, DELETE
CORS_HEADERS=Content-Type, Authorization
CORS_CREDENTIALS=true
CORS_MAX_AGE=86400
```

<Warning>
  Use an explicit `CORS_ORIGINS` allowlist in production — never the wildcard
  `*`. And never combine `CORS_CREDENTIALS=true` with `CORS_ORIGINS=*`: browsers
  reject a credentialed response whose allow-origin is `*`. Full details in the
  [CORS guide](/security/cors).
</Warning>

### Migrations and health

* **Migrations**: run `bun run db:migrate` against the target database *before* routing traffic to the new image, so a broken migration surfaces before the swap. The production pipeline does this in a throwaway container.
* **Health checks**: the image's `HEALTHCHECK` hits `/healthcheck`. Point your load balancer, orchestrator probe, or uptime monitor at the same path. Any HTTP response means the process is up.

## Deploy with CI/CD

If you answered **yes** to "Create CI/CD files?" during `app:create`, you already have a build-and-deploy pipeline for GitHub, GitLab, or Bitbucket. The GitHub setup is two workflows:

* **`ci.yml`** — lints, tests, then builds and pushes the image to GHCR (tagged by branch, short SHA, and `latest` on `main`).
* **`production.yml`** — triggered by a semver tag (`v1.2.3`) or manual dispatch; runs CI, then SSHes to the host to pull the image, run migrations, health-gate the swap, and auto-rollback on failure.

Configure the pipeline's secrets/vars (`PROD_HOST`, `PROD_USER`, `PROD_SSH_KEY`, `PRODUCTION_URL`) in your provider's settings, and gate the `production` environment behind required reviewers for manual approval.

## Checklist

* Build the `production` target with the **repo root** as context.
* Push a **SHA-tagged, immutable** image; move `:latest` to follow it.
* Inject every per-environment value (DB, Redis, `JWT_SECRET`, `CORS_*`) at **deploy time** — never bake them in.
* Run **migrations before** switching traffic.
* Set `HOST_NAME=0.0.0.0` and expose `PORT`.
* Lock `CORS_ORIGINS` to a real allowlist.
* Wire the orchestrator's health probe to `/healthcheck`.

## Related

* [Deploy a microservice](/deployment/microservice) — the same image model, per service.
* [Deploy a spa](/deployment/spa) — ship the front-end that calls this API.
* [CORS](/security/cors) — configure cross-origin access for your front-ends.
* [Configuration](/getting-started/configuration) — how `.env.yml` maps to the env vars you inject.
* [app:create](/cli/commands/app-create) — the generator that scaffolds the Dockerfile and pipelines.
* [Microservice networking](/microservice/networking) — per-environment service URLs and discovery.
