Deployment
Production configuration, Docker, PM2, reverse proxy, and CLI reference
This guide covers running Arcway in production.
Quick Start
# Build pages for production
npx arcway build
# Start the production server
npx arcway startarcway start sets NODE_ENV=production, runs pending migrations, and starts the HTTP server with graceful shutdown handlers.
Environment Variables
Arcway automatically loads .env files in this order (later files override earlier ones):
.env.env.local.env.{mode}(e.g.,.env.production).env.{mode}.local(e.g.,.env.production.local)
Common environment variables:
# Required — vault master secret (sessions, JWT, callbacks, etc. derive from it)
# Generate with: npx arcway vault generate-key
ARCWAY_MASTER_SECRET=v1:…
# Database (PostgreSQL example)
DATABASE_URL=postgresql://user:pass@host:5432/mydb
# Email
SMTP_HOST=smtp.example.com
SMTP_USER=user@example.com
SMTP_PASS=your-smtp-password
# File storage (S3)
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=your-secret-key
# Encryption (if using vault encrypt/decrypt)
ENCRYPTION_KEY=your-encryption-keyProduction Configuration
Dev vs Production Differences
| Aspect | Development | Production |
|---|---|---|
| Session cookies | secure: false (HTTP) | secure: true (HTTPS only) |
| Logging | Pretty-printed with colors | JSON lines for machine parsing |
| File watching | Enabled, auto-restarts | Disabled |
| Shutdown timeout | 5 seconds | 10 seconds |
| Migrations | Auto-run on boot | Auto-run on boot |
Server Configuration
// arcway.config.js
export default {
server: {
host: '0.0.0.0', // Default: all interfaces
port: 3000, // Default: 3000
maxBodySize: 25 * 1024 * 1024, // Default: 25MB
shutdownTimeoutMs: 10_000, // Default: 10 seconds
},
};Database
SQLite works for small deployments. For production, use PostgreSQL or MySQL:
// arcway.config.js
export default {
database: {
client: 'pg',
connection: process.env.DATABASE_URL,
pool: { min: 2, max: 10 },
},
};Install the database driver:
npm install pg # PostgreSQL
npm install mysql2 # MySQLSessions
Session cookies are automatically set to secure: true in production, which requires HTTPS. Make sure your reverse proxy terminates TLS:
export default {
// The session signing key is derived from vault.masterSecret — no password here.
vault: {
masterSecret: process.env.ARCWAY_MASTER_SECRET,
},
session: {
cookieName: 'myapp.session',
ttl: 86_400, // 1 day in seconds
// secure defaults to true in production
},
};Cache and Queue
For multi-server deployments, switch from in-memory to Redis:
export default {
cache: {
driver: 'redis',
redis: { host: 'localhost', port: 6379 },
},
queue: {
driver: 'redis',
redis: { host: 'localhost', port: 6379 },
},
events: {
driver: 'redis',
// Uses redis config from cache section
},
};File Storage
Switch from local storage to S3 for production:
export default {
files: {
driver: 's3',
s3: {
bucket: 'my-bucket',
region: 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
},
},
};Migrations
Migrations run automatically on boot — both arcway dev and arcway start. This is safe because Knex's migration system is idempotent (it tracks which migrations have already run).
Manual migration commands:
# Create a new migration
npx arcway migrate make add-posts-table
# Run pending migrations
npx arcway migrate run
# Rollback the last batch
npx arcway migrate rollbackHealth Check
Arcway provides a built-in health endpoint at /_system/health:
curl http://localhost:3000/_system/healthResponse (200 OK):
{
"status": "ok",
"components": {
"database": { "status": "ok", "responseMs": 2 },
"redis": { "status": "ok", "responseMs": 1 }
}
}Response (503 Service Unavailable):
{
"status": "degraded",
"components": {
"database": { "status": "error", "responseMs": 5001, "error": "Connection timeout" },
"redis": { "status": "ok", "responseMs": 1 }
}
}Use this endpoint with load balancers, Docker health checks, or monitoring tools.
Graceful Shutdown
Arcway handles SIGTERM and SIGINT signals for graceful shutdown. The shutdown sequence:
- Stop accepting new connections
- Stop job runner (halt background jobs)
- Close pages router
- Close API router
- Drain existing HTTP connections
- Disconnect events bus
- Disconnect Redis (if configured)
- Close database connection
- Exit
If shutdown takes longer than shutdownTimeoutMs (default: 10 seconds), the process force-exits with a warning.
Running with PM2
PM2 is a process manager that handles restarts, clustering, and log management:
npm install -g pm2
# Start
pm2 start "npx arcway start" --name myapp
# Or with an ecosystem file:// ecosystem.config.js
module.exports = {
apps: [{
name: 'myapp',
script: 'npx',
args: 'arcway start',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
instances: 1, // Arcway handles its own jobs, use 1 instance
autorestart: true,
max_memory_restart: '512M',
}],
};pm2 start ecosystem.config.js
pm2 save # Save process list for auto-restart on reboot
pm2 startup # Generate startup scriptDocker
Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
# Build pages
RUN npx arcway build
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:3000/_system/health || exit 1
CMD ["npx", "arcway", "start"]docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- ARCWAY_MASTER_SECRET=${ARCWAY_MASTER_SECRET}
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: password
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pgdata:Reverse Proxy
Arcway should run behind a reverse proxy (nginx, Caddy, Traefik) that handles TLS termination.
Nginx
server {
listen 443 ssl;
server_name myapp.example.com;
ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
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;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Traefik (Docker labels)
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`myapp.example.com`)"
- "traefik.http.routers.myapp.entryPoints=websecure"
- "traefik.http.routers.myapp.tls.certResolver=letsencrypt"
- "traefik.http.services.myapp.loadbalancer.server.port=3000"CLI Reference
| Command | Description |
|---|---|
arcway dev | Start development server with file watching |
arcway start | Start production server |
arcway build | Build pages for production |
arcway migrate make <name> | Create a new migration file |
arcway migrate run | Run pending migrations |
arcway migrate rollback | Rollback last migration batch |
arcway seed | Run database seed files |
arcway test | Run test suite |
arcway schema | Schema commands |
arcway graphql-schema | Generate GraphQL schema |