Back to Blog
Tutorial7 min read2026-07-10

How to Deploy AdonisJS with PandaStack

Deploy an AdonisJS 6 app to production: the build step, APP_KEY and env validation, Lucid migrations, DATABASE_URL wiring, and going live on a Git push.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

AdonisJS is the rare Node framework that ships with opinions: a real ORM (Lucid), a migration system, environment validation, and a defined project structure. That makes production deploys more predictable than the average Express app — but there are still four things you have to get right: the compile step, the environment contract, the database connection, and the order you run migrations in. Let's walk through all of it with AdonisJS 6.

Step 1: Understand what the build produces

AdonisJS 6 is TypeScript-first and does not run TypeScript in production. You compile:

node ace build

This writes a build/ directory containing compiled JavaScript, your public/ assets, and — importantly — its own package.json. Production dependencies are installed *inside* that directory, and the server starts from there:

cd build
npm ci --omit=dev
node bin/server.js

If you've only ever run node ace serve --watch locally, internalize this now: production runs build/bin/server.js, not ace serve. Getting the start command wrong is the single most common AdonisJS deploy failure.

Step 2: The environment contract

Adonis validates environment variables at boot via start/env.ts. If a required variable is missing or malformed, the process exits immediately with a clear error instead of limping along and failing on the first request. This is a feature — a bad deploy dies loudly in the build logs rather than quietly serving 500s.

The variables a typical production app needs:

NODE_ENV=production
PORT=3333
HOST=0.0.0.0
APP_KEY=<generated secret>
LOG_LEVEL=info

Two of these bite people:

  • HOST=0.0.0.0 — Adonis binds to localhost by default, which means the platform's router can reach the container but not your app. Bind to all interfaces.
  • APP_KEY — used for cookie signing and encryption. Generate one locally with node ace generate:key, then set it as an environment variable on the platform. Never commit it, and don't regenerate it per deploy or you'll invalidate every session and encrypted value.

If you add your own required variables, declare them in start/env.ts so validation covers them:

import { Env } from '@adonisjs/core/env'

export default await Env.create(new URL('../', import.meta.url), {
  NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const),
  PORT: Env.schema.number(),
  HOST: Env.schema.string({ format: 'host' }),
  APP_KEY: Env.schema.string(),
  DATABASE_URL: Env.schema.string(),
})

Step 3: Point Lucid at DATABASE_URL

Scaffolded Adonis projects expect discrete DB_HOST / DB_USER / DB_PASSWORD variables. Managed platforms hand you one DATABASE_URL connection string instead. Lucid sits on Knex, and Knex's pg client accepts a connection string directly, so the cleanest adaptation in config/database.ts is:

import { defineConfig } from '@adonisjs/lucid'
import env from '#start/env'

const databaseConfig = defineConfig({
  connection: 'postgres',
  connections: {
    postgres: {
      client: 'pg',
      connection: {
        connectionString: env.get('DATABASE_URL'),
        ssl: { rejectUnauthorized: false },
      },
      migrations: {
        naturalSort: true,
        paths: ['database/migrations'],
      },
      pool: { min: 0, max: 10 },
    },
  },
})

export default databaseConfig

Two production notes on that config:

  • Pool size. Managed Postgres plans cap concurrent connections (on PandaStack the free tier allows 50, Pro 300). Each app instance holds its own pool, so max: 10 across three instances is 30 connections. Budget accordingly before you scale replicas.
  • min: 0 lets the pool drain when idle, which plays nicely with platforms that scale idle apps down.

Step 4: Migrations, in the right order

Lucid migrations are forward-only in production:

node ace migration:run --force

The --force flag is deliberate friction — Adonis refuses to run migrations when NODE_ENV=production without it. Two rules that will save you an outage:

  1. 1Never run migration:fresh or migration:reset against production. They drop tables. There is no confirmation prompt that will save you.
  2. 2Run migrations as a separate step before the new version takes traffic, not inside the app's boot code. If migrations run at boot and you deploy two replicas, both race to apply the same migration. Lucid takes a lock, but the loser can still crash-loop on startup timing. A discrete release step is boring and correct.

For zero-downtime deploys, also keep migrations backward-compatible with the previous app version: add the column in this release, start writing it in the next, drop the old one after. Expand, migrate, contract.

Step 5: A production Dockerfile

PandaStack can auto-detect a Node app and build it with buildpacks, but Adonis's two-stage build (compile, then install prod deps inside build/) is explicit enough that I prefer a Dockerfile:

FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN node ace build

FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/build ./
RUN npm ci --omit=dev
USER node
EXPOSE 3333
CMD ["node", "bin/server.js"]

The final image contains only compiled output and production dependencies — no TypeScript, no dev tooling, and it runs as a non-root user.

Step 6: Ship it on PandaStack

With the app containerized, the deploy itself is short:

  1. 1Provision the database. Create a managed PostgreSQL instance (14.x and 16.x are available) from the dashboard. Daily backups are scheduled automatically, retained 7 days on the free plan and longer on paid.
  2. 2Connect the repo. Add your AdonisJS repository as a container app. With the Dockerfile above in the repo root, it's picked up automatically; builds run in rootless BuildKit inside ephemeral Kubernetes job pods, and you can watch the build logs stream live — if node ace build fails on a type error, you'll see the exact compiler output.
  3. 3Attach the database. Because the database is attached to the app, DATABASE_URL is injected into the container environment automatically. No copying credentials between tabs, no secrets in the repo.
  4. 4Set the remaining env vars. APP_KEY, HOST=0.0.0.0, PORT, plus anything your start/env.ts requires, in the app's environment settings.
  5. 5Run migrations. Execute node ace migration:run --force as a release step or one-off command against the app before the new version serves traffic.

From then on, every git push triggers a build and deploy. Rollbacks and deployment history are available from the dashboard if a release goes sideways, and custom domains get SSL certificates automatically.

Framework gotchas worth knowing

  • Trust the proxy. Your app sits behind the platform's ingress, so enable proxy trust in Adonis's HTTP config — otherwise request.ip() returns the load balancer's address and secure cookie logic misfires behind TLS termination.
  • Static assets live in build/public and Adonis serves them via the static middleware. Fine for an admin panel; for a heavily-trafficked marketing site, consider splitting that into a separate static site deploy.
  • Health checks. Add a trivial /health route that returns 200 without touching the database. You want the platform judging liveness on the process, not on a saturated connection pool.
  • Cold starts on the free tier. Free-tier apps scale to zero when idle and cold-start on the next request. Perfectly fine for a staging environment or side project; for a latency-sensitive production API, use a paid tier that stays warm.

The whole thing, condensed

StepCommand
Compilenode ace build
Install prod depscd build && npm ci --omit=dev
Migratenode ace migration:run --force
Startnode bin/server.js
Deploygit push

AdonisJS gives you the production discipline — env validation, forward-only migrations, a real build artifact. The platform's job is to not fight that. If you want to see the full flow with a managed Postgres wired in automatically, try it at https://pandastack.io.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also