Deployment
Deploying Adonis EOS is similar to deploying any standard Node.js application. You need a server running Node.js >= 20.6 and a PostgreSQL database.
This guide provides the path of least resistance to getting your project running in a production environment.
1. Create a Production Build
The first step is to compile your TypeScript code and frontend assets (React/Inertia) into a production-ready format.
node ace build
The compiled output is written to the ./build directory. From this point forward, the build folder is the root of your application. For ESM aliases to resolve correctly, you should change into the build directory before running commands.
2. Configure Environment Variables
In production, you should never use a local .env file committed to version control. Instead:
Platform-managed: If using a cloud platform (e.g. DigitalOcean App Platform, Heroku), use their dashboard to set environment variables.
Server-based: Place a secure
.envfile in a protected directory (e.g.,/var/www/adonis-eos/.env) and point your app to it usingENV_PATH.
⚠️ Critical Note on
.envSyntax: Many production environment parsers (including PM2) can be sensitive to trailing comments. Ensure your variables do not have comments or extra spaces on the same line.Bad:
STORAGE_DRIVER=r2 # Use R2 for storageGood:
STORAGE_DRIVER=r2
Essential Production Variables
Variable | Description |
|---|---|
| Must be set to |
| Generate a fresh one with |
| Set to |
| The port your app will listen on (default |
| Logging verbosity (e.g., |
| Set to |
| Your production PostgreSQL credentials. |
| Set to |
| Set to |
| Required if using Redis for caching or sessions. |
| Port for your Redis instance (default |
| Optional password for Redis authentication. |
| Set to |
| The S3-compatible endpoint (e.g., |
| API Access Key ID. |
| API Secret Access Key. |
| The name of your bucket. |
| The public URL of your bucket. |
| Credentials for your email provider (for notifications). |
| The active mailer (e.g., |
| API key for your email provider (currently supports Resend). |
| Comma-separated list of allowed origins (e.g., |
| Username for the "Protected Access" layer and initial data splash page. |
| Password for the "Protected Access" layer and initial data splash page. |
3. Database & Initial Data
Database Setup
If you are running your own PostgreSQL instance (e.g., on Hetzner), you must manually create the database before running migrations:
# Connect as the postgres user
sudo -u postgres psql
# Create the database
CREATE DATABASE adonis_eos;
\q
Initial Data Seeding
Adonis EOS follows a convention for first-time production launches using a production-export.json file.
⚠️ Security Warning:
production-export.jsoncontains your entire site structure and content. NEVER commit this file to GitHub or any other public repository. It is included in.gitignoreby default.
Method A: Splash Page Upload (Recommended for Fresh Installs)
If you have configured PROTECTED_ACCESS_USERNAME and PROTECTED_ACCESS_PASSWORD, you can upload your export file directly through the web interface on a fresh installation:
Deploy your application and run migrations:
bashENV_PATH=./ node build/ace migration:run --forceNavigate to your site's root URL (e.g.,
https://yourdomain.com).Since no content exists, you will see a "Welcome" splash page.
Enter your protected access credentials and select your
production-export.jsonfile.Click Start Import. The site will automatically refresh once the data is loaded.
Method B: CLI Seeding
Alternatively, you can seed the data via the command line:
Ensure
database/seed_data/**/*is included in themetaFilesarray of youradonisrc.ts.Securely transfer your
production-export.jsontodatabase/seed_data/production-export.jsonon the server (e.g., via SCP or SFTP).Run the production seeder from the project root:
ENV_PATH=./ node build/ace db:seed --files ./database/seeders/production_import_seeder.js
Note: The seeder includes a safety check that allows up to 10 existing users (to account for system users created on boot) but will abort if it detects existing posts or menus to prevent data loss.
4. Common Production Pitfalls
ESM Alias Resolution (ERR_MODULE_NOT_FOUND)
If you see errors stating that a module starting with # cannot be found, it is almost always because the command is being run from the project root instead of the build folder. In production, the package.json inside the build folder contains the correct mappings for compiled code.
FFmpeg (Video Optimization)
If you plan to use video optimization features, ffmpeg must be installed on your production server.
Install FFmpeg:
Ubuntu/Debian:
sudo apt update && sudo apt install -y ffmpegRHEL/CentOS:
sudo dnf install ffmpeg
Verify PATH: Ensure
ffmpegandffprobeare in your system PATH.Custom Paths: If
ffmpegis installed in a non-standard location, you can specify the paths in your.env:typescriptFFMPEG_PATH=/usr/local/bin/ffmpeg FFPROBE_PATH=/usr/local/bin/ffprobe
R2 Signature Mismatch & Clock Drift
If R2 uploads fail with a "Signature Mismatch" error:
Check Server Time: Ensure your server clock is synced. Cloudflare rejects requests if the time drift is > 5 minutes. Use
timedatectl set-ntp on.Clean Keys: Ensure your
R2_SECRET_ACCESS_KEYhas no trailing spaces or hidden carriage returns (\r).Fresh Token: If in doubt, generate a brand new API token in the Cloudflare dashboard.
Media (Video/MP4) Not Rendering
If videos appear as broken links but the URL is correct:
MIME Type: Ensure the file in R2 has
Content-Type: video/mp4(notapplication/octet-stream).CSP: Ensure your
config/shield.tsincludes the R2 domain in themediaSrcdirective.
413 Payload Too Large (Imports)
If the initial data import fails during upload:
Nginx: Increase
client_max_body_sizein your Nginx config (see example below).Bodyparser: Ensure
config/bodyparser.tshas a high enoughmultipart.limit(e.g.,100mb).
Database SSL
Most managed cloud databases require SSL. If migrations fail to connect, ensure DB_SSL=true and DB_SSL_REJECT_UNAUTHORIZED=false are set.
Email Delivery (Hetzner/DigitalOcean)
Cloud providers like Hetzner and DigitalOcean often block outbound traffic on SMTP port 25 to prevent spam.
Use API-based Sending: We recommend using the Resend mailer (
MAIL_MAILER=resend). It uses HTTP APIs instead of SMTP, bypassing these port restrictions.Verify Domain: Ensure you have added the required DKIM/SPF records to your DNS (e.g., in Cloudflare) as provided by your mail provider.
Running Migration Commands in Production
If node build/ace migration:run fails to find your environment variables, you can "force" them by injecting them directly:
# Example of "forced" environment injection for production commands
NODE_ENV=production \
PORT=3333 \
HOST=0.0.0.0 \
CORS_ORIGINS="https://yourdomain.com" \
APP_KEY="your-key" \
DB_HOST="127.0.0.1" \
DB_USER="postgres" \
DB_PASSWORD="your-password" \
DB_DATABASE="adonis_eos" \
STORAGE_DRIVER=r2 \
R2_PUBLIC_BASE_URL="https://media.yourdomain.com" \
node build/ace migrate:media:r2
Note: The production seeder will automatically abort if it detects existing data in key tables (users, posts, etc.) to prevent accidental data loss.
⚠️ Safety: Disable Rollbacks
Rolling back in production is dangerous. We recommend disabling it in config/database.ts:
// config/database.ts
{
pg: {
client: 'pg',
migrations: {
disableRollbacksInProduction: true,
}
}
}
4. Services
Redis
Redis is strongly recommended for production environments. It is used for:
Server-Side Rendering (SSR) Caching: Drastically improves performance by caching rendered pages.
Session Management: Shared sessions across multiple application instances.
Rate Limiting: Accurate tracking of request rates.
Ensure REDIS_CACHE_ENABLED=true is set in your environment variables to enable the caching layer.
Persistent Storage
Since Adonis EOS handles heavy media uploads, you must use persistent storage. Default local storage is ephemeral on many cloud platforms.
Recommended: Use Cloudflare R2 (or S3-compatible). Set
STORAGE_DRIVER=r2and provide your bucket credentials.Local Alternative: Use a Persistent Volume if your host supports it and set
STORAGE_LOCAL_ROOTto that mount point.
Migrating Local Media to R2
If you have been using local storage during development and want to migrate your media to Cloudflare R2 for production:
Preparation: Ensure you have a local copy of all files in
public/uploads.Configuration: Ensure
STORAGE_DRIVER=r2and allR2_*variables are set in your production environment.Public Networking: Managed databases usually provide both internal and public hostnames. Since you are migrating local files to a production database, you must use the Public Hostname/Port and ensure SSL is enabled.
Migration: Run the migration command locally, providing your production database and R2 credentials. This will upload local files to R2 and update the production database records.
# Example: Running migration locally against a production database
DB_HOST=your-public-db-host.com \
DB_PORT=your-public-port \
DB_USER=your-user \
DB_PASSWORD=your-password \
DB_DATABASE=your-db-name \
DB_SSL=true \
DB_SSL_REJECT_UNAUTHORIZED=false \
STORAGE_DRIVER=r2 \
R2_ENDPOINT=https://your-endpoint.com \
R2_ACCESS_KEY_ID=your-key \
R2_SECRET_ACCESS_KEY=your-secret \
R2_BUCKET=your-bucket \
R2_PUBLIC_BASE_URL=https://media.yourdomain.com \
node ace migrate:media:r2 --dry-run
Note: Do not use platform-specific CLIs (like railway run) for this migration if they inject internal hostnames, as they will prevent your local machine from connecting to the database. Provide the public credentials directly as environment variables instead.
5. Health Checks
Adonis EOS includes a built-in health check endpoint for load balancers and uptime monitoring:
Endpoint:
/healthResponse: Returns a
200 OKwith JSON indicating status, uptime, and timestamp.
6. Process Management (PM2)
Use a process manager like PM2 to keep your application running in the background and restart it if it crashes. A pre-configured ecosystem.config.cjs is included in the project root.
Start your app
# Recommended: Ensure you are in the build directory for ESM resolution
cd build && pm2 start ../ecosystem.config.cjs
7. Automated Deployment (GitHub Actions)
Adonis EOS includes a GitHub Actions workflow (.github/workflows/deploy.yml) for automated deployments to a VPS (like Hetzner, DigitalOcean, or Linode) upon pushing to the main branch.
Prerequisites
GitHub Secrets: Add the following secrets to your repository:
HOST: Your server's IP address.USERNAME: Your SSH username (e.g.,root).SSH_PRIVATE_KEY: Your private SSH key.
Environment File: Ensure a
.envfile exists on the server at/var/www/adonis-eos/.env(or your configured path).
Workflow Overview
The workflow performs the following steps:
Checks out the code.
Installs dependencies and runs the build.
Syncs the
buildfolder,package.json, andecosystem.config.cjsto the server.Runs production migrations (via
cd build && node ace ...).Restarts the application via PM2.
8. Serving Static Assets
For the best performance, offload the task of serving static assets (images, CSS, JS) to a Reverse Proxy (Nginx) or a CDN.
Nginx Example
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://localhost:3333;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
# Increase upload limit for data imports
client_max_body_size 100M;
}
# Serve static assets directly via Nginx
location ~ \.(jpg|png|css|js|gif|ico|woff|woff2) {
root /var/www/adonis-eos/build/public;
add_header Cache-Control "public, max-age=31536000";
}
}
Related: Installation | Update Philosophy