Encore.ts is a TypeScript backend framework with an unusual proposition: you declare infrastructure (databases, pub/sub, cron jobs, secrets) as code in your application, and Encore wires it up. It pairs a TypeScript API layer with a high-performance Rust runtime that handles request processing. While Encore offers its own cloud, you can also self-host. This guide covers building an Encore.ts backend into a Docker container and deploying it with a managed PostgreSQL database.
How Encore.ts is structured
Encore apps are organized into services. You define APIs as typed functions, and Encore generates request validation, routing, and client SDKs from the types. Infrastructure is declared inline:
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
const db = new SQLDatabase("app", { migrations: "./migrations" });
export const getUser = api(
{ method: "GET", path: "/users/:id", expose: true },
async ({ id }: { id: number }): Promise<{ name: string }> => {
const row = await db.queryRow`SELECT name FROM users WHERE id = ${id}`;
return { name: row?.name ?? "" };
}
);The SQLDatabase declaration and the typed api function are the framework's core ideas — infrastructure and API contracts as code.
Self-hosting: encore build docker
Encore provides a command to produce a standalone Docker image for self-hosting:
encore build docker my-app:latestThis builds an image containing your compiled backend and the Encore runtime. The image is what you deploy to any container platform.
Configuring infrastructure for self-hosting
When self-hosting, Encore needs to know how to connect to real infrastructure (your database, secrets) rather than the managed Encore Cloud. You provide this via an infrastructure config file referenced by the ENCORE_RUNTIME_CONFIG or the --config mechanism. A minimal infra config maps your declared database to a real connection:
{
"sql_servers": [
{
"host": "<db-host>:5432",
"databases": {
"app": {
"username": "<user>",
"password": {"$env": "DB_PASSWORD"}
}
}
}
]
}The {"$env": "DB_PASSWORD"} form pulls the password from an environment variable, so secrets aren't in the file. Mount or bake this config and point Encore at it.
Wiring up a managed PostgreSQL
Link a managed PostgreSQL instance. You'll get connection details (host, port, database, user, password) — and on PandaStack a DATABASE_URL is injected. Map those into the infra config above (host, user, and DB_PASSWORD from the connection details). Encore's app database declaration then connects to your managed instance.
The Dockerfile approach
If you prefer a hand-written Dockerfile over encore build docker, a typical setup builds the app and runs the Encore runtime. But the supported path for self-hosting is encore build docker, which produces a correct image including the Rust runtime. Wrap it in your CI or run it during the platform build:
encore build docker --config ./infra-config.json my-app:latestThe resulting image listens on a port (configurable) — set it to the injected PORT via the runtime config so the platform's ingress reaches it.
Migrations
Encore manages migrations from the ./migrations directory you declared on the SQLDatabase. When self-hosting, Encore applies pending migrations on startup against the configured database. Because this runs at boot, be cautious with multiple replicas — Encore coordinates this, but for safety in a multi-replica rollout, confirm your version's migration behavior or run an initial single-replica deploy to apply migrations.
Environment variables and secrets
| Variable | Purpose |
|---|---|
PORT | listen port (via runtime config) |
DB_PASSWORD | database password referenced by infra config |
| Encore secrets | set via the infra config / env for secret() values |
Encore has a first-class secret() primitive. When self-hosting, secrets are supplied through the infra configuration and environment, keeping them out of source.
Health checks
Expose a lightweight API endpoint that returns 200 and point the platform's readiness probe at it. Keep it free of database access so the liveness check stays cheap:
export const health = api(
{ method: "GET", path: "/health", expose: true },
async (): Promise<{ status: string }> => ({ status: "ok" })
);Deploying
Produce the image with encore build docker, provide the infra config pointing at your managed PostgreSQL, and deploy as a container web service. Set DB_PASSWORD and any secrets, link the database, and ensure the runtime listens on the injected PORT. Live logs confirm the runtime started, migrations applied, and the DB connected.
encore build docker --config ./infra-config.json my-app:latestEncore's tradeoffs
Encore.ts is opinionated: its infrastructure-as-code model and Rust runtime deliver strong performance and developer ergonomics, but self-hosting requires the infra config step that the managed cloud handles for you. As a newer framework, check the current docs for the exact self-hosting flags — they've evolved. The payoff is a backend where infrastructure, validation, and typing are unified.
Conclusion
Self-hosting Encore.ts means building with encore build docker, supplying an infra config that maps your declared database to a managed PostgreSQL, sourcing secrets from env, and listening on the injected PORT. The framework's infrastructure-as-code approach is its defining strength.
Try self-hosting an Encore.ts backend with a managed PostgreSQL on PandaStack's free tier — get started at [dashboard.pandastack.io](https://dashboard.pandastack.io).
References
- [Encore.ts Documentation](https://encore.dev/docs/ts)
- [Encore: Self-hosting with Docker](https://encore.dev/docs/ts/self-host/build)
- [Encore: Infrastructure Configuration](https://encore.dev/docs/ts/self-host/configure-infra)
- [Encore: SQL Databases](https://encore.dev/docs/ts/primitives/databases)
- [Encore: Secrets](https://encore.dev/docs/ts/primitives/secrets)