n8n-install
CLAUDE.md953 starsChanged 22 days agoApache-2.0View on GitHub
This is Selfhost AI (repository selfhost-ai, formerly n8n-install), a Docker Compose-based installer that provides a comprehensive self-hosted environment for n8n workflow automation and numerous AI/automation services. The installer includes an interactive wizard, automated secret generation, and integrated HTTPS via Caddy.
- Reads credentials
- Deletes or force-pushes
We split this CLAUDE.md into the rules it gives an agent
37 rules. About 0 are common, 8 are distinctive, and 29 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈8
Distinctive · estimate
This author's own choices. Worth a look.
29
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 49
Project Name: All docker-compose commands use -p localai (defined in Makefile as PROJECT_NAME := localai).
- Very specific: line 105
Follow this workflow when adding a new optional service (refer to .claude/commands/add-new-service.md for complete details):
- Very specific: line 107
docker-compose.yml: Add service with profiles: ["myservice"], restart: unless-stopped.
- Very specific: line 108
Caddyfile: Add reverse proxy block using {$MYSERVICE_HOSTNAME}.
- Very specific: line 109
.env.example: Add MYSERVICE_HOSTNAME=myservice.yourdomain.com and credentials if using basic auth.
- Very specific: line 111
scripts/04_wizard.sh: Add service to base_services_data array for wizard selection.
- Very specific: line 112
scripts/databases.sh: If service uses PostgreSQL, add database name to INIT_DB_DATABASES array.
- Very specific: line 113
scripts/generate_welcome_page.sh: Add service to SERVICES_ARRAY for welcome dashboard.
- Very specific: line 114
welcome/app.js: Add SERVICE_METADATA entry with name, description, icon, color, category.
- Very specific: line 115
scripts/07_final_report.sh: Add service URL and credentials output using is_profile_active "myservice".
- Very specific: line 116
README.md: Add a row to the matching category table under "What's Included".
- Very specific: line 117
CHANGELOG.md: Add entry under ## [Unreleased] → ### Added (new service = minor version bump).
- Distinctive: line 153
Add comparison link at bottom of file:
- Very specific: line 157
Update [Unreleased] link to compare from new version
- Very specific: line 164
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true: All executions (including manual tests) run on workers
- Very specific: line 187
All instances share the ollama_storage volume, so models are downloaded once.
- Very specific: line 188
No published ports and no Caddy block for extra instances - they are internal (ollama2:11434).
- Very specific: line 225
No Alertmanager: alert rules are Grafana-managed (grafana/provisioning/alerting/n8n-workflows.yml).
- Very specific: line 234
Never add ports: to services in docker-compose.yml.
- Very specific: line 245
All external compose files use the same project name (-p localai) so containers appear together
- Very specific: line 341
Use depends_on with conditions:
- Very specific: line 352
All secrets/passwords end with _PASSWORD or _KEY
- Very specific: line 353
All hostnames end with _HOSTNAME
- Very specific: line 355
Use ${VAR:-default} for optional vars with defaults
- Very specific: line 356
Always wrap ${VAR} interpolations in docker-compose.yml in double quotes: "${VAR:-default}"
- Very specific: line 388
GOST_NO_PROXY: ALL service container names must be listed in GOST_NO_PROXY in .env.example.
- Very specific: line 396
Always use json_escape "$VAR" when building JSON to handle special characters
- Very specific: line 412
Ensure profile is added to COMPOSE_PROFILES in .env
- Distinctive: line 414
Verify no port conflicts (no services should publish ports)
- Distinctive: line 415
Ensure healthcheck is properly defined if service has dependencies
- Distinctive: line 419
Check Caddy logs for certificate acquisition errors
- Very specific: line 420
Verify LETSENCRYPT_EMAIL is set in .env
- Distinctive: line 472
When modifying installer scripts:
- Distinctive: line 473
Test on a clean Ubuntu 24.04 LTS system (minimum 4GB RAM / 2 CPU)
- Distinctive: line 474
Verify all profile combinations work
- Very specific: line 475
Check that .env is properly generated
- Distinctive: line 477
Test update script preserves custom configurations
≈ “Common” is an estimate: at least 1 in 5 of the 25,680 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
This is Selfhost AI (repository selfhost-ai, formerly n8n-install), a Docker Compose-based installer that provides a comprehensive self-hosted environment for n8n workflow automation and numerous AI/automation services. The installer includes an interactive wizard, automated secret generation, and integrated HTTPS via Caddy.
Core Architecture
- Profile-based service management: Services are activated via Docker Compose profiles (e.g.,
n8n,flowise,monitoring). Profiles are stored in the.envfile'sCOMPOSE_PROFILESvariable. - No exposed ports: Services do NOT publish ports directly. All external HTTPS access is routed through Caddy reverse proxy on ports 80/443.
- Shared secrets: Core services (Postgres, Valkey (Redis-compatible, container named
redisfor backward compatibility), Caddy) are always included. Other services are optional and selected during installation. - Queue-based n8n: n8n runs in
queuemode with Redis, Postgres, and dynamically scaled workers (N8N_WORKER_COUNT).
Key Files
Makefile: Common commands (install, update, logs, etc.)docker-compose.yml: Service definitions with profilesCaddyfile: Reverse proxy configuration with automatic HTTPS.env: Generated secrets and configuration (from.env.example)scripts/install.sh: Main installation orchestrator (runs numbered scripts 01-08 in sequence)scripts/utils.sh: Shared utility functions (sourced by all scripts viasource "$(dirname "$0")/utils.sh" && init_paths)scripts/01_system_preparation.sh: System updates, firewall, security hardeningscripts/02_install_docker.sh: Docker and Docker Compose installationscripts/git.sh: Git utilities (sync with origin, branch detection, configuration)scripts/03_generate_secrets.sh: Secret generation and bcrypt hashingscripts/04_wizard.sh: Interactive service selection using whiptailscripts/05_configure_services.sh: Service-specific configuration logicscripts/databases.sh: Creates isolated PostgreSQL databases for services (library)scripts/telemetry.sh: Anonymous telemetry functions (Scarf integration)scripts/06_run_services.sh: Starts Docker Compose stackscripts/07_final_report.sh: Post-install credential summaryscripts/08_fix_permissions.sh: Fixes file ownership for non-root access. Skips the container-written data directories undersupabase/docker/volumes(db/data,storage) anddify/docker/volumes(default stack plus every optional vector store's data;CONTAINER_OWNED_DIRS; supabase/postgres cannot read its data as the host user) and, as a backstop, gives Supabase Postgres files back to the uidsupabase-dbreportsscripts/generate_n8n_workers.sh: Generates dynamic worker/runner compose file and the Prometheus n8n targets filescripts/generate_ollama_instances.sh: Generates extra Ollama instances compose file (multi-GPU)scripts/update.sh: Update orchestrator (syncs with origin and updates images)scripts/update_preview.sh: Preview available updates without applying (dry-run)scripts/pull_images.sh: Image pull without the rest of the update (pooled, then per-service retries if that fails); skips locally built services (n8n, runners, OpenClaw) and recreates nothing (used bymake pull)scripts/doctor.sh: System diagnostics (DNS, SSL, containers, disk, memory)scripts/apply_update.sh: Applies updates after git syncscripts/docker_cleanup.sh: Removes unused Docker resources (used bymake clean)scripts/import_workflows.sh: Imports workflows fromn8n/backup/workflows/into n8n (used bymake import)scripts/restart.sh: Restarts services with proper compose file handling (used bymake restart)scripts/setup_custom_tls.sh: Configures custom TLS certificates (used bymake setup-tls); supports--removeto revert to Let's Encryptscripts/setup_sysbox.sh: Installs Sysbox (sysbox-runc) for the n8n Assistant sandbox runner; called by05_configure_services.shwhen then8n-sandboxprofile is activestart_services.py: Python orchestrator for service startup order, builds Docker images, handles external services (Supabase/Dify cloning, env preparation, startup), generates SearXNG secret key, stops existing containers. Usespython-dotenv(dotenv_values).
Project Name: All docker-compose commands use -p localai (defined in Makefile as PROJECT_NAME := localai).
Version: Stored in VERSION file at repository root.
Installation Flow
scripts/install.sh orchestrates the installation by running numbered scripts in sequence:
01_system_preparation.sh- System updates, firewall, security hardening02_install_docker.sh- Docker and Docker Compose installation03_generate_secrets.sh- Generate passwords, API keys, bcrypt hashes04_wizard.sh- Interactive service selection (whiptail UI)05_configure_services.sh- Service-specific configuration06_run_services.sh- Start Docker Compose stack07_final_report.sh- Display credentials and URLs08_fix_permissions.sh- Fix file ownership for non-root access
The update flow (scripts/update.sh) similarly orchestrates: git fetch + reset → service selection → apply_update.sh → restart. During updates, 03_generate_secrets.sh adds new variables from .env.example without regenerating existing ones (preserves user-set values). Note the --update flag apply_update.sh passes is not parsed - the behavior is unconditional, and code that needs to tell a fresh install from an upgrade tests ${#existing_env_vars[@]} -gt 0 instead (see the DB_MIGRATION_VARS and OPEN_WEBUI_DATABASE blocks).
Git update modes: Default is reset (hard reset to origin). Set GIT_MODE=merge in .env for fork workflows (merges from upstream instead of hard reset). The make git-pull command uses merge mode. Git branch support is explicit: GIT_SUPPORTED_BRANCHES=("main" "develop") in git.sh; unknown branches warn and fall back to main.
Common Development Commands
Makefile Commands
make install # Full installation (runs scripts/install.sh)
make update # Update system and services (resets to origin)
make update-preview # Preview available updates (dry-run)
make pull # Pull prebuilt images only (then make restart)
make git-pull # Update for forks (merges from upstream/main)
make clean # Remove unused Docker resources (preserves data)
make clean-all # Remove ALL Docker resources including data (DANGEROUS)
make logs # View logs (all services)
make logs s=<service> # View logs for specific service
make status # Show container status
make monitor # Live CPU/memory monitoring (docker stats)
make restart # Restart all services
make stop # Stop all services
make start # Start all services
make show-restarts # Show restart count per container
make doctor # Run system diagnostics (DNS, SSL, containers, disk, memory)
make import # Import n8n workflows from backup
make import n=10 # Import first N workflows only
make setup-tls # Configure custom TLS certificates
make openclaw a="..." # Run an OpenClaw CLI command (e.g. a="pairing approve telegram <CODE>")
make switch-beta # Switch to develop branch and update
make switch-stable # Switch to main branch and update
make help # Show all available commandsAdding a New Service
Follow this workflow when adding a new optional service (refer to .claude/commands/add-new-service.md for complete details):
- docker-compose.yml: Add service with
profiles: ["myservice"],restart: unless-stopped. Do NOT expose ports. - Caddyfile: Add reverse proxy block using
{$MYSERVICE_HOSTNAME}. Consider if basic auth is needed. Add the"myservice MYSERVICE_HOSTNAME"pair toDNS_PROFILE_HOSTNAMESinscripts/doctor.shsomake doctorchecks its DNS. - .env.example: Add
MYSERVICE_HOSTNAME=myservice.yourdomain.comand credentials if using basic auth. - scripts/03_generate_secrets.sh: Generate passwords and bcrypt hashes. Add to
VARS_TO_GENERATEmap. - scripts/04_wizard.sh: Add service to
base_services_dataarray for wizard selection. - scripts/databases.sh: If service uses PostgreSQL, add database name to
INIT_DB_DATABASESarray. Database creation is idempotent (checks existence before creating). Note: Postiz also requirestemporalandtemporal_visibilitydatabases. - scripts/generate_welcome_page.sh: Add service to
SERVICES_ARRAYfor welcome dashboard. - welcome/app.js: Add
SERVICE_METADATAentry with name, description, icon, color, category. - scripts/07_final_report.sh: Add service URL and credentials output using
is_profile_active "myservice". - README.md: Add a row to the matching category table under "What's Included". Longer setup notes go into
docs/<service>.md(linked from the row and the "Guides" list), not into README. - CHANGELOG.md: Add entry under
## [Unreleased]→### Added(new service = minor version bump).
Always ask users if the new service requires Caddy basic auth protection.
Versioning (CHANGELOG.md)
This project uses Semantic Versioning. When updating CHANGELOG.md:
Version Format: MAJOR.MINOR.PATCH
| Type | When to bump | Examples |
| MAJOR (X.0.0) | Breaking changes that require user action | n8n 2.0 migration, config format changes, removed features |
| MINOR (0.X.0) | New services or features (backward compatible) | Adding NocoDB, new wizard options, new Makefile commands |
| PATCH (0.0.X) | Bug fixes (backward compatible) | Healthcheck fixes, proxy bypass fixes, typo corrections |
Changelog Entry Format
## [Unreleased]
## [2.6.0] - 2026-01-15
### Added
- **NewService** - Brief description of what it provides
### Changed
- Description of modified behavior
### Fixed
- Description of bug fixAfter Release
- Move items from
[Unreleased]to new version section - Add comparison link at bottom of file:
[2.6.0]: https://github.com/kossakovsky/selfhost-ai/compare/v2.5.3...v2.6.0 - Update
[Unreleased]link to compare from new version
Important Service Details
n8n Configuration (v2.0+)
- n8n runs in
EXECUTIONS_MODE=queuewith Redis as the queue backend - OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true: All executions (including manual tests) run on workers
- Worker-Runner Sidecar Pattern: Each worker has its own dedicated task runner
- Workers and runners are generated dynamically via
scripts/generate_n8n_workers.sh - Configuration stored in
docker-compose.n8n-workers.yml(auto-generated, gitignored) - Runner connects to its worker via
network_mode: "service:n8n-worker-N"(localhost:5679) - Runner image
n8nio/runnersmust match n8n version
- Workers and runners are generated dynamically via
- Template profile pattern:
docker-compose.ymldefinesn8n-worker-templateandn8n-runner-templatewithprofiles: ["n8n-template"](never activated directly).generate_n8n_workers.shuses these as templates to generatedocker-compose.n8n-workers.ymlwith the actual worker/runner services. - Scaling: Change
N8N_WORKER_COUNTin.envand runbash scripts/generate_n8n_workers.sh. The generator also removes worker/runner containers above the count (cleanup_stale_n8n_workersin utils.sh): the rewritten compose file no longer lists them, sodownwould leave them running as orphans that keep taking queue jobs - Code node libraries: Configured via
n8n/n8n-task-runners.jsonandn8n/Dockerfile.runner:- JavaScript runner: packages installed via
pnpm addin Dockerfile.runner; allowlist inn8n-task-runners.json(NODE_FUNCTION_ALLOW_EXTERNAL,NODE_FUNCTION_ALLOW_BUILTIN); default packages:cheerio,axios,moment,lodash - Python runner: also configured in
n8n-task-runners.json; uses/opt/runners/task-runner-python/.venv/bin/pythonwithN8N_RUNNERS_STDLIB_ALLOW: "*"andN8N_RUNNERS_EXTERNAL_ALLOW: "*"
- JavaScript runner: packages installed via
- Workflows can access the host filesystem via
/data/shared(mapped to./shared) N8N_BLOCK_ENV_ACCESS_IN_NODE=falseallows Code nodes to access environment variables
Ollama Multi-Instance (multi-GPU)
OLLAMA_INSTANCE_COUNTin.env(default 1, max 8) drivesscripts/generate_ollama_instances.sh, which writesdocker-compose.ollama-instances.yml(auto-generated, gitignored)- Template profile pattern, same as the n8n workers:
docker-compose.ymldefinesollama-instance-templateandollama-instance-template-amdwithprofiles: ["ollama-template"](never activated directly) - The templates deliberately carry no
deploy:block. Compose appendsdeploy.resources.reservations.devicesacrossextends, so a count-based reservation on the template would leak an extra, arbitrary GPU into every instance that pins specific device IDs. Do not add one. - Instance 1 is the stock
ollamacontainer and is never modified; count 1 generates nothing and removes a stale file, which otherwise resurrects the old instance set after a downscale - Per-instance tuning uses
OLLAMA<N>_*variables emitted as nested defaults (${OLLAMA2_KEEP_ALIVE:-${OLLAMA_KEEP_ALIVE:-}}), so users tune.envand restart without regenerating - Anything else (llama.cpp
LLAMA_ARG_*, ROCm vars) comes from optional, gitignored env files:ollama.envon thex-ollamaanchor (inherited by every instance viaextends) andollama<N>.envemitted per instance, bothrequired: false. Never map such variables throughenvironment:with${VAR:-}- an emptyLLAMA_ARG_FIT_TARGETmakes llama-server exit at startup (std::stoull("")), andenvironment:keys override the files - NVIDIA pins via
deploy.…device_ids; AMD pins viaHIP_VISIBLE_DEVICES/ROCR_VISIBLE_DEVICES(ROCm passes all devices through) - All instances share the
ollama_storagevolume, so models are downloaded once; only instance 1 has a model-pull job - No published ports and no Caddy block for extra instances - they are internal (
ollama2:11434);caddy-addon/site-*.confis the documented extension point - The generator is invoked unconditionally from
05_configure_services.sh, which covers install andmake update, and self-heals a stale file after a hardware-profile switch
n8n Assistant sandbox (n8n-sandbox profile)
- Upstream n8n-sandbox-service as three services in
docker-compose.yml: one-shotsandbox-certs(mTLS bootstrap into then8n_sandbox_tlsvolume, skips when certs exist),sandbox-api(HTTP 8080 + gRPC 9090) and the Docker-in-Dockersandbox-runner-1. No ports, no Caddy block; n8n reachessandbox-apiby service name. The cert SANs are the service namessandbox-api/sandbox-runner-1- do not rename them. Exactly one runner by design. - All three images share
N8N_SANDBOX_VERSION(defaultlatest, like most images here). The runner pulls the sandbox image into its inner Docker on first use, and deliberately has no volume for that inner Docker: a persisted cache would keep an old sandbox image whilemake updatemoves api and runner forward. The cost is one ~330 MB pull after every recreate. - Three secrets, each referenced on both sides from one
.envvar:N8N_SANDBOX_API_KEY(n8n'sN8N_SANDBOX_SERVICE_API_KEY= api'sSANDBOX_API_KEYS),N8N_SANDBOX_RUNNER_REGISTRATION_TOKEN,N8N_SANDBOX_RUNNER_API_KEY. - The n8n env anchor carries
N8N_INSTANCE_AI_SANDBOX_ENABLED(writtentrue/falseby05_configure_services.shfrom the profile), the sandbox URL/key,N8N_INSTANCE_AI_SEARXNG_URL(set tohttp://searxng:8080whilesearxngis active, cleared otherwise; a custom value is left alone) andN8N_ENABLED_MODULES(empty by default -instance-aiis default-on in n8n, and an unknown module name stops n8n from booting, so never default it to a name). There is deliberately nodepends_onfromn8ntosandbox-api: a dependency on a profile-gated service breaks compose when the profile is off. The model/API key is configured in the n8n UI, not in compose. - Runner isolation:
runtime: "${N8N_SANDBOX_RUNNER_RUNTIME:-runc}"+privileged: "${N8N_SANDBOX_RUNNER_PRIVILEGED:-false}"(compose casts the interpolated string to boolean).05_configure_services.shrunssetup_sysbox.sh; on success it writessysbox-runc/false, otherwise it asks (wt_yesno, default No) before writingrunc/true, and drops the profile on No. It also loadsbr_netfilterin both modes (the sandbox egress policy needs it) and persists it in/etc/modules-load.d/n8n-sandbox.conf. Without root, 05 exits 1 for this profile instead of configuring a runner that cannot start. setup_sysbox.shmust stay non-interactive and must not restart Docker: thesysbox-cepackage refuses to install while any container exists unless/etc/docker/daemon.jsonalready has pretty-printedbipanddefault-address-poolskeys, so the script pre-seeds them with Docker's current values viajq --indent 4and then the package only registers the runtime and SIGHUPs dockerd. Hosts with a custom Docker network setup (-b/--bridge/--bip/--default-address-pool/--fixed-cidrflags;bridge,fixed-cidr,fixed-cidr-v6oripv6: truein daemon.json; nodocker0) are rejected instead of guessed at. An already installed Sysbox is re-tested with a container on every run, never trusted fromdpkgstate alone.jqis not installed by 01/02 - only this script may rely on it, after its own apt step.make doctorerrors when.envsayssysbox-runcbut Docker lacks the runtime, and warns while the runner is privileged.
Open Terminal (open-terminal profile)
- Single service
open-terminal(ghcr.io/open-webui/open-terminal, tag fromOPEN_TERMINAL_VERSION, defaultlatest; only the full image -latestor a release tag like0.12.5- supports multi-user and runtime installs, the*-slim/*-alpine/openshiftvariants do not). No ports, no Caddy block; Open WebUI reacheshttp://open-terminal:8000. The wizard drops the profile whenopen-webuiis not selected, same pattern asn8n-sandbox/n8n. - The connection is configured only in the Open WebUI Admin UI (Admin Settings → Integrations → Open Terminal, URL +
OPEN_TERMINAL_API_KEY). Do not pre-seed it viaTERMINAL_SERVER_CONNECTIONS: Open WebUI seeds that key into its DB only when the key is missing, i.e. on a brand-new database, and a connection withoutconfig.access_grantsis admin-only anyway. Open WebUI proxies every request server-side and sendsX-User-Id, which multi-user mode maps to a Linux account under/home- the volume must be/home, not/home/user. - Never add
OPEN_TERMINAL_ALLOWED_DOMAINSto the compose file. The entrypoint distinguishes unset (full egress) from set-but-empty (drop all outbound) and runssudo iptablesunderset -e, which needsNET_ADMIN; an interpolated${VAR:-}would crash the container. Egress filtering is adocker-compose.override.ymljob together withcap_add: [NET_ADMIN]. - Never mount
docker.sock: the entrypoint auto-joins the socket's group and the agent's shell would control the host daemon. The server process runs asuserwith passwordless sudo, so nocap_drop(apt, useradd and sudo need CHOWN/SETUID/SETGID/DAC_OVERRIDE). Provisioned per-user accounts are plainuseraddusers without sudo; onlyOPEN_TERMINAL_MULTI_USER=falsegives the chat the sudo-capable shell, and only then are the startup package lists optional. The boundary is the network and the Open WebUI access grants. Healthcheck usescurlagainst the unauthenticated/docs. 05_configure_services.shrefuses aslim/alpine/openshiftvariant tag (bare or-suffixed) whileOPEN_TERMINAL_MULTI_USER=true: those entrypoints ignore the multi-user and package variables, so every user would silently share one shell.
OpenClaw (openclaw profile)
- Image built from
openclaw/Dockerfile(ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}plus the static Docker CLI; the base image currently shipsopenssh-client, whichssh hostdepends on). Runs asnode(uid 1000), state in theopenclaw_datavolume on/home/node/.openclaw, gateway and Control UI onopenclaw:18789. - Dashboard login is Caddy basic auth only (
OPENCLAW_USERNAME/OPENCLAW_PASSWORD). The gateway runs withgateway.auth.mode=trusted-proxy: Caddy sendsheader_up X-Forwarded-User {http.auth.user.id}toopenclaw-gateway:18789,trustedProxy.allowUsersis${OPENCLAW_USERNAME}, anddeviceAutoApprove(includingoperator.admin, so the owner can configure models/channels) removes the browser pairing screen. The trust boundary is theopenclaw-proxynetwork: only caddy, openclaw and openclaw-init join it,openclaw-gatewayis an alias on that network only, andgateway.trustedProxiesis exactly its subnet. That subnet includes the bridge gateway (.1), i.e. the host: any process on the host, of any user, can forge the header. Accepted because the stack assumes a single-admin server whose users already have root/docker access; pin Caddy's IP to /32 if that changes. Never widentrustedProxiesto the default network or RFC1918 ranges - every container could then become admin (root on the host).OPENCLAW_GATEWAY_PASSWORDis env-only, for local loopback CLI calls (make openclaw) and the password-login fallback; never writegateway.auth.passwordinto openclaw.json.dangerouslyDisableDeviceAuthis retired and inert andallowInsecureAuthis no longer in the strict schema; do not use them. openclaw-init(one-shot, same image, runs as root,openclaw/init.sh) runs on everydocker compose up, not when Docker restarts the gateway on its own. It copiesopenclaw/ssh/into theopenclaw_sshvolume owned by node, reads theopenclaw-proxysubnet from its only non-loopback IPv4 interface (it joins no other network;node -eoveros.networkInterfaces(), since iproute2 is not guaranteed in the base image), then as node runsconfig set --batch-jsonto enforce onlygateway.mode,gateway.bind,gateway.auth.mode,gateway.auth.trustedProxy,gateway.controlUi.allowedOrigins(https://${OPENCLAW_HOSTNAME}, written literally and substituted by OpenClaw) andgateway.trustedProxies. It setschannels.telegram.enabled=trueonly while the.envtoken is set and never forces it off. If the subnet lookup fails, the trusted-proxy batch is rejected, or the container lacksOPENCLAW_INIT_PROXY_NETWORK=true(a pre-1.14.1 container on the default network, reused bymake start), it switches the gateway to password auth withtrustedProxies: [](a subnet from an earlier run may now belong to another network; as a last resort it clears onlytrustedProxies) and says which state it reached. These errors printopenclaw-init: ERRORbut exit 0, so an upstream schema change cannot abortcompose upfor the whole stack - except when not eventrustedProxiescan be cleared, where it exits 1 so the gateway (waiting onservice_completed_successfully) does not start with a possibly stale trust;openclaw_init_failed(utils.sh; used by doctor, 07 and restart.sh) catches them and non-zero exits. Everything else belongs to the user in the dashboard; check the upstream schema before adding keys.- Telegram:
TELEGRAM_BOT_TOKENenv fallback fromOPENCLAW_TELEGRAM_BOT_TOKEN, long polling, defaultdmPolicy: pairing. - OpenRouter:
05_configure_services.shasks forOPENCLAW_OPENROUTER_API_KEY(asked like the Telegram token: visible input, only while empty in.env; unlike it, surrounding whitespace is trimmed, a value outside[A-Za-z0-9_-]is rejected because a quote would break.envfor compose, and a missingsk-or-prefix warns - checked on every run, also for a reused value), passed only to the gateway (notopenclaw-init) asOPENROUTER_API_KEYand never written to openclaw.json; the model stays a dashboard choice (Settings → Models → Defaults for all agents). Upstream fixes the OpenRouter OAuth callback tohttp://localhost:3000(extensions/openrouter/oauth.ts, no override), so the dashboard's OAuth flow cannot complete behind Caddy. - Full host access by design:
/var/run/docker.sockwithgroup_add: OPENCLAW_DOCKER_GID(05 writes it fromstat -c %g /var/run/docker.sock;group_addhas no compose default for it).05_configure_services.sh(root required) generatesopenclaw/ssh/(gitignored, owned by root or the installing user). Only the rootopenclaw-initbind-mounts it (read-only); the gateway gets a node-owned copy in theopenclaw_sshvolume, because a gateway bind mount would need uid 1000 ownership on the host, which08_fix_permissions.shwould undo. 05 writes anssh hostconfig pointing athost.docker.internalwith the port fromsshd -Tas the real user and authorizes the key in that user'sauthorized_keyswith theopenclaw@selfhost-aimarker (derived from the private key withssh-keygen -y, so no.pubfile is trusted). The marker line is re-added on every run after making sure the file ends with a newline, removed from the previous account when the detected user changes (OPENCLAW_SSH_USER), and removed by the next install /make updatewith the profile off.
Monitoring (Prometheus + Grafana)
- n8n metrics are enabled in the
x-n8nanchor:N8N_METRICSplusN8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS/_WORKFLOW_ID_LABEL/_WORKFLOW_NAME_LABEL/_WORKFLOW_INFO. They exposen8n_workflow_{started,success,failed,cancelled}_total{workflow_id,workflow_name}and then8n_workflow_info/n8n_active_workflow_infoid-to-name gauges (leader main only). The alerts and recording rules also usen8n_workflow_execution_duration_seconds{status,mode,workflow_id}, which is on by default (N8N_METRICS_INCLUDE_WORKFLOW_EXECUTION_DURATION); turning it off silently disables them. SettingN8N_METRICS_PREFIXwould break every panel and alert - In queue mode the workflow counters and the duration histogram are emitted by
n8nmain, which finalises every top-level execution; workers only report sub-workflow executions, node events and their own process metrics. Panels and rules therefore aggregate across instances (sum by (...)) and must not filter on$instance - Scrape targets are not static:
generate_n8n_workers.shwritesprometheus/targets/n8n.json(gitignored) with then8n:5678main target and onen8n-worker-N:5678per worker, each group carrying itsjoblabel, andprometheus.ymlreads it viafile_sd_configs. The generator runs unconditionally from05_configure_services.shand removes the file when the n8n profile is inactive, so a monitoring-only install has no n8n targets and no permanently-down alert. Metrics are served on the queue-health port (QUEUE_HEALTH_CHECK_PORT, default 5678; 5679 is the task broker) prometheus/rules/*.ymlholds recording rules keyed onworkflow_id(n8n:workflow_success:increase5m,n8n:workflow_last_success_timestamp_seconds); names are joined at query time fromn8n_active_workflow_info, so renames are followed. The wholeprometheus/directory is bind-mounted read-only to/etc/prometheus- Grafana's datasource, dashboards and alert rules are file-provisioned from
grafana/provisioning/; contact points and notification policies are not, and no SMTP is configured. The Prometheus datasource has the fixeduid: Prometheusthat dashboards and alert rules reference - keep them in sync. Grafana cannot change the uid of an existing datasource, somain.ymldeletes it by name (deleteDatasources) and recreates it on every start; pre-1.10.0 installations had a random uid and crash-looped without this. Use$__rate_interval, never a fixed range window, in range queries - No Alertmanager: alert rules are Grafana-managed (
grafana/provisioning/alerting/n8n-workflows.yml); a malformed file stops Grafana from starting.make doctorchecks that Grafana/Prometheus run, that the targets file matchesN8N_WORKER_COUNTand that Prometheus loaded then8n-workflowsrule group without errors - cAdvisor runs without the
disk/diskIOmetrics (nocontainer_fs_*/container_blkio_*; nothing provisioned uses them): thediskmetric's du over every container's layers swap-thrashed small hosts with many images (issue #150).--disable_metricsreplaces cAdvisor's default list instead of adding to it, so the flag repeats that list - keep it whole when editing and re-sync it with upstream's default when bumping the image /metricsmust never be public. n8n serves it unauthenticated on the UI port (5678) with every workflow name and ID, so the n8n Caddy block hasrespond /metrics* 404beforereverse_proxy- do not remove it. Prometheus scrapesn8n:5678over the Docker network and is unaffected. Cloudflare Tunnel bypasses Caddy, socloudflare-instructions.mddocuments a WAF rule instead.make doctorrequests/metricsfrom the local Caddy (curl --resolve N8N_HOSTNAME:443:127.0.0.1): 200 is an error, only 404 is OK, anything else warns; withcloudflare-tunnelactive the local 000 is only info (Caddy gets no certificate for a host the tunnel routes to n8n), and it also requests the public URL: 200 error, 401/403/404 or a redirect to*.cloudflareaccess.comOK, acf-mitigated: challengeresponse warns (a browser that passes the challenge still sees the metrics), anything else warns
Caddy Reverse Proxy
- Automatically obtains Let's Encrypt certificates when
LETSENCRYPT_EMAILis set - Hostnames are passed via environment variables (e.g.,
N8N_HOSTNAME,FLOWISE_HOSTNAME) - Basic auth uses bcrypt hashes generated by
scripts/03_generate_secrets.shvia Caddy's hash command - Never add
ports:to services in docker-compose.yml; let Caddy handle all external access - Caddy Addons (
caddy-addon/): Extend Caddy config without modifying the main Caddyfile. Files matchingsite-*.confare auto-imported (gitignored, user-created). TLS is controlled viatls-snippet.conf(all service blocks useimport service_tls). Seecaddy-addon/README.mdfor details. - Custom TLS certificates go in
certs/directory (gitignored), referenced as/etc/caddy/certs/inside the container
External Compose Files (Supabase/Dify)
Complex services like Supabase and Dify maintain their own upstream docker-compose files:
start_services.pyhandles cloning repos, preparing.envfiles, and starting services- Each external service needs:
is_*_enabled(),clone_*_repo(),prepare_*_env(),start_*()functions instart_services.py scripts/utils.shprovidesget_*_compose()getter functions andbuild_compose_files_array()includes themstop_all_services()instart_services.pychecks compose file existence (not profile) to ensure cleanup when a profile is removed- All external compose files use the same project name (
-p localai) so containers appear together docker-compose.override.yml: User customizations file (gitignored). Bothstart_services.pyandbuild_compose_files_array()inutils.shauto-detect and include it last (highest precedence). Users can override any service property without modifying tracked files.
Secret Generation
The scripts/03_generate_secrets.sh script:
- Generates random passwords, JWT secrets, API keys, and encryption keys
- Creates bcrypt password hashes with
generate_bcrypt_hash(caddy hash-passwordin adocker run --rmof the stack's Caddy image). Never install a Caddy package on the host: the stack's Caddy runs in Docker, the distro package'scaddy.servicetakes port 80, and Caddy's Cloudsmith apt repository answers 402. Missing hashes are computed before.envis rewritten, so a failure leaves.envuntouched; when any hash is missing, Docker and the image are checked before the first prompt - Preserves existing user-provided values in
.env WEBUI_SECRET_KEY(Open WebUI JWT key, also derives its MFA encryption key) is generated here and passed toopen-webui; on upgrade an empty value is first copied from the existing container (its env, else/app/backend/.webui_secret_keyviadocker cp; 03 runs beforedown), and an unreachable Docker, a container whose key cannot be read or a key in an unexpected format stops 03 with exit 1 (before.envis rewritten).restart.shexits 1 whileopen-webuiis active and the key is empty. Never addMFA_ENCRYPTION_KEYby default: MFA secrets enrolled before it existed would no longer decrypt and Open WebUI refuses to start- Supports different secret types via
VARS_TO_GENERATEmap:password:32,jwt,api_key,base64:64,hex:32 - Preserves existing values on every run; the
--updateflag passed during updates is accepted but never parsed
Utility Functions (scripts/utils.sh)
Source with: source "$(dirname "$0")/utils.sh" && init_paths
Key functions:
is_profile_active "myservice"- Check if profile is enabledread_env_var "VAR_NAME"/write_env_var "VAR_NAME" "value"- .env manipulationload_env- Source .env file to make variables availableupdate_compose_profiles "profile1,profile2"- Update COMPOSE_PROFILES in .envremove_compose_profile "$list" "profile"- Print a comma list without one profile (space-tolerant)gen_password 32/gen_hex 64/gen_base64 64- Secret generationgenerate_bcrypt_hash "password"- Create Caddy-compatible bcrypt hash: password over stdin toCADDY_HASH_IMAGE(keep in sync with thecaddyservice), output validated as bcryptremove_legacy_caddy_apt_source- Remove the Cloudsmith Caddy apt source/key pre-1.16.4 installers added; called beforeapt updatein 01 andupdate.sh, and in 03json_escape "string"- Escape string for JSON outputwt_input,wt_password,wt_yesno,wt_msg- Whiptail dialog wrapperswt_checklist,wt_radiolist,wt_menu- Whiptail selection dialogswt_parse_choices "$result" array_name- Parse quoted checklist output safelylog_info,log_success,log_warning,log_error- Logging functionslog_header,log_subheader,log_divider,log_box- Formatted outputprint_ok,print_error,print_warning,print_info- Doctor output helpersget_real_user/get_real_user_home- Get actual user even under sudobackup_preserved_dirs/restore_preserved_dirs- Directory preservation for git updatescleanup_legacy_n8n_workers- Remove old n8n worker containers from previous naming conventioncleanup_legacy_supabase_kong- Remove thesupabase-kongcontainer (labelslocalai/kong) left running after Supabase moved its gateway to Envoy; it holds port 8000. Called byrestart.sh; install andmake updateuseremove_legacy_supabase_kong()instart_services.py, after the Supabasegit pulland beforedown, so a failed pull or removal leaves the running stack untouched (the Envoy image itself is still pulled only by Supabase's ownup). Both look the container up withdocker ps -a --filter name=…and decide by the exit code and the exact name: never match Docker's error text (Docker 29 lowercasedNo such object, which broke 1.18.0)cleanup_legacy_comfyui- Remove the pre-1.13comfyuicontainer (service renamed tocomfyui-*, container name kept) soupdoes not hit a name conflictget_n8n_workers_compose/get_supabase_compose/get_dify_compose- Get compose file path if profile active AND file existsget_ollama_instances_compose/get_open_webui_postgres_compose- Conditional compose overrides (multi-Ollama, Open WebUI on Postgres)get_neo4j_compose-docker-compose.neo4j.yml, which publishes Bolt7687on caddy only whileneo4jis active. Never put7687back intodocker-compose.yml(Docker-published ports bypass ufw); the overlay is also listed inrestart.shandstart_services.py, andmake doctor's exposed-ports check allows 7687 on caddy only withneo4j(CADDY_PORTSindoctor.sh; a new Caddy-published port needs adding there)harden_supabase_gateway_bind- Keep the Supabase API gateway bound to loopback (issue #108)cleanup_stale_ollama_instances- Remove Ollama instance containers above the configured countbuild_compose_files_array- Build globalCOMPOSE_FILESarray with all active compose files (main + external)compose_with_retry "label" ARGS...-docker compose -p localai ARGSup to 3 times, 15s then 30s apartpull_images_with_retry- Pooledpull --ignore-buildableoverCOMPOSE_FILES; on failure (and with Docker reachable) pulls one service at a time viacompose_with_retryand leaves the unpulled services in globalFAILED_PULLS.apply_update.shcontinues when the image each of them is configured with is present locally, then runs a retriedbuild --pullof the main stack; a missing image or a failed build stops the update before 06, whosedownwould otherwise leave the stack stopped when its own unretried pull/build fails. The image is read fromconfig --format json, becauseconfig --images SERVICEalso lists the images of itsdepends_onservicesopenclaw_init_failed- True if the last openclaw-init run exited non-zero or loggedopenclaw-init: ERROR(used by doctor, 07 and restart.sh)
Service Profiles
Common profiles:
n8n: n8n workflow automation (includes main app, worker, runner, and import services)flowise: Flowise AI agent builder (archived upstream). Image pinned to3.1.3indocker-compose.ymlandscripts/update_preview.sh(keep both in sync): 3.1.4 crashes with SQLite and runs as non-root, which does not use the/root/.flowisemount. Never go back tolatest/3.1.4; Keelflow is the upgrade pathragflow: RAGFlow, image pinned tov0.27.2indocker-compose.ymlandscripts/update_preview.sh(keep both in sync):latestmoved to 1.0 release candidates, which need kvrocks (and NATS) the stack does not providekeelflow: Keelflow, continuation of Flowise (separate service andkeelflow_datavolume; Flowise data is copied in manually, seedocs/keelflow.md)monitoring: Prometheus, Grafana, cAdvisor, node-exporterlangfuse: Langfuse observability (includes ClickHouse, MinIO, worker, web)minio(langfuse) andragflow-minio(ragflow) runpgsty/silo, a MinIO fork with the same data format andMINIO_*vars. The server binary issilo(nominioalias). The healthcheck curls/minio/health/cluster, which returns 503 until the server is ready;/minio/health/liveanswers 200 too earlycpu,gpu-nvidia,gpu-amd: Ollama hardware profiles (mutually exclusive)invokeai-nvidia,invokeai-amd,invokeai-cpu: InvokeAI hardware profiles (mutually exclusive)comfyui-nvidia,comfyui-amd,comfyui-cpu: ComfyUI hardware profiles (mutually exclusive; the pre-1.13comfyuiprofile is migrated by the wizard). NoCLI_ARGSin compose - the images ship the right default and the volume must be/rootcloudflare-tunnel: Cloudflare Tunnel for zero-trust access (seecloudflare-instructions.md)supabase: Supabase BaaS (external compose, cloned at runtime; mutually exclusive withdify)dify: Dify AI platform (external compose, cloned at runtime; mutually exclusive withsupabase)gost: HTTP/HTTPS proxy for routing AI service outbound trafficpython-runner: Internal Python execution environment (no external access)n8n-sandbox: n8n Assistant code-execution sandbox (requiresn8n; internal only, see below)openclaw: OpenClaw AI agent with dashboard and Telegram bot; full host access (see below)open-terminal: Open Terminal execution sandbox for Open WebUI agents (requiresopen-webui; internal only, see below)searxng,letta,lightrag,libretranslate,crawl4ai,docling,waha,paddleocr,ragapp,gotenberg,postiz,n8n-mcp: Additional optional services
Architecture Patterns
Docker Compose YAML Anchors
docker-compose.yml defines reusable anchors at the top:
x-logging: &default-logging-json-filewithmax-size: 1m,max-file: 1x-proxy-env: &proxy-env- HTTP/HTTPS proxy vars fromGOST_PROXY_URL/GOST_NO_PROXYx-n8n: &service-n8n- Full n8n service definition (reused by workers viaextends)x-ollama: &service-ollama- Ollama service definition (reused by CPU/GPU variants)x-init-ollama: &init-ollama- Ollama model pre-puller (auto-pullsqwen2.5:7b-instruct-q4_K_Mandnomic-embed-text)x-n8n-worker-runner: &service-n8n-worker-runner- Runner template for worker generation
Healthchecks
Services should define healthchecks for proper dependency management:
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
interval: 30s
timeout: 10s
retries: 5Service Dependencies
Use depends_on with conditions:
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthyEnvironment Variable Patterns
- All secrets/passwords end with
_PASSWORDor_KEY - All hostnames end with
_HOSTNAME - Password hashes end with
_PASSWORD_HASH - Use
${VAR:-default}for optional vars with defaults - Always wrap
${VAR}interpolations indocker-compose.ymlin double quotes:"${VAR:-default}"
Profile Activation Logic
In bash scripts, check if a profile is active:
if is_profile_active "myservice"; then
# Service-specific logic
fiProxy Configuration (for AI services)
Services making outbound HTTP requests to AI APIs (OpenAI, Anthropic, etc.) should use the shared proxy anchor:
x-proxy-env: &proxy-env
HTTP_PROXY: "${GOST_PROXY_URL:-}"
HTTPS_PROXY: "${GOST_PROXY_URL:-}"
NO_PROXY: "${GOST_NO_PROXY:-}"
services:
myservice:
environment:
<<: *proxy-env # Inherit proxy settingsImportant: Healthchecks must bypass proxy:
healthcheck:
test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= wget -qO- http://localhost:8080/health || exit 1"]GOST_NO_PROXY: ALL service container names must be listed in GOST_NO_PROXY in .env.example. This prevents internal Docker network traffic from routing through the proxy. This applies to every service, not just those using <<: *proxy-env.
Welcome Page Dashboard
The welcome page (welcome/) provides a post-install dashboard showing all active services:
scripts/generate_welcome_page.sh: Generateswelcome/services.jsonwith service URLs, credentials, and metadatawelcome/app.js: ContainsSERVICE_METADATAobject defining display properties (name, description, icon, color, category)- Categories:
ai,database,monitoring,tools,infra,automation - Always use
json_escape "$VAR"when building JSON to handle special characters
Preserved Directories
Directories in PRESERVE_DIRS (defined in scripts/utils.sh) survive git updates:
python-runner/- User's custom Python code
These are backed up before git reset --hard and restored after.
Restart Behavior
scripts/restart.sh stops all services first, then starts external stacks (Supabase/Dify) separately before the main stack (10s delay between). This is required because external compose files use relative volume paths that resolve from their own directory.
Common Issues and Solutions
Service won't start after adding
- Ensure profile is added to
COMPOSE_PROFILESin.env - Check logs:
docker compose -p localai logs <service> - Verify no port conflicts (no services should publish ports)
- Ensure healthcheck is properly defined if service has dependencies
Caddy certificate issues
- DNS must be configured before installation (wildcard A record:
*.yourdomain.com) - Check Caddy logs for certificate acquisition errors
- Verify
LETSENCRYPT_EMAILis set in.env
Password hash generation fails
- The hash comes from
docker run --rm -i --network none "$CADDY_HASH_IMAGE" caddy hash-password --algorithm bcrypt(password on stdin;CADDY_HASH_IMAGEinscripts/utils.sh). The Caddy container does not need to run, but Docker must, and the image must be present or pullable. Check:printf 'test\n' | docker run --rm -i caddy:2-alpine caddy hash-password
File Locations
- Shared files accessible by n8n:
./shared(mounted as/data/sharedin n8n) - n8n backup/workflows:
n8n/backup/workflows/(mounted as/backupin n8n containers) - n8n storage: Docker volume
localai_n8n_storage - Flowise storage:
~/.flowiseon host (mounted from user's home directory, not a named volume) - Keelflow storage: Docker volume
localai_keelflow_data - Custom TLS certificates:
certs/(gitignored, mounted as/etc/caddy/certs/) - Caddy addon configs:
caddy-addon/site-*.conf(gitignored, auto-imported) - Service-specific volumes: Defined in
volumes:section at top ofdocker-compose.yml - Installation logs: stdout during script execution
- Service logs:
docker compose -p localai logs <service>
Testing Changes
Syntax Validation
# Docker Compose syntax
docker compose -p localai config --quiet
# Bash script syntax (validate all key scripts)
bash -n scripts/utils.sh
bash -n scripts/git.sh
bash -n scripts/databases.sh
bash -n scripts/telemetry.sh
bash -n scripts/03_generate_secrets.sh
bash -n scripts/04_wizard.sh
bash -n scripts/05_configure_services.sh
bash -n scripts/07_final_report.sh
bash -n scripts/generate_welcome_page.sh
bash -n scripts/generate_n8n_workers.sh
bash -n scripts/generate_ollama_instances.sh
bash -n scripts/apply_update.sh
bash -n scripts/update.sh
bash -n scripts/pull_images.sh
bash -n scripts/install.sh
bash -n scripts/restart.sh
bash -n scripts/doctor.sh
bash -n scripts/setup_custom_tls.sh
bash -n scripts/setup_sysbox.sh
bash -n scripts/docker_cleanup.sh
sh -n openclaw/init.shFull Testing
When modifying installer scripts:
- Test on a clean Ubuntu 24.04 LTS system (minimum 4GB RAM / 2 CPU)
- Verify all profile combinations work
- Check that
.envis properly generated - Confirm final report displays correct URLs and credentials
- Test update script preserves custom configurations
Other versions1 project uses a version of this.
Word for word · 1
- kossakovsky/selfhost-ai★ 953100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

