Skip to main content

Overview

Manifest deploys as a single service that serves both the API and frontend. You can deploy it using:
  • Docker — Containerized deployment (recommended)
  • Railway — Managed platform with one-click deploy
  • Manual — Build and run with Node.js
All methods require a PostgreSQL database. The application automatically runs migrations on startup.

Docker Deployment

Prerequisites

  • Docker and Docker Compose installed
  • PostgreSQL 16 (can run in Docker or use external database)

Step 1: Create Docker Network

docker network create manifest-network

Step 2: Start PostgreSQL

If you don’t have an existing PostgreSQL instance:
docker run -d \
  --name manifest-postgres \
  --network manifest-network \
  -e POSTGRES_USER=manifest \
  -e POSTGRES_PASSWORD=your_secure_password \
  -e POSTGRES_DB=manifest \
  -p 5432:5432 \
  -v manifest-data:/var/lib/postgresql/data \
  postgres:16

Step 3: Build Manifest

Clone the repository and build:
git clone https://github.com/mnfst/manifest.git
cd manifest
npm install
npm run build
The build process:
  1. Turborepo builds the frontend (Vite) first
  2. Then builds the backend (NestJS)
  3. Frontend static files are copied to packages/backend/dist/public/

Step 4: Create Dockerfile

Create a Dockerfile in the project root:
FROM node:22-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./
COPY packages/backend/package*.json ./packages/backend/
COPY packages/frontend/package*.json ./packages/frontend/

# Install production dependencies only
RUN npm ci --workspace=packages/backend --omit=dev

# Copy built artifacts
COPY packages/backend/dist ./packages/backend/dist

EXPOSE 3001

CMD ["node", "packages/backend/dist/main.js"]

Step 5: Build and Run Container

1

Build the image

docker build -t manifest:latest .
2

Generate auth secret

openssl rand -hex 32
Save this value for the next step.
3

Run the container

docker run -d \
  --name manifest \
  --network manifest-network \
  -p 3001:3001 \
  -e NODE_ENV=production \
  -e PORT=3001 \
  -e BIND_ADDRESS=0.0.0.0 \
  -e BETTER_AUTH_SECRET=your_generated_secret \
  -e DATABASE_URL=postgresql://manifest:your_secure_password@manifest-postgres:5432/manifest \
  -e BETTER_AUTH_URL=https://your-domain.com \
  manifest:latest
4

Verify the deployment

docker logs -f manifest
You should see:
[Manifest] Server running on http://0.0.0.0:3001

Docker Compose (Alternative)

Create docker-compose.yml:
version: '3.8'

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: manifest
      POSTGRES_PASSWORD: your_secure_password
      POSTGRES_DB: manifest
    volumes:
      - manifest-data:/var/lib/postgresql/data
    networks:
      - manifest-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U manifest"]
      interval: 10s
      timeout: 5s
      retries: 5

  manifest:
    build: .
    ports:
      - "3001:3001"
    environment:
      NODE_ENV: production
      PORT: 3001
      BIND_ADDRESS: 0.0.0.0
      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
      DATABASE_URL: postgresql://manifest:your_secure_password@postgres:5432/manifest
      BETTER_AUTH_URL: https://your-domain.com
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - manifest-network

volumes:
  manifest-data:

networks:
  manifest-network:
    driver: bridge
Create .env file:
BETTER_AUTH_SECRET=your_generated_secret_here
Start services:
docker-compose up -d

Railway Deployment

Railway provides managed PostgreSQL and automatic HTTPS.
1

Create Railway project

  1. Sign in to Railway
  2. Click “New Project”
  3. Select “Deploy from GitHub repo”
  4. Connect your Manifest fork
2

Add PostgreSQL

  1. Click “New” → “Database” → “PostgreSQL”
  2. Railway automatically provisions a database
  3. Copy the connection string from the “Connect” tab
3

Configure environment variables

Add these variables to your Manifest service:
NODE_ENV=production
PORT=3001
BIND_ADDRESS=0.0.0.0
BETTER_AUTH_SECRET=<generate with: openssl rand -hex 32>
DATABASE_URL=${{Postgres.DATABASE_URL}}
BETTER_AUTH_URL=${{RAILWAY_PUBLIC_DOMAIN}}
Railway automatically sets RAILWAY_PUBLIC_DOMAIN with your service URL.
4

Configure build settings

Railway should auto-detect the setup. If needed, manually set:
  • Build Command: npm run build
  • Start Command: npm start
  • Root Directory: /
5

Deploy

