Skip to main content
Every microservice created with microservice:create gets its own Dockerfile so it builds, ships, and scales independently of the API and of every other service. A microservice is a standalone process: its own port, its own datastores, reached by the API over HTTP at a URL resolved from configuration (see Microservice networking). Deploying one is the same container workflow as the API — build, push, run — but scoped to a single module.

The generated Dockerfile

microservice:create copies the app module’s multi-stage Bun Dockerfile into the microservice folder and rewrites it for that service:
  • {{NAME}} becomes the microservice’s snake-case name (used in image tags and comments).
  • modules/app/ paths become modules/<kebab-name>/, so the Dockerfile lives at and refers to the microservice’s own folder.
The result is a modules/<kebab-name>/Dockerfile with the same five stages as the API image — base, deps, prod-deps, build, production — producing a compiled dist bundle plus production node_modules plus the runtime YAML the framework reads at startup. Everything in The Dockerfile on the API page applies here too; only the paths and the image name differ.
Like the API image, the build context must be the repository root. The Dockerfile installs from the root package.json/bun.lock and reads shared runtime files by repo-relative paths — a microservice image is still built from the top of the monorepo, just pointed at a different Dockerfile.

Build the image

Point -f at the microservice’s own Dockerfile and build the production target from the repo root. For a service named billing:
docker build \
  -f modules/billing/Dockerfile \
  --target production \
  -t billing:latest \
  .
The production stage behaves exactly as the API’s: it binds HOST_NAME=0.0.0.0, listens on PORT (image default 3500, override per environment), runs as the non-root bun user, and ships a HEALTHCHECK that polls /healthcheck.
Each microservice is a separate image with a separate tag. Give each its own repository in the registry (.../billing, .../notifications) and its own SHA-tagged builds, so services version and roll back independently.

Push to a container registry

Tag for your registry and push, exactly as for the API — one repository per service:
echo "$GITHUB_TOKEN" | docker login ghcr.io -u <username> --password-stdin

docker tag billing:latest ghcr.io/<owner>/billing:latest
docker push ghcr.io/<owner>/billing:latest
The API deployment page covers registry authentication and immutable SHA tagging in more depth — the same rules apply to every service.

Run it in production

Run each microservice as its own container, on its own port, pointed at its own datastores through the environment. A per-service Compose block on a single host:
docker-compose.production.yml
services:
  billing:
    image: ${BILLING_IMAGE:-ghcr.io/owner/billing:latest}
    restart: unless-stopped
    ports:
      - "3001:3001"
    env_file: ./billing.env
    environment:
      - HOST_NAME=0.0.0.0
      - PORT=3001
docker pull ghcr.io/<owner>/billing:latest
BILLING_IMAGE=ghcr.io/<owner>/billing:latest docker compose \
  -f docker-compose.production.yml up -d --no-deps --wait billing
The --wait flag blocks until the container reports healthy through the Dockerfile HEALTHCHECK. The same image runs on Kubernetes, ECS/Fargate, Cloud Run, Fly.io, and the rest — treat each microservice as its own deployable unit with its own scaling policy.
Each service owns its data. Give every microservice its own Postgres and Redis rather than pointing several services at one database for shared state — services talk to each other over HTTP, not a shared schema. See Microservice networking.

Configure the environment

A microservice reads the same flattened env vars as the API, from its own modules/<kebab-name>/.env.yml in development and from injected secrets in production. Set at minimum:
VariablePurpose
APP_ENVEnvironment name.
HOST_NAME0.0.0.0 inside a container.
PORTThe service’s distinct port (assigned from 3001 upward at create time).
DATABASE_URLThe service’s own Postgres.
CACHE_REDIS_URL, PUBSUB_REDIS_URL, RATE_LIMIT_REDIS_URLThe service’s own Redis endpoints.
CORS_*Only if browsers call the service directly (see below).

Service discovery: wire the URL back to the API

Deploying the container is only half the job — the API has to be able to reach it. Discovery is env-var-driven and per-environment:
  1. modules/app/app.yml declares the service and names the env var that holds its URL — MICROSERVICE_<SNAKE_UPPER>_URL (e.g. MICROSERVICE_BILLING_URL).
  2. In each environment, set that variable to where the service is actually reachable — https://billing.internal.example.com in production, http://localhost:3001 locally.
So after you deploy the billing image, set MICROSERVICE_BILLING_URL in the API’s production environment to the deployed service’s address. Never hard-code the hostname in source — the API always resolves it from configuration. Full details in Microservice networking.
Prefer a private network address (internal load balancer, service mesh, or private DNS) for service-to-service URLs so internal traffic never leaves your network. Only expose a microservice publicly if a browser or third party must reach it directly.

CORS

A microservice usually sits behind the API and is called server-to-server, so it needs no CORS configuration — CORS is a browser mechanism. Configure CORS_* on a microservice only when a browser calls it directly; then apply the same rules as the API’s CORS setup: an explicit CORS_ORIGINS allowlist, never * with credentials.

Deploy with CI/CD

The pattern mirrors the API pipeline: build the service’s image, push it under its own repository tag, then pull-and-swap on the target. Give each microservice its own build/deploy job (or a matrix entry) keyed on its modules/<kebab-name>/Dockerfile, and build only the services whose files changed so unrelated services aren’t rebuilt on every commit.

Checklist

  • Build the production target with -f modules/<name>/Dockerfile and the repo root as context.
  • Push each service to its own registry repository with immutable SHA tags.
  • Run each service on its own port with its own Postgres and Redis.
  • After deploy, set MICROSERVICE_<NAME>_URL in the API’s environment to the service’s address.
  • Keep service-to-service traffic on a private network where possible.
  • Add CORS only if a browser calls the service directly.