#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). 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 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:
localhostinside your container is the VPS's loopback. A socket to127.0.0.1:6432from your app code is the same kernel socket as a socket to127.0.0.1:6432from the host process. There is no NAT, nodocker-proxy, no userspace TCP shim.- No
-p host:containermapping. Your app binds thePORTit was assigned directly on the host. nginx upstreams to that port for<project>.coduck.apptraffic (and any custom domain). - 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):
| 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 CASCADEon your project DB. PgBouncer authenticates incoming connections by calling apublic.user_lookup()function in your DB; if you drop and recreatepublic, that function (and the schema-levelUSAGEgrant topgbouncer_auth) goes with it.DATABASE_URLwill start returningbouncer config erroron every query until support restores them. Useprisma migrate/drizzle-kit dropetc. instead, which target your own tables without nukingpublic.
Both 6432 and 5433 are loopback-only — neither is reachable from outside
the VPS. That's why there's no raw external 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.
#Filesystem
- Working directory:
/app, bind-mounted from CoDuck's project store on the host. Reads and writes both work — your build can writenode_modules,.next/, etc. and they persist across redeploys of the same project. - User:
uid 1001(coduck, non-root).HOMEis set to/tmpso npm's cache/lockfile machinery has somewhere to write without trying to touch/root. - Restart policy:
on-failurewith 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_URLconnect to? → PgBouncer on127.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:5433in 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
localhostinside my container the same aslocalhoston 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 — instance sizes, deploy lifecycle, failure modes.
- Database overview — schema, migrations, backups.
coduck.jsonreference — the per-project deploy config.