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_URLs, JWT_SECRET, CORS_*, …) injected at deploy time. Never bake secrets or per-environment values into the image.
What app:create generates
The Dockerfile
The generatedDockerfile is a multi-stage build on the official oven/bun image. Each stage has one job:
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).
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.Build the image
Build theproduction target from the repository root, pointing at the app module’s Dockerfile:
production stage:
- listens on
PORT(defaults to3500in the image; override it with an env var), - binds
HOST_NAME=0.0.0.0so it is reachable outside the container, - runs as the non-root
bunuser, - ships a
HEALTHCHECKthat polls/healthcheck— any HTTP response (200/401/404) counts as healthy; only a refused connection fails.
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.- GitHub (GHCR)
- Docker Hub
- AWS ECR
- Google Artifact Registry
write:packages scope. This is exactly what the
generated .github/workflows/ci.yml does with docker/login-action and
docker/build-push-action.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:
docker-compose.production.yml
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.
Platform options
The same image runs anywhere that runs OCI containers. Pick per your ops appetite:Configure the environment
Every per-environment value is an environment variable read throughAppEnv. In development they live in modules/shared/.env.yml; in production you inject the same keys through your platform’s secret manager. See Configuration for how the YAML keys flatten to env vars (app.port → PORT, database.url → DATABASE_URL, …).
The essentials to set at deploy time:
CORS
Your API almost always answers a browser front-end (a spa) on another origin, so configure CORS explicitly for each deployed environment. TheCorsMiddleware is already wired into the app’s cors slot; its policy comes entirely from CORS_* environment variables:
Migrations and health
- Migrations: run
bun run db:migrateagainst 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
HEALTHCHECKhits/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?” duringapp: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, andlatestonmain).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.
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
productiontarget with the repo root as context. - Push a SHA-tagged, immutable image; move
:latestto 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.0and exposePORT. - Lock
CORS_ORIGINSto a real allowlist. - Wire the orchestrator’s health probe to
/healthcheck.
Related
- Deploy a microservice — the same image model, per service.
- Deploy a spa — ship the front-end that calls this API.
- CORS — configure cross-origin access for your front-ends.
- Configuration — how
.env.ymlmaps to the env vars you inject. - app:create — the generator that scaffolds the Dockerfile and pipelines.
- Microservice networking — per-environment service URLs and discovery.