← Guides
Deployment Guides

How to Deploy AdonisJS v7 to Production (VPS Guide)

AdonisJS v7 compiles TypeScript to a self-contained build folder before deployment. This guide covers the full pipeline from build to reverse proxy, including migrations, environment variables, and process management.

ThomasThomas·2026-05-27

AdonisJS has a well-thought-out deployment story once you understand a few non-obvious things: the build output is a standalone Node.js app, environment variables are never bundled with the build, and migrations require the --force flag in production. Get those three things right and everything else follows naturally.

This guide covers AdonisJS v7 deployed to a Linux VPS. v7 requires Node.js 24 — this is the biggest infrastructure change from v6 and the first thing to get right.

How AdonisJS builds work

When you run node ace build, AdonisJS does several things in one command:

  • Compiles TypeScript to JavaScript in the build/ directory
  • Compiles Vite assets if you are using the @adonisjs/vite package
  • Copies package.json and your lockfile into build/
  • Rewrites ace.js for production use
  • Copies any metaFiles you have configured (edge templates, etc.)

The result is a build/ folder that is a complete, self-contained Node.js application. It has its own package.json, its own node_modules (after you install), and no TypeScript anywhere. You deploy build/, not your source directory.

This means your deployment process has two distinct phases: build on your CI or build machine, then copy and run on your production server.

Choosing a server

AdonisJS apps are typically full-stack with a database, so you want a proper VPS rather than a serverless platform. Good choices:

Hetzner Cloud offers exceptional value. A CAX21 (4 ARM vCPU, 8 GB RAM) costs around 7 EUR/month and handles a production AdonisJS app with plenty of room. Their ARM instances perform well for Node.js workloads. Data centers in Nuremberg, Falkenstein, Helsinki, and Ashburn.

DigitalOcean is a reliable choice with more polished managed services. Their $12/month Droplet with a managed Postgres cluster is a clean setup if you want the database handled for you.

Hetzner dedicated root servers start around 40 EUR/month if you need more raw power or want to host multiple apps on one machine.

Server setup

AdonisJS v7 requires Node.js 24. This is mandatory — do not use an older version.

curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs

Verify:

node --version  # must be v24.x.x

Install PM2 for process management:

npm install -g pm2

Install PostgreSQL if you are running your own database:

sudo apt install postgresql postgresql-contrib
sudo systemctl enable postgresql
sudo systemctl start postgresql

Create the database:

sudo -u postgres psql
CREATE DATABASE myapp_production;
CREATE USER myapp_user WITH ENCRYPTED PASSWORD 'a_strong_password_here';
GRANT ALL PRIVILEGES ON DATABASE myapp_production TO myapp_user;
ALTER DATABASE myapp_production OWNER TO myapp_user;
\q

Environment variables

AdonisJS intentionally excludes .env files from the build output. Environment variables must be provided separately in production — as a .env file in the build/ directory or via your platform's secrets management.

The minimum variables for a production AdonisJS v7 app:

# Application key — generated with: node ace generate:key
# Never change this once your app has users
# Changing it invalidates all sessions, encrypted cookies, and any encrypted data
APP_KEY=base64:your_32_byte_key_here

NODE_ENV=production

# Server binding — 0.0.0.0 is required inside containers
HOST=0.0.0.0
PORT=3333

# Database
DB_HOST=127.0.0.1
DB_PORT=5432
DB_USER=myapp_user
DB_PASSWORD=a_strong_password_here
DB_DATABASE=myapp_production

LOG_LEVEL=info

Generate APP_KEY with:

node ace generate:key

Run this once in your local development environment and copy the output. Treat it like a private key — store it in a password manager or your deployment platform's secrets store. If you lose it or rotate it by mistake, every session in your database becomes invalid and any encrypted data becomes permanently unreadable.

v7 note: In AdonisJS v7 the app key is used via config/encryption.ts rather than config/app.ts. This is a code-level change in how the key is referenced, but the environment variable name APP_KEY stays the same. If you are migrating from v6, remove export const appKey = env.get('APP_KEY') from config/app.ts to avoid conflicts.

The build pipeline

The recommended pattern is to build on your CI or local machine and rsync the output to the server. This keeps your production server lean — no devDependencies, no TypeScript compiler, no build toolchain.

Step 1: Install all dependencies (including devDeps for TypeScript compilation)

npm ci

