N8Forge Documentation

Admin guide

Use this guide when you install, configure, sell, provision, monitor, and maintain N8Forge as the platform operator.

Audience Founders, technical operators, support leads, and platform admins.
What it covers Install, billing, plans, provisioning placement, tenant operations, updates, and troubleshooting.
What it does not cover Partner presentation workflows and workspace how-to content.

Overview

Platform ownership

Admins own the full runtime and commercial configuration of the platform. That includes install decisions, billing credentials, plan design, tenant placement, infrastructure pools, runtime health, and incident response.

  • Core setup controls branding, billing credentials, landing-page content, AI provider keys, and docs visibility.
  • SaaSForge controls tenants, domains, provisioning jobs, provisioning servers, health checks, backups, runtime dependencies.
  • N8NForge controls managed instances, launch metadata, and workflow-template delivery.

Server setup

VPS preparation

Before installing N8Forge, the target server must have a working LAMP/LEMP stack with PHP, a database, a reverse proxy for TLS termination, Docker for n8n runtimes, plus supporting services for queues, caching, and background work. This section guides you through preparing a fresh Ubuntu 24.04 server. Adjust package names if you use Debian or a different version.

Requirements overview
Web & proxy

Apache (HTTPD) serves the N8Forge application on loopback. Caddy terminates TLS on ports 80/443 and reverse-proxies to Apache. Caddy also manages per-tenant site configs for provisioned n8n instances.

PHP 8.3+

Required extensions: BCMath, Ctype, Fileinfo, JSON, Mbstring, OpenSSL, PDO, Tokenizer, XML, Curl, GD, Intl, Zip, MySQL (pdo_mysql), Redis, exif, gmp. The pcntl and posix extensions are compiled into PHP CLI by default — no separate packages needed.

Database

MySQL 8.0+ or MariaDB 10.11+ for the main application database. PostgreSQL can also be used but is not covered in this guide.

Docker

Docker Engine and Compose plugin for running n8n runtime containers provisioned by SaaSForge. The deploy user must be in the docker group.

Queue & cache

Redis for Laravel queues, sessions, and cache. Supervisor runs the queue worker process persistently.

Additional tooling

Composer (PHP dependency manager), Node.js 20+ and npm (asset compilation), Git (version control), cron (scheduler), UFW (firewall).

Architecture overview

The N8Forge app server runs two web-facing services on the same host:

  • Caddy listens on ports 80 and 443 (TLS). It reverse-proxies the main N8Forge domain to Apache and also manages dynamic per-tenant site files for n8n instances (written by the SaaSForge provisioning system).
  • Apache listens on 127.0.0.1:8080 and serves the N8Forge Laravel application via PHP-FPM. It is never exposed directly to the internet.
  • Docker runs n8n containers on allocated host ports (default range 23000+). Caddy reverse-proxies tenant subdomains to these ports.

This separation keeps TLS termination, tenant routing, and application serving each in their own layer.

Caddy manages both the app and tenants

When Caddy is configured as shown below, the same Caddy instance reverse-proxies the main app domain and imports per-tenant site configs from the sites-enabled/saasforge/ directory. SaaSForge writes tenant site files into that directory and reloads Caddy automatically during provisioning.

Automated setup script

The following script prepares a fresh Ubuntu 24.04 server. Run it as root or with a sudo-enabled user. It installs every service listed above, applies hardening defaults, and prints a summary at the end.

#!/usr/bin/env bash
set -euo pipefail

###############################################################################
# N8Forge — install-server bootstrap script
#
# Prepares a fresh Ubuntu 24.04 VPS with:
#   PHP 8.3 + extensions, MySQL 8, Apache (loopback :8080), Caddy (80/443),
#   Docker Engine + Compose plugin, Redis, Supervisor, Composer, Node.js.
#
# Usage:
#   sudo bash bootstrap-n8forge.sh
#
# After the script finishes, review the summary, then proceed to the
# N8Forge installation section.
###############################################################################

# ── Config ─────────────────────────────────────────────────────────────────
APP_DOMAIN="${APP_DOMAIN:-app.example.com}"
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}"
MYSQL_N8FORGE_DATABASE="${MYSQL_N8FORGE_DATABASE:-n8forge}"
MYSQL_N8FORGE_USER="${MYSQL_N8FORGE_USER:-n8forge}"
MYSQL_N8FORGE_PASSWORD="${MYSQL_N8FORGE_PASSWORD:-}"
DEPLOY_USER="${DEPLOY_USER:-deploy}"
PHP_VERSION="${PHP_VERSION:-8.3}"
TIMEZONE="${TIMEZONE:-UTC}"

# Colors for output
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
ok()  { echo -e "${GREEN}[OK]${NC} $1"; }
info(){ echo -e "${YELLOW}[..]${NC} $1"; }
fail(){ echo -e "${RED}[FAIL]${NC} $1"; exit 1; }

# ── Pre-flight checks ──────────────────────────────────────────────────────
[[ $EUID -eq 0 ]] || fail "Run this script as root (or with sudo)."
command -v apt-get &>/dev/null || fail "This script requires apt (Debian/Ubuntu)."

# Detect Ubuntu version
. /etc/os-release
UBUNTU_MAJOR="${VERSION_ID%%.*}"
[[ "$UBUNTU_MAJOR" -ge 22 ]] || fail "Ubuntu 22.04+ required (detected $VERSION_ID)."

# ── 1. System updates & base packages ──────────────────────────────────────
info "Updating system packages ..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get upgrade -y -qq
apt-get install -y -qq \
    curl wget gnupg2 ca-certificates lsb-release software-properties-common \
    ufw fail2ban unzip git cron

# Timezone
timedatectl set-timezone "$TIMEZONE"

# Swap (if none exists)
if ! swapon --show | grep -q .; then
    info "Creating 2 GB swap file ..."
    fallocate -l 2G /swapfile
    chmod 600 /swapfile
    mkswap /swapfile
    swapon /swapfile
    echo '/swapfile none swap sw 0 0' >> /etc/fstab
    ok "Swap created."
