Skip to main content
Express APIs deploy on Suga straight from a GitHub repo, with Postgres running alongside as a private container. Suga detects Node from your package.json and runs your start script, so no Dockerfile is needed. This guide explains how to deploy an Express API with Postgres in two ways:
  1. Step-by-step via the canvas
  2. Using a coding agent
Your repo needs a package.json with a start script (typically node index.js or node dist/server.js), express in dependencies, and a Postgres client (pg, or an ORM like prisma, drizzle-orm, or knex). The guide assumes your app reads process.env.DATABASE_URL for the Postgres connection and binds to process.env.PORT on 0.0.0.0.

Deploy using the Canvas

1

Create a project from your repo

In the Suga dashboard, click New project and pick your Express repo from the GitHub list, installing the Suga GitHub App if prompted. On the import page, name the project, set the branch you want deployed, and leave the build method on auto-detect; Suga detects Node from package.json and uses your start script. See Import from a GitHub repository for the full set of import options.Click Create project. Suga opens the canvas with the app service already connected to your repo.
2

Add Postgres from a template

Right-click empty space on the canvas, choose Add → Template, and pick PostgreSQL. Suga prompts for POSTGRES_USER and POSTGRES_DB (both default to postgres) and pre-fills a generated POSTGRES_PASSWORD.
Copy the password now, it won’t be visible after creation.
Suga adds the postgres container with port 5432 private and a volume mounted at /var/lib/postgresql/data.
3

Expose the app on HTTPS

Select the app service. On its Config tab, in the Private Network section, set the port to 3000. Then, in the Public Network section, click Add Endpoint → HTTPS Domain and choose port 3000.
4

Wire PORT and DATABASE_URL

On the app container, add a PORT env var set to 3000. Nothing sets it for you, so without it your app falls back to whatever default it sets in code.Then add a DATABASE_URL env var. Set the value to a connection string with an embedded reference to Postgres’s password:
The {{...}} picker inserts the reference alongside literal text, so the app reads the password from postgres at deploy time without duplicating it. Swap the user and database name if you changed them from the defaults.
Suga canvas showing postgres and app containers wired together with the postgres data volume attached and the app container's build repo visible in the properties panel
5

Apply to deploy

Add any other secrets your app needs (JWT signing keys, session secrets, API keys) as Sensitive env vars on the app container, then click Apply in the top right.Suga clones the app repo, runs npm install, starts your app with npm start, and rolls out both containers. The Express API is served at the public URL and reaches Postgres privately at postgres:5432. Open the public URL and you’ll get your app’s root route.

Deploy using an Agent

To deploy with a coding agent such as Claude Code, Codex, or OpenCode, follow these steps:
1

Connect the Suga MCP

If you haven’t already, connect your agent to the Suga MCP server.
2

Ask your agent to deploy

Ask your agent to deploy your repo to Suga. Here’s a basic prompt you can use, with your repo and branch filled in:
Prompt
The agent will give you a link to the new environment on the Suga canvas. Open it to review the setup.
3

Set the sensitive values

Set POSTGRES_PASSWORD on the postgres container: click the value input and paste a value generated with openssl rand -hex 32. If your app has other secrets (JWT signing keys, session secrets, API keys), add them to the app container as Sensitive env vars.
Save the postgres password somewhere secure, you’ll want it later.
4

Apply to deploy

Click Apply in the top right. Suga clones the app repo, runs npm install, starts your app with npm start, and rolls out both containers. The Express API is served at the public URL and reaches Postgres privately at postgres:5432. Open the public URL and you’ll get your app’s root route.

FAQ

No. Suga detects Node from package.json and runs npm install followed by your start script. If you already have a Dockerfile, you can tell the agent to use it instead; otherwise auto-detect handles the common case.
Any version. Suga uses the version pinned in engines.node in package.json, or a .nvmrc file if present, and falls back to a recent LTS release.
Two options. Simplest: set the start script in package.json to whatever you actually run (node dist/server.js, tsx src/index.ts, and so on). Alternatively, pass RAILPACK_START_CMD as a build arg on the app container with your exact command, for example node --enable-source-maps dist/server.js.
Suga detects the package manager from your lockfile (yarn.lock, pnpm-lock.yaml, bun.lock or bun.lockb) and runs the matching install and start commands. Commit the lockfile to your repo and the auto-detect handles the rest.
Yes. Set the start script to tsx src/index.ts (or ts-node) so the runtime handles compilation. For production performance, prefer a real build step with tsc and run the compiled output. Suga runs npm run build automatically if you define a build script in package.json.
Either works. Suga runs whatever your start script says; Node’s module resolution is decided by package.json "type" and file extensions, both of which the runtime handles at boot. No Suga-side configuration needed.
Skip DATABASE_URL on the app container and set the individual variables the pg client reads directly: PGHOST=postgres, PGPORT=5432, PGUSER=<postgres-user>, PGDATABASE=<postgres-db>, and PGPASSWORD referencing the postgres container’s POSTGRES_PASSWORD. The cross-container reference works for any variable name.
Wrap the start command with the migration: pass RAILPACK_START_CMD=npx prisma migrate deploy && node dist/server.js as a build arg. The migration runs each time a new container boots and no-ops if there’s nothing to apply.
Same pattern. For Drizzle: RAILPACK_START_CMD=npx drizzle-kit migrate && node dist/server.js. For Knex: RAILPACK_START_CMD=npx knex migrate:latest && node dist/server.js. Either wraps the migration into the start command so it runs at boot.
Yes. Drop the migration prefix from RAILPACK_START_CMD and run migrations manually — from the container’s shell in the dashboard, or from your CI pipeline before you Apply. Running migrations at boot is convenient, but teams with strict migration workflows often prefer to trigger them explicitly.
Yes. Add a Redis container from the template on the canvas and set REDIS_URL on the app container to redis://:{{redis.variables.REDIS_PASSWORD}}@redis:6379. The app reaches it over the private network without going through the public endpoint.
Yes. Swap the postgres:18-alpine container for a MySQL, MariaDB, or MongoDB image. Update DATABASE_URL (or the equivalent env vars) and the client library in your package.json to match.
Yes. Attach your domain to the app container in the dashboard once the app is deployed.
The Free tier fits a small Express plus Postgres stack, including the build history and volume. Pro is per-seat with hosting credits that offset compute and storage. Full pricing at suga.app/pricing.