CoDuck Docs

#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:

  • 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).
  • 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 varPoints atWhat it's for
DATABASE_URLpostgres://…@127.0.0.1:6432/<projectDb>?pgbouncer=true&connection_limit=1Runtime 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_URLpostgres://…@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.

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 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