fi

ok "System packages installed."

# ── 2. PHP ─────────────────────────────────────────────────────────────────
info "Installing PHP $PHP_VERSION and extensions ..."
add-apt-repository -y ppa:ondrej/php
apt-get update -qq
apt-get install -y -qq \
    php${PHP_VERSION} php${PHP_VERSION}-fpm \
    php${PHP_VERSION}-bcmath php${PHP_VERSION}-ctype php${PHP_VERSION}-curl \
    php${PHP_VERSION}-dom php${PHP_VERSION}-fileinfo php${PHP_VERSION}-gd \
    php${PHP_VERSION}-gmp php${PHP_VERSION}-intl php${PHP_VERSION}-mbstring \
    php${PHP_VERSION}-mysql php${PHP_VERSION}-opcache \
    php${PHP_VERSION}-redis php${PHP_VERSION}-xml \
    php${PHP_VERSION}-zip php${PHP_VERSION}-exif

# Tune PHP-FPM for a production Laravel app
sed -i 's/^pm.max_children =.*/pm.max_children = 50/' /etc/php/${PHP_VERSION}/fpm/pool.d/www.conf
sed -i 's/^pm.start_servers =.*/pm.start_servers = 8/' /etc/php/${PHP_VERSION}/fpm/pool.d/www.conf
sed -i 's/^pm.min_spare_servers =.*/pm.min_spare_servers = 4/' /etc/php/${PHP_VERSION}/fpm/pool.d/www.conf
sed -i 's/^pm.max_spare_servers =.*/pm.max_spare_servers = 16/' /etc/php/${PHP_VERSION}/fpm/pool.d/www.conf

systemctl enable php${PHP_VERSION}-fpm
ok "PHP $PHP_VERSION installed."

# ── 3. MySQL ───────────────────────────────────────────────────────────────
info "Installing MySQL 8 ..."
# Pre-seed root password if provided (interactive install otherwise)
if [[ -n "$MYSQL_ROOT_PASSWORD" ]]; then
    debconf-set-selections <<< "mysql-server mysql-server/root_password password $MYSQL_ROOT_PASSWORD"
    debconf-set-selections <<< "mysql-server mysql-server/root_password_again password $MYSQL_ROOT_PASSWORD"
fi
apt-get install -y -qq mysql-server

systemctl enable mysql
systemctl start mysql

# Create application database and user
if [[ -z "$MYSQL_N8FORGE_PASSWORD" ]]; then
    MYSQL_N8FORGE_PASSWORD="$(openssl rand -base64 24)"
    echo -e "\nGenerated MySQL password for '${MYSQL_N8FORGE_USER}': ${MYSQL_N8FORGE_PASSWORD}\n" >> /root/.mysql_credentials
fi