Do not use --omit=dev here. The TypeScript compiler and other build tools are devDependencies and are needed for the build step.

Step 2: Build

node ace build

This creates the build/ directory.

Step 3: Install production dependencies inside build/

cd build
npm ci --omit=dev
cd ..

This installs only production dependencies. Your server receives a pre-built, ready-to-run folder.

Step 4: Copy to the server

rsync -avz --delete build/ user@yourserver.com:/var/www/myapp/

Step 5: Create the .env file on the server

# On the server
nano /var/www/myapp/.env
chmod 600 /var/www/myapp/.env

Running database migrations

Migrations must run before starting the app. The --force flag is required in production — AdonisJS v7 disables migrations by default in production environments and --force explicitly enables them:

cd /var/www/myapp
node ace migration:run --force

AdonisJS migrations use database-level locking so running this across multiple deployment instances simultaneously is safe. Only one instance will execute the migration while others wait.

There is no rollback in production by design. If a migration causes a problem, write a new forward migration to fix it.

Starting with PM2

cd /var/www/myapp
pm2 start bin/server.js --name myapp
pm2 save
pm2 startup

Run the command pm2 startup outputs to register PM2 with systemd so it restarts on reboot.

Useful PM2 commands:

pm2 logs myapp          # tail logs
pm2 status              # see all processes
pm2 restart myapp       # full restart
pm2 reload myapp        # zero-downtime reload (preferred for deploys)

Use pm2 reload for deploys — it sends SIGINT to the old process only after the new one is listening, preventing dropped connections.

Reverse proxy with Nginx

sudo apt install nginx

Create /etc/nginx/sites-available/myapp:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    # Must be higher than AdonisJS keepAliveTimeout (65s default)
    keepalive_timeout 70;

    location / {
        proxy_pass http://127.0.0.1:3333;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

Enable the site and get a TLS certificate:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

The keepalive_timeout 70 line matters. AdonisJS defaults its Node.js keepAliveTimeout to 65 seconds. If Nginx's timeout is lower, it closes the connection while Node still thinks it is alive, producing intermittent 502 errors on long-running requests. Always set Nginx's timeout slightly higher.

Static file serving

If your app uses Vite for frontend assets, serve them from Nginx directly rather than through the Node process:

location /assets/ {
    root /var/www/myapp/public;
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Vite generates hashed filenames so these can be cached indefinitely.

Workers

If your app has background workers (queue consumers, scheduled jobs), run them as separate PM2 processes:

// ecosystem.config.cjs
module.exports = {
  apps: [
    { name: 'myapp', script: 'bin/server.js' },
    { name: 'myapp-worker', script: 'bin/worker.js' },
  ],
}
pm2 start ecosystem.config.cjs
pm2 save

A complete deployment script

#!/bin/bash
set -e

REMOTE=user@yourserver.com
APP_DIR=/var/www/myapp

echo "Building..."
npm ci
node ace build
cd build && npm ci --omit=dev && cd ..

echo "Uploading..."
rsync -avz --delete build/ $REMOTE:$APP_DIR/

echo "Migrating and restarting..."
ssh $REMOTE "cd $APP_DIR && \
  node ace migration:run --force && \
  pm2 reload myapp"

echo "Done."

Common issues

"E_MISSING_APP_KEY"APP_KEY is not in the environment. Verify your .env file is in the build/ directory and PM2 is loading it.

502 Bad Gateway intermittently — almost always the keepalive_timeout mismatch. Set Nginx's timeout to 70 or higher.

"Migration not found" — you ran node ace migration:run from the source directory instead of build/. The compiled migration files are in build/database/migrations/.

"Cannot find module" — forgot to run npm ci --omit=dev inside build/. The production node_modules must be inside the build folder, not the project root.

Node version error on startup — you are running Node.js 20 or 22. AdonisJS v7 requires Node.js 24. Update with nvm install 24 && nvm use 24 or reinstall via NodeSource.


There is nothing especially hard about deploying AdonisJS but there are enough steps that missing one — wrong Node version, missing --force flag, .env in the wrong directory — causes confusing errors. If you would rather skip the server setup and just get your AdonisJS app online, Jetpacked detects AdonisJS automatically, runs migrations, provisions the database, and handles the reverse proxy. Connect your GitHub repo and you are live.

Deploy your app in minutes

Jetpacked handles Docker, HTTPS, databases, and deployments — so you can focus on building.

Launch your app free