Railway automatically builds and deploys on every push to your connected branch.View logs in the Railway dashboard to verify the deployment succeeded.

Railway Public Networking

Railway generates a public URL like manifest-production.up.railway.app. To use a custom domain:
  1. Go to your service settings
  2. Click “Networking” → “Custom Domain”
  3. Add your domain and configure DNS as instructed
  4. Update BETTER_AUTH_URL to your custom domain

Manual Deployment

Deploy directly on a VPS or bare metal server.

Prerequisites

  • Node.js 22.x or later
  • PostgreSQL 16 running and accessible
  • Git for cloning the repository
1

Clone and build

git clone https://github.com/mnfst/manifest.git
cd manifest
npm install
npm run build
2

Set up PostgreSQL

Create a database:
psql -U postgres
CREATE DATABASE manifest;
CREATE USER manifest WITH PASSWORD 'your_secure_password';
GRANT ALL PRIVILEGES ON DATABASE manifest TO manifest;
\q
3

Configure environment

Create packages/backend/.env:
NODE_ENV=production
PORT=3001
BIND_ADDRESS=0.0.0.0
BETTER_AUTH_SECRET=<generate with: openssl rand -hex 32>
DATABASE_URL=postgresql://manifest:your_secure_password@localhost:5432/manifest
BETTER_AUTH_URL=https://your-domain.com
4

Run migrations

Migrations run automatically on first startup, but you can verify:
cd packages/backend
npm run migration:show
5

Start the server

npm start
The server starts on http://0.0.0.0:3001.

Process Management with PM2

For production, use a process manager like PM2:
npm install -g pm2
Create ecosystem.config.js:
module.exports = {
  apps: [{
    name: 'manifest',
    script: './packages/backend/dist/main.js',
    instances: 1,
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 3001,
      BIND_ADDRESS: '0.0.0.0',
    },
    error_file: './logs/err.log',
    out_file: './logs/out.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
  }]
};
Start with PM2:
pm2 start ecosystem.config.js
pm2 save
pm2 startup

Systemd Service (Alternative)

Create /etc/systemd/system/manifest.service:
[Unit]
Description=Manifest AI Observability
After=network.target postgresql.service
Requires=postgresql.service

[Service]
Type=simple
User=manifest
WorkingDirectory=/opt/manifest
EnvironmentFile=/opt/manifest/.env
ExecStart=/usr/bin/node packages/backend/dist/main.js
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl enable manifest
sudo systemctl start manifest
sudo systemctl status manifest

Reverse Proxy (Nginx)

For production deployments, put Nginx in front of Manifest for HTTPS and load balancing. Create /etc/nginx/sites-available/manifest:
server {
    listen 80;
    server_name manifest.example.com;

    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name manifest.example.com;

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

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;

    # Proxy to Manifest
    location / {
        proxy_pass http://localhost:3001;
        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;
    }

    # SSE support
    location /api/v1/events {
        proxy_pass http://localhost:3001;
        proxy_http_version 1.1;
        proxy_set_header Connection '';
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 86400s;
    }
}
Enable and reload:
sudo ln -s /etc/nginx/sites-available/manifest /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Health Checks

Manifest exposes a public health check endpoint:
curl http://localhost:3001/api/v1/health
Response:
{
  "status": "ok",
  "timestamp": "2026-03-04T12:34:56.789Z"
}
Use this for:
  • Docker health checks
  • Kubernetes liveness probes
  • Load balancer health checks
  • Uptime monitoring

Troubleshooting

Server won’t start

Check environment variables:
node -e "console.log(require('dotenv').config())"
Ensure BETTER_AUTH_SECRET and DATABASE_URL are set. Check database connection:
psql $DATABASE_URL -c "SELECT version();"
View logs:
# Docker
docker logs -f manifest

# PM2
pm2 logs manifest

# Systemd
sudo journalctl -u manifest -f

Migrations fail

Migrations run automatically on startup. If they fail:
  1. Check PostgreSQL is running and accessible
  2. Verify the user has DDL permissions (CREATE TABLE, ALTER TABLE)
  3. Check for conflicting manual schema changes
Manually run migrations:
cd packages/backend
npm run migration:show  # Check status
npm run migration:run   # Run pending

Cannot access from outside container

Ensure BIND_ADDRESS=0.0.0.0 (not 127.0.0.1). Test from host:
curl http://localhost:3001/api/v1/health
Test from inside container:
docker exec manifest curl http://localhost:3001/api/v1/health

Next Steps

Configuration

Configure environment variables and server options

Database Setup

Advanced database configuration and backup strategies

Build docs developers (and LLMs) love