mysql <<SQL
CREATE DATABASE IF NOT EXISTS \`${MYSQL_N8FORGE_DATABASE}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${MYSQL_N8FORGE_USER}'@'localhost' IDENTIFIED BY '${MYSQL_N8FORGE_PASSWORD}';
GRANT ALL PRIVILEGES ON \`${MYSQL_N8FORGE_DATABASE}\`.* TO '${MYSQL_N8FORGE_USER}'@'localhost';
FLUSH PRIVILEGES;
SQL

# Secure the installation
mysql --execute="DELETE FROM mysql.user WHERE User=''; DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1'); DROP DATABASE IF EXISTS test; DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%'; FLUSH PRIVILEGES;" || true

ok "MySQL 8 installed. DB '${MYSQL_N8FORGE_DATABASE}' created for user '${MYSQL_N8FORGE_USER}'."

# ── 4. Apache ──────────────────────────────────────────────────────────────
info "Installing Apache with PHP-FPM proxy ..."
apt-get install -y -qq apache2 libapache2-mod-fcgid
a2enmod proxy_fcgi setenvif rewrite
a2enconf php${PHP_VERSION}-fpm

# Listen only on loopback
sed -i 's/^Listen 80$/Listen 127.0.0.1:8080/' /etc/apache2/ports.conf

# Create the N8Forge vhost
cat > /etc/apache2/sites-available/n8forge.conf <<'APACHE'
<VirtualHost 127.0.0.1:8080>
    ServerName _default_

    DocumentRoot /var/www/n8forge/public

    <Directory /var/www/n8forge/public>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    <FilesMatch \.php$>
        SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
    </FilesMatch>

    ErrorLog ${APACHE_LOG_DIR}/n8forge-error.log
    CustomLog ${APACHE_LOG_DIR}/n8forge-access.log combined
</VirtualHost>
APACHE

a2dissite 000-default
a2ensite n8forge

systemctl enable apache2
ok "Apache configured on 127.0.0.1:8080."

# ── 5. Caddy (reverse proxy for the app + tenant domains) ──────────────────
info "Installing Caddy ..."
apt-get install -y -qq debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt-get update -qq
apt-get install -y -qq caddy

# Create the SaaSForge tenant site directory
mkdir -p /etc/caddy/sites-enabled/saasforge

# Write the main Caddyfile
cat > /etc/caddy/Caddyfile <<CADDY
{
    email admin@${APP_DOMAIN#*.}
    on_demand_tls {
        ask https://${APP_DOMAIN}/saasforge/caddy/on-demand-tls
        burst 5
    }
}

# Main N8Forge app — reverse-proxy to Apache
${APP_DOMAIN} {
    tls {
        on_demand
    }

    encode gzip

    reverse_proxy 127.0.0.1:8080 {
        # Pass the real client IP (Caddy is at the edge, not behind another proxy)
        header_up X-Real-IP {remote_host}
        #header_up X-Forwarded-For {remote_host}
    }
}

import /etc/caddy/sites-enabled/saasforge/*
CADDY

systemctl enable caddy
ok "Caddy installed. App domain: ${APP_DOMAIN}"

# ── 6. Docker ──────────────────────────────────────────────────────────────
info "Installing Docker Engine ..."
for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do
    apt-get remove -y "$pkg" 2>/dev/null || true
done

curl -fsSL https://get.docker.com | bash

# Docker daemon tuning
cat > /etc/docker/daemon.json <<'DOCKER'
{
    "log-driver": "json-file",
    "log-opts": {
        "max-size": "10m",
        "max-file": "3"
    },
    "live-restore": true,
    "storage-driver": "overlay2"
}
DOCKER

systemctl enable docker
systemctl restart docker
ok "Docker Engine installed."

# ── 7. Create deploy user ──────────────────────────────────────────────────
info "Creating deploy user '${DEPLOY_USER}' ..."
id -u "$DEPLOY_USER" &>/dev/null || useradd -m -s /bin/bash "$DEPLOY_USER"
usermod -aG docker "$DEPLOY_USER"

# Create runtime directories
mkdir -p /var/lib/saasforge/runtime
chown -R "${DEPLOY_USER}:${DEPLOY_USER}" /var/lib/saasforge/runtime

# Create the app directory (content placed later)
mkdir -p /var/www/n8forge
chown "${DEPLOY_USER}:www-data" /var/www/n8forge

ok "Deploy user '${DEPLOY_USER}' created."

# ── 8. Composer & Node.js ──────────────────────────────────────────────────
info "Installing Composer ..."
EXPECTED_CHECKSUM="$(php -r 'copy("https://composer.github.io/installer.sig", "php://stdout");')"
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"
if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
    echo 'ERROR: Composer installer checksum mismatch' >&2
    rm composer-setup.php
    exit 1
fi
php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php
ok "Composer installed."

info "Installing Node.js 20 LTS ..."
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y -qq nodejs
ok "Node.js $(node --version) installed."

# ── 9. Redis ───────────────────────────────────────────────────────────────
info "Installing Redis ..."
apt-get install -y -qq redis-server
sed -i 's/^supervised no/supervised systemd/' /etc/redis/redis.conf
systemctl enable redis-server
systemctl restart redis-server
ok "Redis installed."

# ── 10. Supervisor (queue worker) ──────────────────────────────────────────
info "Configuring Supervisor for queue worker ..."
apt-get install -y -qq supervisor

        cat > /etc/supervisor/conf.d/n8forge-worker.conf <<SUPERVISOR
[program:n8forge-worker]
process_name=%(program_name)s_%(process_num)02d
command=/usr/bin/php /var/www/n8forge/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --timeout=600
directory=/var/www/n8forge
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=${DEPLOY_USER}
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/n8forge/storage/logs/worker.log
stdout_logfile_maxbytes=10MB
SUPERVISOR

systemctl enable supervisor
ok "Supervisor configured for queue worker."

# ── 11. Firewall ───────────────────────────────────────────────────────────
info "Configuring UFW firewall ..."
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
ok "UFW active — SSH, HTTP, HTTPS allowed."

# ── 12. Kernel tuning ──────────────────────────────────────────────────────
info "Applying kernel tuning ..."
cat > /etc/sysctl.d/99-n8forge.conf <<'SYSCTL'
fs.file-max = 100000
vm.max_map_count = 262144
net.core.somaxconn = 1024
SYSCTL
sysctl --system

# ── 13. Cron (Laravel scheduler) ───────────────────────────────────────────
info "Setting up Laravel scheduler cron job ..."
cat > /etc/cron.d/n8forge-scheduler <<CRON
* * * * * ${DEPLOY_USER} cd /var/www/n8forge && /usr/bin/php artisan schedule:run >> /dev/null 2>&1
CRON
chmod 0644 /etc/cron.d/n8forge-scheduler

# ── Summary ────────────────────────────────────────────────────────────────
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}  N8Forge — VPS bootstrap complete${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo "  App directory      : /var/www/n8forge"
echo "  App domain         : ${APP_DOMAIN}"
echo ""
echo "  Database           : ${MYSQL_N8FORGE_DATABASE}"
echo "  DB user            : ${MYSQL_N8FORGE_USER}"
echo "  DB password        : ${MYSQL_N8FORGE_PASSWORD:-<set via MYSQL_N8FORGE_PASSWORD>}"
echo "  DB host            : localhost"
echo ""
echo "  Apache (loopback)  : http://127.0.0.1:8080"
echo "  Caddy (public)     : https://${APP_DOMAIN}"
echo ""
echo "  Deploy user        : ${DEPLOY_USER}"
echo "  Runtime root       : /var/lib/saasforge/runtime"
echo ""
echo "  Services enabled:"
echo "    - php${PHP_VERSION}-fpm    : Active"
echo "    - mysql          : Active"
echo "    - apache2        : Active"
echo "    - caddy          : Active"
echo "    - docker         : Active"
echo "    - redis-server   : Active"
echo "    - supervisor     : Active"
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo "  1. Point DNS for ${APP_DOMAIN} to this server's public IP."
echo "  2. Upload the N8Forge release ZIP into /var/www/n8forge."
echo "  3. Set correct ownership: chown -R \${DEPLOY_USER}:www-data /var/www/n8forge"
echo "  4. Copy and edit the .env file with the database credentials above."
echo "  5. Open https://${APP_DOMAIN}/install in your browser."
echo "  6. Complete the web installer, then proceed to the First-day checklist."
echo ""
echo -e "${RED}Save the database password above. It will not be shown again.${NC}"
echo ""
Running the script

Save the script as bootstrap-n8forge.sh, make it executable (chmod +x bootstrap-n8forge.sh), and run it as root: sudo bash bootstrap-n8forge.sh. You can pre-set the app domain, database name, and deploy user by exporting environment variables before running:

export APP_DOMAIN=n8forge.yourcompany.com
export MYSQL_N8FORGE_PASSWORD="$(openssl rand -base64 24)"
export DEPLOY_USER=n8forge
sudo -E bash bootstrap-n8forge.sh
Post-bootstrap steps
1
DNS

Point your domain

Create an A record for ${APP_DOMAIN} pointing at the server's public IP address. If you use a subdomain of a delegated zone (e.g., *.n8forge.example.com), ensure the wildcard or individual tenant records also resolve to this server.

2
Files

Upload the release

Upload the N8Forge release ZIP into /var/www/n8forge and extract it. Set ownership so the deploy user owns the files and the web server group can read them: chown -R n8forge:www-data /var/www/n8forge.

3
Permission

Set writable paths

Ensure storage/ and bootstrap/cache/ are writable by the web server group. Run: chmod -R 775 storage bootstrap/cache && chmod g+s storage bootstrap/cache.

The g+s flag ensures new files and directories created by the web server inherit the www-data group.

Installation

First deploy
  • PHP 8.3+ with the Laravel and Wave requirements expected by the packaged app.
  • Application database on MySQL, MariaDB, PostgreSQL, or SQLite.
  • Queue and scheduler support because provisioning, templates, cleanup, and reminders depend on scheduled work.
  • Docker-capable runtime hosts for the local Linux-host driver or any SSH provisioning target.
1
Step 1

Extract the main package

Deploy the versioned install ZIP into the target app directory and point the web root at public/.

2
Step 2

Prepare writable paths

Ensure Laravel can write to storage/ and bootstrap/cache/, then provide a valid .env.

3
Step 3

Run the installer

Open /install, finish the environment and database steps, then create the first admin account.

4
Step 4

Log in and complete product setup

Move immediately into Setup, Plans, Themes, and provisioning resources before testing real customer flows.

First-day checklist

Recommended order
  • Configure Setup for General, Billing, Landing Page, AI, and Authentication.
  • Review plans and assign real billing price IDs plus any plan-level delivery mappings.
  • Choose the public theme and replace seeded marketing content.
  • Enable scheduler execution so provisioning and health automation can run.
  • Create provisioning resources such as SSH servers, external databases, and external storage backends if you will not rely only on the default local runtime path.
  • Run a real test subscription and confirm it creates a tenant, queues a job, and produces a usable runtime instance page.
  • or custom domains.

Setup page

Shared admin config

The Core Setup page is the main live configuration surface for branding, billing credentials, marketing content, AI provider keys, and authentication presentation.

General

Site title, description, logos, icons, analytics, and docs visibility in app menus.

Billing

Stripe or Paddle provider selection plus encrypted credential and webhook storage.

Landing Page

Hero, CTA, stats, pricing copy, testimonials, and public marketing sections.

AI

Encrypted provider keys for OpenAI, Anthropic, Grok, and DeepSeek.

Authentication

Shared auth-related setup exposed through the same admin flow.

Behavior

Database-backed setup values are loaded into runtime config and become the live source of truth.

Key env reference

Operator-level settings
Variable Default Purpose
SaaSForge — Base
SAASFORGE_WORKSPACE_SUFFIX Workspace Suffix appended to tenant workspace names
SAASFORGE_PROVISIONING_DRIVER docker Default runtime driver (docker / ssh)
SAASFORGE_PROVISIONING_QUEUE default Queue connection for provisioning jobs
SAASFORGE_PROVISIONING_BATCH_SIZE 10 Max jobs processed per scheduler run
SAASFORGE_SHOW_CADDY_FIX_BUTTONS false Show admin UI buttons for Caddy repair
SAASFORGE_BASE_DOMAIN app.url host Base domain for generated tenant subdomains
SAASFORGE_AUTO_SSL true Auto-provision SSL via Let's Encrypt
SAASFORGE_PRODUCT_TYPE n8nforge Default product type for new tenants
SAASFORGE_AUTO_CREATE_FIRST_INSTANCE true Auto-create the first n8n instance when a tenant is created from a subscription
SaaSForge — Docker Runtime
SAASFORGE_LINUX_RUNTIME_ENABLED true Enable the local Docker driver
SAASFORGE_RUNTIME_ROOT_PATH storage/.../runtime Host path for compose files and runtime data
SAASFORGE_DOCKER_BIN docker Docker binary path
SAASFORGE_DOCKER_COMPOSE_COMMAND docker compose Compose command
SAASFORGE_RUNTIME_NETWORK saasforge Docker network name for runtime containers
SAASFORGE_RUNTIME_IMAGE docker.n8n.io/n8nio/n8n:latest n8n container image tag
SAASFORGE_RUNTIME_BASE_PORT 23000 Starting port for container port mapping
SAASFORGE_RUNTIME_PORT_STEP 1 Port increment between instances
SAASFORGE_RUNTIME_WAIT_TIMEOUT 180 Seconds to wait for container health
SAASFORGE_RUNTIME_TIMEZONE UTC Timezone set on runtime containers
SAASFORGE_RUNTIME_CPUS 0.5 Default CPU limit per container
SAASFORGE_RUNTIME_MEMORY 600m Default memory limit per container
SAASFORGE_RUNTIME_MEMORY_SWAP 700m Default swap limit per container
SAASFORGE_RUNTIME_RUNNERS_ENABLED true Enable n8n task runners
SaaSForge — Caddy (Docker)
SAASFORGE_RUNTIME_CADDY_ENABLED true Manage Caddy reverse-proxy sites
SAASFORGE_RUNTIME_CADDY_BIN caddy Caddy binary path
SAASFORGE_RUNTIME_CADDY_RELOAD true Run caddy reload after site changes
SAASFORGE_RUNTIME_CADDY_MAIN_CONFIG /etc/caddy/Caddyfile Main Caddyfile path
SAASFORGE_RUNTIME_CADDY_SITE_DIR storage/.../caddy-sites Directory for per-instance site files
SAASFORGE_RUNTIME_CADDY_TLS_EMAIL MAIL_FROM_ADDRESS Email for Let's Encrypt certificate notices
SaaSForge — SSO / OIDC
SAASFORGE_RUNTIME_N8N_SSO_ENABLED false Enable OIDC SSO for instance login
SAASFORGE_RUNTIME_N8N_SSO_PROTOCOL oidc SSO protocol (oidc)
SAASFORGE_RUNTIME_N8N_SSO_MANAGED_BY_ENV true Inject SSO config via container env vars
SAASFORGE_RUNTIME_N8N_SSO_USER_ROLE_PROVISIONING disabled Auto-provision n8n user roles on login
SAASFORGE_RUNTIME_N8N_SSO_OIDC_LOGIN_ENABLED true Show OIDC login button on n8n sign-in
SAASFORGE_RUNTIME_N8N_SSO_OIDC_CLIENT_ID n8n-sso OIDC client ID
SAASFORGE_RUNTIME_N8N_SSO_OIDC_CLIENT_SECRET generated OIDC client secret (regenerated on install)
SAASFORGE_RUNTIME_N8N_SSO_OIDC_DISCOVERY_ENDPOINT app.url /.well-known/… OIDC discovery URL
SAASFORGE_RUNTIME_N8N_SSO_OIDC_PROMPT empty OIDC prompt parameter (login, consent, etc.)
SAASFORGE_RUNTIME_N8N_SSO_OIDC_ACR_VALUES empty OIDC acr_values parameter
SaaSForge — SSH Runtime
SAASFORGE_SSH_RUNTIME_ENABLED true Enable the SSH runtime driver
SAASFORGE_SSH_CONNECT_TIMEOUT 10 SSH connection timeout in seconds
SAASFORGE_SSH_WAIT_TIMEOUT 180 Wait timeout for remote operations
SAASFORGE_SSH_RUNTIME_ROOT_PATH /var/lib/saasforge/runtime Remote path for runtime files
SAASFORGE_SSH_DOCKER_BIN docker Remote Docker binary path
SAASFORGE_SSH_DOCKER_COMPOSE_COMMAND docker compose Remote compose command
SAASFORGE_SSH_RUNTIME_IMAGE docker.n8n.io/n8nio/n8n:latest Remote n8n image tag
SAASFORGE_SSH_RUNTIME_BASE_PORT 23000 Remote base port
SAASFORGE_SSH_RUNTIME_CPUS 0.5 Remote CPU limit
SAASFORGE_SSH_RUNTIME_MEMORY 600m Remote memory limit
SAASFORGE_SSH_RUNTIME_MEMORY_SWAP 700m Remote swap limit
SAASFORGE_SSH_RUNTIME_RUNNERS_ENABLED true Remote task runners
SAASFORGE_SSH_CADDY_RELOAD true Remote Caddy reload after site changes
SAASFORGE_SSH_CADDY_BIN caddy Remote Caddy binary path
SAASFORGE_SSH_CADDY_MAIN_CONFIG /etc/caddy/Caddyfile Remote main Caddyfile
SAASFORGE_SSH_CADDY_SITE_DIR /etc/caddy/sites-enabled/saasforge Remote Caddy site files directory
SaaSForge — Backups & Health
SAASFORGE_BACKUPS_ENABLED true Enable scheduled runtime backups
SAASFORGE_BACKUPS_MAX_PER_TENANT 7 Max backups retained per tenant
SAASFORGE_HEALTH_CHECKS_ENABLED true Enable scheduled health checks
SAASFORGE_HEALTH_CURL_TIMEOUT 5 Health check HTTP timeout in seconds
SAASFORGE_HEALTH_FAILURE_THRESHOLD 3 Consecutive failures before instance marked unhealthy
SaaSForge — Domains & TLS
SAASFORGE_CADDY_ON_DEMAND_TLS_ENABLED true Enable Caddy on-demand TLS for custom domains
N8NForge — Instances
N8NFORGE_PRODUCT_TYPE n8nforge Product type for N8NForge instances
N8NFORGE_DEFAULT_N8N_VERSION latest Default n8n version tag for new instances
N8NFORGE_INSTANCE_SUBFOLDER / Subfolder path for instance URL
N8NFORGE_INSTANCE_RANDOM_SLUG_PREFIX inst Prefix for random slug when instance slug would repeat the tenant slug
N8NForge — Templates
N8NFORGE_AUTO_INSTALL_FEATURED true Auto-queue featured templates on new workspaces
N8NFORGE_AUTO_INSTALL_STATUS queued Status for auto-installed templates
N8NFORGE_MANUAL_INSTALL_STATUS queued Status for manually installed templates
N8NForge — Metrics
N8NFORGE_METRICS_POLLING false Enable background metrics polling via scheduler
N8NFORGE_METRICS_POLLING_INTERVAL 15 Minutes between metric collection cycles
N8NFORGE_METRICS_RETENTION_DAYS 60 Days to retain metric snapshots before cleanup
N8NForge — Updates
N8NFORGE_UPDATE_SERVER_URL empty Update server URL for platform releases
N8NFORGE_UPDATE_REQUEST_TIMEOUT 120 Timeout for update server requests
Admin versus env

Branding, billing keys, landing copy, and docs visibility are meant to be changed from admin. Runtime topology, Docker and proxy paths, domain defaults, and SSO settings remain infrastructure configuration.

Instance resource limits

Every n8n container gets default CPU, memory, and swap limits. These can be overridden per instance from the admin panel.

  • Open Automation → Managed Instances → Edit on the instance you want to adjust.
  • The Resource Overrides section lets you set CPU, memory, memory swap, and toggle task runners.
  • When saved, the instance restarts automatically to apply the new limits.

Metrics collection

Each n8n instance exposes a metrics endpoint that feeds the customer dashboard stat cards, history bars, and the admin Top Resource Consumers widget (shows the 10 instances with highest memory usage).

Metrics are captured automatically when customers view their dashboard. An optional background poller can be enabled via N8NFORGE_METRICS_POLLING=true to collect data across all instances regardless of dashboard activity. Snapshots are stored in the n8nforge_instance_metrics table.

Retention & cleanup

Old snapshots can be pruned automatically to keep the metrics table lean:

  • Retention window: controlled by N8NFORGE_METRICS_RETENTION_DAYS (default 60). Rows older than this threshold are candidates for deletion.
  • Manual cleanup: php artisan n8nforge:clean-metrics — add --dry-run to preview without deleting.
  • Scheduled cleanup: the framework console kernel registers n8nforge:clean-metrics on the daily schedule. Make sure the Laravel scheduler runs every minute via cron: * * * * * cd /path/to/project && php artisan schedule:run.

Plans and billing

Commercial delivery
  • Provider choose Stripe or Paddle in Setup.
  • Credentials save publishable, secret, and webhook values in encrypted storage.
  • Plan pricing enter monthly, yearly, or one-time provider price IDs on the plan resource.
  • Role assignment map each plan to the correct post-subscription role.
  • Free tier handling mark the fallback plan if unsubscribed users should keep some access.
  • Delivery policy use the plan resource to map provisioning servers, external databases, and external storage pools to each offer.
Plan reference behavior

SaaSForge stores the active Wave plan_id on the tenant as product_reference. That value is what server and runtime dependency selectors use when applying plan-level placement rules.

Provisioning model

Runtime orchestration

Provisioning is driver-based and job-driven. Initial deploy, suspend, resume, restart, redeploy, and domain sync are all tracked lifecycle jobs.

  • Driver selection persists as provisioning_driver on the tenant.
  • Server placement persists as provisioning_server_id so later health, backup, and lifecycle actions use the same host.
  • Dependency placement persists selected external database and storage backends so restarts and redeploys keep using the same infrastructure choices.
  • Provisioning jobs are the audit trail for all tracked runtime actions.
Provisioning resources
Local Linux-host driver

Uses the application host as the runtime host.

SSH server driver

Uses reusable remote provisioning servers selected from plan mappings.

Future providers

The same placement model can support later API-based runtime providers.

Provisioning servers

Provisioning servers define SSH connection settings, runtime defaults, reverse proxy behavior, and optional plan-level tenant capacity. Treat them as the actual hosts that carry tenant traffic.

How domain wiring actually works

Domain sync does not register domains with a DNS provider and does not use a wildcard Caddy listener with an ask endpoint back to the main app. Instead, SaaSForge writes a concrete Caddy site file for each tenant using that tenant's exact domain_hosts list, then reloads Caddy and updates the n8n container environment so N8N_HOST, N8N_EDITOR_BASE_URL, and WEBHOOK_URL point at the tenant's primary domain.

Provisioning server requirements
  • Docker and Compose must already be installed on the provisioning host and available to the configured deploy user.
  • Caddy must already be installed if you enable reverse-proxy management for that server. SaaSForge writes per-tenant site files and reloads Caddy, but it does not install or bootstrap Caddy for you.
  • A writable runtime root must exist for compose files, env files, logs, backups, and tenant data.
  • A shared Docker network must be available or creatable by the deploy user for tenant containers.
  • DNS must point tenant domains at the provisioning host or fronting load balancer before HTTPS traffic can succeed.
  • Firewall and ports must allow public HTTP/HTTPS traffic to the reverse proxy and local container traffic on the allocated host ports.
Admin setup workflow
  1. Prepare the host with Docker, Docker Compose, Caddy, and a deploy user that can write the runtime directories and reload Caddy.
  2. Point DNS for any tenant or base-domain records at that host or its fronting proxy/load balancer.
  3. Create a provisioning server in admin with the SSH connection, root path, Docker command names, network, and optional Caddy paths.
  4. Assign the server to plans so new tenants can be placed onto that host automatically.
  5. Run a real provisioning test and confirm the tenant compose files, Caddy site file, Caddy reload, HTTPS response, and n8n launch URL all succeed.
What SaaSForge does automatically
  • Creates tenant runtime files such as the compose file, env file, log path, backup path, and tenant data path under the configured runtime root.
  • Creates or updates a tenant-specific Caddy site file that contains the tenant's exact domain list and reverse proxies traffic to that tenant's allocated host port.
  • Starts, stops, restarts, or redeploys the tenant container with the configured Docker and Compose commands.
  • Reloads Caddy after writing or removing a tenant site file when reverse proxy management is enabled.
  • Updates n8n runtime URLs so the primary tenant domain becomes the editor base URL and webhook base URL.
What SaaSForge does not do for you
  • It does not install Docker or Docker Compose on the provisioning host.
  • It does not install or bootstrap Caddy on the provisioning host.
  • It does not create DNS records with Cloudflare, Route53, or any other DNS provider.
  • It does not create a wildcard or catch-all Caddy listener that asks the central app whether a domain is allowed.
  • It does not manage operating-system firewall rules or cloud load balancer listeners for you.
Recommended host preparation

Treat provisioning hosts as pre-built runtime targets. SaaSForge expects the server to be ready for deployments before you save it in admin.

#!/usr/bin/env bash
set -euo pipefail

DEPLOY_USER="${1:-deploy}"
HOSTNAME="$(hostname -f)"

_has_cmd() { command -v "$1" || return 1; }
_deb_installed() { dpkg -s "$1" || return 1; }
_install_if_missing() {
    local missing=()
    for pkg in "$@"; do
        _deb_installed "$pkg" || missing+=("$pkg")
    done
    [[ ${#missing[@]} -eq 0 ]] || sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${missing[@]}"
}

echo "=== 1. System updates ==="
sudo apt-get update -qq
sudo apt-get upgrade -y -qq
_install_if_missing curl ufw unattended-upgrades

if ! swapon --show | grep -q .; then
    sudo fallocate -l 1G /swapfile
    sudo chmod 600 /swapfile
    sudo mkswap /swapfile
    sudo swapon /swapfile
    echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
fi

echo "=== 2. Docker ==="
if ! _has_cmd docker; then
    for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do
        sudo apt-get remove -y "$pkg" || true
    done
    curl -fsSL https://get.docker.com | sudo bash
fi
_install_if_missing docker-compose-plugin

echo "=== 3. Docker daemon config ==="
sudo mkdir -p /etc/docker
printf '%s\n' '{"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"},"live-restore":true,"storage-driver":"overlay2"}' \
    | sudo tee /etc/docker/daemon.json
if _has_cmd dockerd; then
    sudo systemctl restart docker
fi

echo "=== 4. Caddy ==="
if ! _has_cmd caddy; then
    _install_if_missing debian-keyring debian-archive-keyring apt-transport-https
    sudo mkdir -p /usr/share/keyrings
    if [[ ! -f /usr/share/keyrings/caddy-stable-archive-keyring.gpg ]]; then
        curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
            | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
    fi
    if [[ ! -f /etc/apt/sources.list.d/caddy-stable.list ]]; then
        curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
            | sudo tee /etc/apt/sources.list.d/caddy-stable.list
    fi
    sudo apt-get update -qq
    _install_if_missing caddy
fi

echo "=== 5. Deploy user & runtime dirs ==="
sudo id -u "$DEPLOY_USER" || sudo useradd -m -s /bin/bash "$DEPLOY_USER"
sudo usermod -aG docker "$DEPLOY_USER"
sudo mkdir -p /var/lib/saasforge/runtime
sudo chown -R "$DEPLOY_USER":"$DEPLOY_USER" /var/lib/saasforge/runtime
sudo mkdir -p /etc/caddy/sites-enabled/saasforge

echo "=== 6. Kernel tuning ==="
printf 'fs.file-max = 100000\nvm.max_map_count = 262144\nnet.core.somaxconn = 1024\n' \
    | sudo tee /etc/sysctl.d/99-saasforge.conf
sudo sysctl --system

echo "=== 7. Firewall ==="
if sudo ufw status | grep -q 'Status: active'; then
    for port in ssh 80/tcp 443/tcp; do
        sudo ufw status | grep -q "$port" || sudo ufw allow "$port"
    done
else
    _install_if_missing ufw
    sudo ufw --force reset
    sudo ufw default deny incoming
    sudo ufw default allow outgoing
    sudo ufw allow ssh
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw --force enable
fi

echo "=== 8. Enable services ==="
sudo systemctl enable --now docker

# Write a minimal Caddyfile so caddy can start
if ! caddy validate --config /etc/caddy/Caddyfile; then
    printf ':80 {\n    respond "SaaSForge provisioning host"\n}\n\nimport /etc/caddy/sites-enabled/saasforge/*\n' \
        | sudo tee /etc/caddy/Caddyfile
fi
cd /etc/caddy && caddy start
echo "Caddy started in /etc/caddy/"

# unattended-upgrades is a timer, not a service
sudo systemctl enable unattended-upgrades || true
sudo systemctl start unattended-upgrades || true

echo "=== 9. Verification ==="
echo -n "Docker: "; sudo -u "$DEPLOY_USER" docker version --format '{{.Server.Version}}'
echo -n "Compose: "; sudo -u "$DEPLOY_USER" docker compose version --short
echo -n "Caddy: "; caddy version
echo -n "UFW: "; sudo ufw status | head -1

echo ""
echo "Host $HOSTNAME ready. Add it as a Provisioning Server in admin.
  Host     : $HOSTNAME
  Port     : 22
  Username : $DEPLOY_USER"
Caddy main config expectation

The main Caddy config should import the SaaSForge-managed site directory. SaaSForge writes individual tenant site files into that directory and then reloads Caddy. If you want on-demand TLS protection for tenant and custom domains, add the global on_demand_tls ask endpoint that points at the central app.

# Example /etc/caddy/Caddyfile excerpt
{
    email admin@example.com

    on_demand_tls {
        ask https://app.example.com/saasforge/caddy/on-demand-tls
    }
}

import /etc/caddy/sites-enabled/saasforge/*

When SAASFORGE_CADDY_ON_DEMAND_TLS_ASK_URL is configured, SaaSForge-generated site files switch to tls { on_demand } and expect the main Caddy config to expose the matching ask URL above.

DNS expectation

Point tenant domains or delegated subdomains at the provisioning host or its fronting load balancer before expecting HTTPS to succeed. Domain sync only updates the runtime and Caddy side after the hostname is already routed to the host.

First connection trust prompt

A brand-new SSH target can fail on its first deployment if the app host has never trusted that server before. When strict host key checking is enabled, trust the host in known_hosts first or disable strict checking temporarily for the first test.

External runtime dependencies

Database and storage pools
External databases

Managed PostgreSQL pools with host, port, database, credentials, schema, pool sizing, optional SSL material, and plan-level capacity mapping.

External storage

Managed S3-compatible binary-data storage backends with endpoint, protocol, bucket, region, credentials, and plan-level capacity mapping.

  • Fallback behavior remains local SQLite plus local filesystem storage when no external resource is assigned.
  • Tenant overrides can explicitly pin a tenant to a specific database or storage backend.
  • Plan mappings let you keep infrastructure placement policy on the product offer instead of per-tenant manual edits.
n8n storage licensing

External binary-data storage depends on n8n enterprise licensing. If you are not intentionally using that feature, leave storage unmapped and back up the tenant data directory on the runtime host.

Tenant operations

Day-two admin work
  • Tenant edit page is for ownership, plan changes, infrastructure overrides, and primary record maintenance.
  • Tenant instance page is for day-two operations such as health review, uptime visibility, backup creation, restart, and redeploy.
  • Provisioning feedback allows readiness callbacks to close a running lifecycle job after the runtime reports back.
  • Runtime metadata stores base URL, launch URL, host port, latest failure details, and dependency selections.
Use the instance page first

When a tenant looks unhealthy, open the instance page before taking action. Review the latest health snapshot, uptime, active host, latest backup, and last provisioning job so you can choose the right response.

Scheduler and commands

Required automation

This is one of the most important parts of setup. If the scheduler is not running, new tenants stay queued, template installs wait, health checks stop, backups stop, and scheduled account actions do not run.

Simple rule

You do not create one cron job for each command below.

You create one server cron job that runs php artisan schedule:run every minute. Laravel then runs the correct commands automatically.

1
Step 1

Find the app folder

Open the folder where N8Forge is installed. This is the folder that contains the artisan file.

2
Step 2

Test the scheduler once

Run php artisan schedule:run from that folder. If it works, you can set the server to run the same command automatically every minute.

3
Step 3

Add one cron job on the server

In your server panel or cron manager, create a job that runs every minute. Replace the sample path below with your real N8Forge install path and PHP path.

* * * * * cd /path/to/n8forge && /usr/bin/php artisan schedule:run >> /dev/null 2>&1

If your host asks for separate fields, use:

  • Frequency: every minute
  • Working folder: the N8Forge folder that contains artisan
  • Command: /usr/bin/php artisan schedule:run
4
Step 4

Confirm it is working

After a few minutes, create or update something that uses automation and confirm the related action moves forward. A newly created tenant should not remain stuck in queued for long if the scheduler is active.

If you use more than one app server

Run the cron job on only one main app server unless you have intentionally designed a clustered scheduler setup.

Command Schedule Purpose
subscriptions:cancel-expired Every hour Cancels expired subscriptions automatically
saasforge:send-suspension-reminders Every hour Sends scheduled suspension reminders
saasforge:process-provisioning Every minute Processes tenant create, restart, redeploy, suspend, and destroy jobs
saasforge:check-runtime-health Every five minutes Updates runtime health snapshots and instance status data
saasforge:run-backups Daily Creates tenant backups
n8nforge:process-template-installs Every minute Processes queued template installs for managed workspaces
accounts:process-deletions Daily Deletes accounts that reached the end of their deletion grace period
activity:clean Daily Removes old activity logs based on retention settings
Quick check after setup

Create a test tenant. If the scheduler is working, provisioning should begin shortly instead of sitting in queued.

If jobs stay queued

The cron job is missing, using the wrong path, using the wrong PHP binary, or running in the wrong folder.

If you are not technical

Send your hosting provider or server admin the cron command above and ask them to run it every minute on the N8Forge application server.

Backups and restore

Automated backups capture the full n8n data directory, including the SQLite database, credentials, and workflow files.

  • Scope: The entire /home/node/.n8n directory is tar'd, so the SQLite database is always included.
  • Schedule: saasforge:run-backups runs daily. Retention is configurable via SAASFORGE_BACKUPS_MAX_PER_TENANT (default 7).
  • Manual backup: From the instance edit page at Automation → Managed Instances → Edit → Backup.
  • Restore: From the instance view page (Automation → Managed Instances → {instance} → View), scroll to the Backups section and click Restore on a succeeded backup. The container stops, the data directory is replaced, and the container restarts.
  • Restore status is shown as a badge on the backup card. All restore operations are logged to the activity log.
  • SSH driver: Backup files are downloaded locally, then uploaded back to the remote server during restore. Both Docker and SSH drivers are supported.

Activity log

A unified activity log at SaaS → Activity Log shows all platform operations in reverse chronological order.

Action type Source What it tracks
saasforge_job_* Provisioning Every job start, success, and failure
n8nforge_template_* Templates Template installs and failures
saasforge_runtime_backup_* Backups Backup queued, completed, or failed
saasforge_runtime_restore_* Restore Restore started, completed, or failed
saasforge_runtime_health_* Health Health check results
saasforge_runtime_*_queued Actions Restart, redeploy, or destroy queued

Filter by action type or time period (today, yesterday, last 7/30 days). Error details are shown inline. Logs older than the configured retention period are cleaned automatically.

Production requirement

Without the every-minute scheduler cron job, provisioning, template delivery, reminders, cleanup, backups, health monitoring, and scheduled account actions will drift or stop.

Updates and deployment tooling

Release operations
  • Admin → Updates verifies the purchase code, checks for the latest release, and applies downloads when configured.
  • php artisan n8forge:update runs pending update tasks for the current install.
  • php artisan n8forge:update --download downloads and applies the latest release before migrations and cache refresh.
  • ops/scripts/build-mainfiles.sh builds install and update packages.
  • ops/scripts/push-demo.sh helps deploy the N8Forge app itself, not tenant runtimes.
Separation of concerns

The ops/ scripts are for deploying the N8Forge application. They are not a replacement for SaaSForge provisioning servers or tenant runtime placement.

Maintenance workflow

Recommended routine
  • Daily review provisioning jobs, health checks, failed backups, and account notifications. Open the instance page for any degraded workspace.
  • After plan changes verify billing IDs, role mapping, and provisioning, database, and storage assignments.
  • After server changes confirm SSH credentials, Docker access, proxy paths, and capacity limits.
  • After dependency changes validate one real tenant deployment before treating the change as complete.
  • Before updates take backups and verify restore confidence for both the app and tenant runtime data.
Activity logging

Keep ACTIVITY_LOG_ENABLED active if you need an audit trail for runtime actions such as health checks, backups, restart, or redeploy operations.

Troubleshooting

Common admin issues
A subscription does not create a tenant

Check the active plan, subscription status, role mapping, and whether the observer, queue, and scheduler path is functioning.

A tenant is queued but not provisioning

Confirm saasforge:process-provisioning is scheduled and that the selected driver, server, and optional runtime dependencies are valid.

SSH provisioning fails immediately

Verify host, port, username, key or password, host-key policy, Docker access, remote paths, and whether the deploy user can write files and reload the reverse proxy.

A domain saves but still does not open

Check that DNS points at the correct host, the tenant Caddy site file exists on that provisioning server, Caddy reloaded successfully, and the primary domain matches the n8n runtime base URL. There is no central wildcard ask-endpoint flow in the current implementation.

A runtime fails after enabling external database or storage

Check credentials, SSL settings, S3 region formatting, enterprise licensing, and whether the plan or tenant still points at the intended dependency pool.

An instance looks unhealthy or stale

Open the tenant instance page first, review the last health result and active host, then decide between health check, backup, restart, or redeploy.