---
title: Runtime architecture
description: How your project runs on CoDuck — container model, networking, database connectivity, filesystem.
category: reference
order: 2
agent: "One Docker container per project on CoDuck's VPS. --network=host: localhost inside the container IS the host's loopback (no proxy, shim, or NAT). DATABASE_URL points at PgBouncer on 127.0.0.1:6432 (transaction-pool mode; max_prepared_statements=100 server-side, so prepared-statement-using clients like drizzle-orm/node-postgres/knex/sequelize work; ?pgbouncer=true&connection_limit=1 already applied for Prisma) — this is the runtime request path for ALL stacks. DIRECT_URL points at postgres directly on 127.0.0.1:5433 — used by Prisma's schema engine ('prisma db push' / 'prisma migrate deploy'), NOT the runtime path. Footgun: running 'DROP SCHEMA public CASCADE' on your project DB destroys the PgBouncer auth function (user_lookup) and breaks DATABASE_URL until restored — don't do that. Container runs as uid 1001 with working dir /app. Read this when reasoning about what a connection string, env var, or 'localhost:...' actually points at."
---

# Runtime architecture

This page describes the runtime environment a deployed project actually runs
in: the container, the network, the database connection, the filesystem. Use
it when you're reasoning about what `process.env.DATABASE_URL` connects to,
what `localhost` means inside your container, or what sits between your app
and Postgres.

## Container model

Each deployed project runs in its **own Docker container on CoDuck's VPS**.
One project, one container. The base image is Node (Node 20 by default; pick
another via `runtime` in [`coduck.json`](/docs/reference/coduck-json)). Your
code is bind-mounted at `/app` as the working directory, and the container
runs as a non-root user (uid `1001`, the image's `coduck` user).

Container resource caps (CPU, memory) come from the instance size — see
[Deploying → Instance size](/docs/projects/deploy#instance-size-resources)
for the table.

## Networking

Containers run with Docker's `--network=host`. The consequences are worth
spelling out, because they're different from the bridged-network mental
model most "sandboxed hosting" platforms use:

- **`localhost` inside your container is the VPS's loopback.** A socket to
  `127.0.0.1:6432` from your app code is the same kernel socket as a socket to
  `127.0.0.1:6432` from the host process. There is no NAT, no
  `docker-proxy`, no userspace TCP shim.
- **No `-p host:container` mapping.** Your app binds the `PORT` it was
  assigned directly on the host. nginx upstreams to that port for
  `<project>.coduck.app` traffic (and any [custom domain](/docs/domains/custom-domains)).
- **External egress** (calls to public services — your own external API,
  Stripe, OpenAI, etc.) goes out the host's interface normally. Nothing
  intercepts it.

When something in this environment looks like "a local proxy" — it isn't.
The only thing between your app and `127.0.0.1:<port>` is the kernel's
loopback interface.

## Database connectivity

Two database env vars are injected on every deploy. Both are reserved
(you can't override them via `coduck env set` — see
[Environment variables](/docs/projects/deploy#environment-variables)):

| Env var | Points at | What it's for |
|---|---|---|
| `DATABASE_URL` | `postgres://…@127.0.0.1:6432/<projectDb>?pgbouncer=true&connection_limit=1` | **Runtime queries — all stacks.** The host:port is **PgBouncer**, running on the VPS in transaction-pool mode with server-side prepared statements enabled (`max_prepared_statements = 100`). Prisma reads `?pgbouncer=true` to disable its client-side prepared-statement caching; other clients (`node-postgres`, `pg`, `drizzle-orm`, `knex`, `sequelize`) just send prepared statements normally — PgBouncer caches them per backend. `connection_limit=1` keeps Prisma clients to one server connection per request. |
| `DIRECT_URL` | `postgres://…@127.0.0.1:5433/<projectDb>` | **Schema operations only.** `prisma db push` and `prisma migrate deploy` need the schema engine to talk directly to Postgres (their connection pattern doesn't match what the pool expects), so CoDuck routes those to a direct pooler-bypass URL. `127.0.0.1:5433` is the project's Postgres cluster bound directly to loopback. You should not use this URL for runtime queries — use `DATABASE_URL`. |

> [!WARNING]
> **Don't `DROP SCHEMA public CASCADE` on your project DB.** PgBouncer authenticates
> incoming connections by calling a `public.user_lookup()` function in your DB; if
> you drop and recreate `public`, that function (and the schema-level `USAGE` grant
> to `pgbouncer_auth`) goes with it. `DATABASE_URL` will start returning
> `bouncer config error` on every query until support restores them. Use
> `prisma migrate` / `drizzle-kit drop` etc. instead, which target your own tables
> without nuking `public`.

Both `6432` and `5433` are loopback-only — neither is reachable from outside
the VPS. That's why there's [no raw external connection URL](/docs/database/overview#why-theres-no-raw-connection-url).

PgBouncer's transaction-pool mode means a single client connection to `:6432`
is served by any of N backend Postgres connections, transparently and
per-transaction. That's why your **client-side pool should stay small**
(≤ 10) — opening many client connections doesn't help when PgBouncer is
already pooling on the server side, and oversized client pools waste
PgBouncer slots. See [Connection pooling](/docs/database/overview#connection-pooling).

## Filesystem

- **Working directory:** `/app`, bind-mounted from CoDuck's project store on
  the host. Reads and writes both work — your build can write `node_modules`,
  `.next/`, etc. and they persist across redeploys of the same project.
- **User:** `uid 1001` (`coduck`, non-root). `HOME` is set to `/tmp` so npm's
  cache/lockfile machinery has somewhere to write without trying to touch
  `/root`.
- **Restart policy:** `on-failure` with 2 retries. If your app crashes 3
  times in a row, the container stays stopped and the deploy reports it —
  redeploy to restart.

## Common questions this page answers

- **What does `process.env.DATABASE_URL` connect to?** → PgBouncer on
  `127.0.0.1:6432` (transaction-pool mode), on CoDuck's VPS loopback. Works for
  all stacks — Prisma, drizzle-orm, node-postgres, knex, sequelize.
- **Why is `localhost:5433` in my env (`DIRECT_URL`)?** → That's the
  pooler-bypass connection used by Prisma's schema engine at deploy time. It's
  not the runtime path.
- **Is `localhost` inside my container the same as `localhost` on the
  host?** → Yes — `--network=host`.
- **Is there a proxy between my app and Postgres?** → PgBouncer (a connection
  pooler, not a proxy in the security sense) sits on the host on `:6432`. The
  network hop between your container and PgBouncer is a direct loopback
  socket — no shim or proxy in addition to PgBouncer itself.

## Next

- [Deploying](/docs/projects/deploy) — instance sizes, deploy lifecycle, failure modes.
- [Database overview](/docs/database/overview) — schema, migrations, backups.
- [`coduck.json` reference](/docs/reference/coduck-json) — the per-project deploy config.
