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 becomemodules/<kebab-name>/, so the Dockerfile lives at and refers to the microservice’s own folder.
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:
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.
Push to a container registry
Tag for your registry and push, exactly as for the API — one repository per service:- GitHub (GHCR)
- Docker Hub
- AWS ECR
- Google Artifact Registry
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
--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.
Configure the environment
A microservice reads the same flattened env vars as the API, from its ownmodules/<kebab-name>/.env.yml in development and from injected secrets in production. Set at minimum:
| Variable | Purpose |
|---|---|
APP_ENV | Environment name. |
HOST_NAME | 0.0.0.0 inside a container. |
PORT | The service’s distinct port (assigned from 3001 upward at create time). |
DATABASE_URL | The service’s own Postgres. |
CACHE_REDIS_URL, PUBSUB_REDIS_URL, RATE_LIMIT_REDIS_URL | The 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:modules/app/app.ymldeclares the service and names the env var that holds its URL —MICROSERVICE_<SNAKE_UPPER>_URL(e.g.MICROSERVICE_BILLING_URL).- In each environment, set that variable to where the service is actually reachable —
https://billing.internal.example.comin production,http://localhost:3001locally.
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. ConfigureCORS_* 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 itsmodules/<kebab-name>/Dockerfile, and build only the services whose files changed so unrelated services aren’t rebuilt on every commit.
Checklist
- Build the
productiontarget with-f modules/<name>/Dockerfileand 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>_URLin 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.
Related
- Deploy an API — the shared image model and registry workflow in full.
- Microservice networking — ports, service declaration, and per-environment URLs.
- Microservice overview — when to reach for a microservice.
- microservice:create — the generator that writes the Dockerfile and wires discovery.
- Configuration — how
.env.ymlmaps to injected env vars. - CORS — cross-origin config, if the service is browser-facing.