Repository navigation
Config Custom Services
Custom services are user-defined Docker containers that ɳSelf manages alongside the core stack. They live in numbered slots, CS_1 through CS_10, and are treated as first-class citizens: ɳSelf generates their docker-compose config, injects core service credentials, routes them through Nginx, and exposes them in nself status and nself logs.
Use custom services to run any application your project needs, a REST API, a background worker, a webhook processor, an AI inference service, without leaving the ɳSelf config system.
nSelf stack
├── postgres, hasura, auth, nginx (core services — always present)
├── redis, minio, search, ... (optional services — toggled by *_ENABLED)
└── CS_1 .. CS_10 (custom services — defined in .env)
├── any Docker image or nself template
├── auto-receives DATABASE_URL, HASURA_GRAPHQL_ADMIN_SECRET
├── optionally exposed via Nginx at {CS_N_ROUTE}.{BASE_DOMAIN}
└── managed by nself start / stop / logs / status
Up to 10 custom service slots are available per project. Slots are independent, you can use CS_1 and CS_3 without defining CS_2.
The CS_N variable accepts a single definition string that encodes the most common settings:
CS_N=name:template[:port][:route]
| Field | Required | Description |
|---|---|---|
name |
Yes | Service name. Used in container labels, logs, and Nginx config. |
template |
Yes | Language/framework template. See Language Templates. |
port |
No | Port the service listens on. Auto-assigned if omitted. |
route |
No | Nginx subdomain. If omitted, the service is internal-only. |
Example, a Go service on port 8001 exposed at ping.{BASE_DOMAIN}:
# .env
CS_1=ping_api:go:8001:pingɳSelf parses this into the service configuration. The Compose service name is sanitized: ping_api becomes ping-api. Use that name for logs and internal DNS. The default build context prefers the original directory services/ping_api/ when it exists.
Keep the CS_N definition and add supported overrides. CS_N_PORT and CS_N_ROUTE override the shorthand fields. An empty or absent CS_N skips the slot entirely, even if overrides are present. There are no CS_N_ENABLED, CS_N_NAME, CS_N_TEMPLATE, CS_N_WORKDIR, or CS_N_REPLICAS overrides. Set the working directory in the image's Dockerfile.
# .env
CS_1=ping_api:go:8001:ping # base definition
CS_1_MEMORY=512m # override the default 256m
CS_1_CPU=1.0 # override the default 0.5
CS_1_PUBLIC=true # expose via NginxN is the slot number (1–10). The name and template come from the required CS_N definition; the supported overrides are listed below.
| Variable | Type | Default | Description |
|---|---|---|---|
CS_N |
string | (empty) | Shorthand definition. Format: name:template[:port][:route]
|
CS_N_PORT |
int | parsed from CS_N or auto | Port the service listens on inside the Docker network |
CS_N_ROUTE |
string | parsed from CS_N | Nginx subdomain , set this to expose the service externally |
CS_N_PUBLIC |
bool |
true when shorthand includes a route, otherwise false
|
With a nonempty route, enables Nginx routing at {CS_N_ROUTE}.{BASE_DOMAIN}. Set explicitly when adding a route only through CS_N_ROUTE. |
CS_N_MEMORY |
string | 256m |
Docker memory limit |
CS_N_CPU |
string | 0.5 |
Docker CPU limit (fractional cores) |
CS_N_HEALTHCHECK |
string | /health |
Healthcheck override. A path (e.g. /auth/health) probes that path instead of /health on the service's own port. A full CMD ... / CMD-SHELL ... command is passed through verbatim (split on whitespace) for services that need curl, a non-HTTP probe, or a different port. disabled / none / false omits the healthcheck entirely. |
CS_N_TABLE_PREFIX |
string | (empty) | Injected into the container as TABLE_PREFIX; the application decides how to use it |
CS_N_ENV_PASSTHROUGH |
string | (empty) | Comma-separated allowlist of project .env var names to forward into this container in addition to the fixed core set. CS_N_ENV still wins on a name conflict. |
CS_N_ENV_FILE |
string | (empty) | Project-relative path to a dotenv-format file whose KEY=VALUE lines are injected into this container, or a comma-separated list of such files (.env.dev,.env.secrets). Files load in order and a later file wins on a repeated key. Applied after CS_N_ENV_PASSTHROUGH, before CS_N_ENV. Use this instead of CS_N_ENV when a value itself contains a comma (e.g. some SMTP passwords) or when there are too many vars for one line. A missing file fails nself build rather than silently starting the service without those vars. Each entry must be relative and must not contain ... |
CS_N_ENV |
string | (empty) | Additional env vars to inject, in KEY=VALUE,KEY=VALUE format. Always applied last — overrides the fixed core set, CS_N_ENV_PASSTHROUGH, and CS_N_ENV_FILE. |
CS_N_IMAGE |
string | (empty) | Run a pre-built image instead of building from a Dockerfile — e.g. docker.io/pgsty/minio:RELEASE.2026-08-04T00-00-00Z@sha256:... to pin an exact digest. Mutually exclusive with CS_N_PATH; when set, no build: block is emitted at all. |
CS_N_VOLUMES |
string | (empty) | Comma-separated extra bind mounts in host:container[:mode] form, e.g. ./email-templates:/app/templates:ro. Appended to the service's generated volume list. See Host binds. |
CS_N_PATH |
string | ./services/<name> |
Build context. A path inside the project, or an ancestor of the project written as .. segments only (e.g. ../.. for a monorepo root). See Ancestor build context. Mutually exclusive with CS_N_IMAGE. |
CS_N_DOCKERFILE |
string | Dockerfile |
Dockerfile path relative to the build context (for example backend/services/api/Dockerfile when the context is the monorepo root). No .., not absolute. Cannot be combined with CS_N_IMAGE. |
CS_N_BUILD_TARGET |
string | (empty) | Multi-stage build target, emitted as build.target. Must match ^[A-Za-z0-9_.-]{1,64}$. Cannot be combined with CS_N_IMAGE. |
CS_N_COMMAND |
string | image default | Container command in exec form. The value is split on whitespace: no shell, no quoting, no variable expansion. node dist/server.js becomes ["node", "dist/server.js"]. |
CS_N_DEPENDS_ON |
string | (empty) | Comma-separated name[:started|healthy|completed]. Adds compose depends_on entries next to the built-in postgres: service_healthy. healthy (the default) is service_healthy, started is service_started, completed is service_completed_successfully. A name must be a core service, another custom service or a service of an installed plugin; otherwise nself build fails with E500. |
CS_N_NETWORKS |
string | (empty) | Comma-separated extra networks, each named <PROJECT_NAME>_<name> (lowercase letters, digits, -, _). They are emitted as external networks and attached next to the project network, so they must already exist (docker network create). Any other name fails with E501: a custom service never joins another project's network. |
All CS_* variables are automatically exempt from "unknown env var" warnings.
CS_N_PATH may name an ancestor of the project directory so a service can build from a monorepo root:
# .env, project at <repo>/apps/api, repository root two levels up
CS_1=worker:node:9500
CS_1_PATH=../..
CS_1_DOCKERFILE=apps/api/services/worker/Dockerfile
CS_1_BUILD_TARGET=runtimeThe whole context directory is sent to the image builder, so it is bounded:
- It may reach at most the nearest ancestor of the project directory that holds
.git. When no ancestor holds.git, the project directory is the limit. - The ignore file BuildKit will use (a
<Dockerfile>.dockerignorebeside the Dockerfile if one exists, otherwise.dockerignorein the context) must exclude.env*and.secretsat every depth: use the lines**/.env*and**/.secrets. A bare.env*is not enough, because Docker matches it at the context root only andapps/api/.envwould still be sent.nself buildchecks this by matching probe paths (root, each directory down to the project, and below it) with Docker's pattern rules. - A
!line that can re-include a.env*or.secretspath (for example!**/.env.productionor!**/.secrets/pub) is refused too. Only a name ending.example,.sample,.templateor.distmay be re-included. A leading UTF-8 byte order mark is ignored, as Docker does. - Anything else fails
nself buildwith E528, naming the directory. - A path that climbs and then descends (
../sibling) is not an ancestor and is rejected at config load, namingCS_N_PATH.
The ancestor-context bound, the ignore-file check and the Dockerfile symlink check run in nself build (compose generation). Config load checks syntax only, so a tool that loads the config without building does not enforce them. A CS_N_PATH inside the project that is itself a symlink leaving the project is not checked.
CS_N_DEPENDS_ON is syntax-checked when the compose file is generated and resolved during build post-validation, after the plugin step has written .nself/compose-files.txt. A first nself build therefore already sees every plugin service. An unknown name fails with E500 naming CS_N_DEPENDS_ON and the name. An unknown condition fails with E500 naming the condition. A dependency cycle fails with E500 naming the path (api -> db2 -> api): custom-service cycles when the compose file is generated, cycles through core or plugin services in build post-validation.
The network prefix is a naming rule, not ownership. Another project whose name extends yours (app and app_staging) also owns names that start with app_, so choose network names no other project uses.
Extra networks are external: nself build declares them but does not create them. For PROJECT_NAME=myapp and CS_1_NETWORKS=myapp_shared, create the network before starting the stack:
docker network create myapp_sharedCS_N_VOLUMES keeps its v1.4 rules. In v1.5 mode (NSELF_V15=1, see Compat-V15) these entries fail with E502:
- a writable absolute host path outside the project directory (add
:ro, use a named volume or a project-relative path); -
/var/run/docker.sockor/in any mode suffix.
In v1.4 mode the Docker socket and host root binds still work and print one warning. Named volumes, project-relative binds and read-only outside binds pass in both modes.
These stay outside CS_N; see Custom-Services-Migration for the pattern table and the open gaps. There is no key that includes hand-written compose.
When a custom service container starts, ɳSelf automatically injects the following variables:
| Variable | Value | Description |
|---|---|---|
DATABASE_URL |
postgresql://user:pass@postgres:5432/db |
Full PostgreSQL connection string |
HASURA_GRAPHQL_ADMIN_SECRET |
from project .env
|
For making privileged Hasura API calls |
PROJECT_NAME |
from project .env
|
The project namespace |
BASE_DOMAIN |
from project .env
|
The root domain |
ENV |
from project .env
|
dev, staging, or prod
|
POSTGRES_HOST, POSTGRES_PORT
|
postgres, 5432
|
Internal database address |
POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD
|
resolved Postgres config | Database credentials |
HASURA_GRAPHQL_ENDPOINT |
http://hasura:8080/v1/graphql |
Internal GraphQL endpoint |
AUTH_SERVER_URL, AUTH_CLIENT_URL
|
resolved Auth config | Internal Auth server and client URLs |
SERVICE_NAME, SERVICE_PORT, SERVICE_ROUTE
|
parsed service configuration | This service's identity |
TABLE_PREFIX |
CS_N_TABLE_PREFIX |
Application table-prefix hint |
REDIS_URL |
resolved Redis config | Included when Redis is enabled |
S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET
|
active object-store config | Included when storage is enabled; v1.5 ops profile omits these |
CS_N_ENV values |
parsed from CS_N_ENV
|
Any additional vars you define for this slot |
The generated environment consists of that core set plus explicit overrides. Each service's surface
area is explicit and auditable rather than silently coupled to whatever
another part of the stack happens to define. To pull in a specific project
.env var, name it in CS_N_ENV_PASSTHROUGH; to define a new one outright,
use CS_N_ENV.
Example, reading injected vars in a Go service:
dbURL := os.Getenv("DATABASE_URL") // injected by nSelf
adminSecret := os.Getenv("HASURA_GRAPHQL_ADMIN_SECRET") // injected by nSelf
apiKey := os.Getenv("MY_API_KEY") // forwarded via CS_N_ENV_PASSTHROUGHUse CS_N_ENV_PASSTHROUGH to forward specific vars that already exist in the
project's .env (a comma-separated allowlist of names, values are not
repeated):
# .env
MY_API_KEY=sk_live_...
CS_2_ENV_PASSTHROUGH=MY_API_KEY,STRIPE_WEBHOOK_SECRETUse CS_N_ENV to pass service-specific variables that aren't in the main .env,
or to override a value that was forwarded via CS_N_ENV_PASSTHROUGH
(CS_N_ENV always wins on a name conflict):
# .env
CS_2_ENV=QUEUE_SIZE=100,WORKER_TIMEOUT=30,LOG_FORMAT=jsonnself service add <name> --template <lang> writes starter source files and a Dockerfile, then registers the next free slot in .env.dev.
nself service add my-api --template go # doc-check: skip registered in cmd/commands/service.go; no command-signature fixture yetAvailable templates:
| Template | Description |
|---|---|
go (default) |
Go HTTP service |
node |
Node.js service |
python |
Python service |
static |
Static file server |
rust |
Rust service |
other |
Minimal scaffold for anything else |
The Go, Node, Python, and Rust scaffolds expose /healthz; the default generated healthcheck probes /health with wget. Set CS_N_HEALTHCHECK=/healthz for those apps if the final image supplies wget, or supply a suitable CMD/CMD-SHELL probe. Static and other scaffolds need a probe appropriate to their application. Adapt the starter image and probe before relying on health-based dependencies.
The ɳSelf infrastructure itself (web/backend) uses CS_1 to run ping_api, a Go service on port 8001 that handles telemetry and license validation for the CLI. It is accessible at ping.nself.org.
# web/backend/.env
CS_1=ping_api:go:8001:ping
CS_1_PUBLIC=true
CS_1_MEMORY=128m
CS_1_CPU=0.25
CS_1_HEALTHCHECK=/healthThis configuration produces:
| Derived value | Result |
|---|---|
| Compose service name | ping-api |
| Template | go |
| Internal port | 8001 |
| Route | ping |
| External URL | https://ping.nself.org |
| Internal URL |
http://ping-api:8001 (within Docker network) |
The ping_api service receives DATABASE_URL and HASURA_GRAPHQL_ADMIN_SECRET automatically on startup, so it can validate license keys against the database without any additional configuration.
1. Create a service using a template:
nself service add my-api --template go # doc-check: skip registered in cmd/commands/service.go; no command-signature fixture yetThis scaffolds a Go service in ./services/my-api/ with a Dockerfile and /healthz endpoint, and assigns it the next available CS_N slot and port automatically.
2. Configure the assigned slot in .env.dev:
The command already registers the service. If it assigned CS_1, add a route and a healthcheck suitable for your final image; keep the registered name:
# .env.dev
CS_1=my-api:go:8001:api
CS_1_MEMORY=256m
# Supply a probe tool in the image, or use an application-specific probe.
CS_1_HEALTHCHECK=/healthz3. Rebuild and start:
nself build # regenerates docker-compose.yml with CS_1 included
nself start # brings up the full stack including my-api4. Verify it's running:
nself status # shows all services including custom ones
nself logs -f -S my-api # follow logs by Compose service nameOnce the application is running and routing is configured, its route is https://api.{BASE_DOMAIN} and its internal address is http://my-api:8001.
You can define up to 10 slots simultaneously. Slots are independent, gaps are fine.
# .env
CS_1=ping_api:go:8001:ping # telemetry service
CS_2=worker:node:8002 # background job worker (internal-only, no route)
CS_3=cms_api:python:8003:cms # FastAPI CMS
CS_5=search_proxy:go:8005:search # search proxy (skipped CS_4 intentionally)
# Override resources on the worker
CS_2_MEMORY=512m
CS_2_CPU=1.0- Custom service images are built from the scaffolded Dockerfile in
./services/{name}/. To use a pre-built image instead, setCS_N_IMAGEdirectly — a full image reference, optionally digest-pinned with@sha256:.... -
CS_N_TABLE_PREFIXsupplies the container'sTABLE_PREFIXenvironment variable. It does not configure CLI migration or backup filtering. - If a service's health endpoint isn't
/healthon its own port (e.g. an auth service serving/auth/health), setCS_N_HEALTHCHECK=/auth/health— otherwise Docker probes the wrong path and reports the service unhealthy forever regardless of its actual state. -
nself logsincludes all Compose services by default.--allincreases the tail to 100 lines; select a custom service by its Compose name, not itsCS_Nslot. -
CS_N_IMAGE,CS_N_ENV_FILE, andCS_N_VOLUMEScover the cases that previously forced a hand-authoreddocker-compose.override.yml: a pinned third-party image digest, many/complex injected env vars (e.g. SMTP credentials), and an extra bind mount (e.g. an email-template directory) — see the reference table above.
For a step-by-step walkthrough of building and deploying a custom service from scratch, see Guide-Custom-Services.
← Config-Optional-Services | Configuration | Guide-Custom-Services →
Getting Started
Commands
- Commands, Overview
- Lifecycle: cmd-init · cmd-build · cmd-start · cmd-stop · cmd-restart · cmd-dev
- Monitoring: cmd-status · cmd-logs · cmd-health · cmd-urls · cmd-doctor · cmd-monitor · cmd-alerts · cmd-sentry · cmd-watchdog
- Data: cmd-db · cmd-backup · cmd-dr · cmd-queue · cmd-webhooks
- Config: cmd-config · cmd-service · cmd-env · cmd-promote
- Networking: cmd-ssl · cmd-trust · cmd-dns-setup
- Security: cmd-access · cmd-security · cmd-secrets
- Tenancy: cmd-tenant · cmd-billing
- Plugins: cmd-plugin · cmd-license · cmd-dogfood (extracted, CLI-R11) · cmd-k8s (extracted, CLI-R11) · cmd-encryption (extracted, CLI-R11) · cmd-waf (extracted, CLI-R11) · cmd-federation (extracted, CLI-R11) · cmd-mail (extracted, CLI-R11) · cmd-dlq (extracted, CLI-R11)
- AI: cmd-ai · cmd-claw · cmd-model
- Templates: cmd-template
- Utilities: cmd-exec · cmd-clean · cmd-reset · cmd-update · cmd-upgrade · cmd-version · cmd-admin · cmd-migrate · cmd-migrate-firebase · cmd-migrate-supabase · cmd-completion
Features
- Features, Overview
- Feature-Auth
- Feature-Storage
- Feature-Search
- Feature-Functions
- Feature-Email
- Feature-Monitoring
- Feature-Plugins
- Feature-nClaw, AI Assistant
- Feature-nChat, Messaging
- Feature-nFamily, Family Social
- Feature-nCloud, Managed Hosting
- Feature-Memory-Rooms, Knowledge Organization
- Feature-Agent-Dashboard, Agent Metrics
- Feature-Image-Generation, AI Image Generation
Configuration
- Configuration, Overview
- Config-Env-Vars
- Config-Postgres
- Config-Hasura
- Config-Auth
- Config-Nginx
- Shared-Proxy
- Config-Optional-Services
- Config-Custom-Services
- Config-System
Plugins (87 + 10 monitoring)
- Plugin-Overview
- Plugin-Catalog
- Plugin-Install
- Plugin-Licensing
- Plugin-Architecture
- Plugin-Dev-Guide
- Plugin-Migrations-Contract
Free (25)
- plugin-backup
- plugin-content-acquisition
- plugin-content-progress
- plugin-cron
- plugin-donorbox
- plugin-feature-flags
- plugin-github
- plugin-github-runner
- plugin-invitations
- plugin-jobs
- plugin-link-preview
- plugin-mdns
- plugin-mlflow
- plugin-monitoring
- plugin-notifications
- plugin-notify
- plugin-paypal
- plugin-search
- plugin-shopify
- plugin-stripe
- plugin-subtitle-manager
- plugin-tokens
- plugin-torrent-manager
- plugin-vpn
- plugin-webhooks
Pro (62)
- plugin-access-controls
- plugin-activity-feed
- plugin-admin-api
- plugin-analytics
- plugin-auth
- plugin-bots
- plugin-browser
- plugin-calendar
- plugin-cdn
- plugin-chat
- plugin-claw
- plugin-claw-budget
- plugin-claw-news
- plugin-claw-web
- plugin-cloudflare
- plugin-cms
- plugin-compliance
- plugin-ddns
- plugin-devices
- plugin-documents
- plugin-entitlements
- plugin-file-processing
- plugin-game-metadata
- plugin-geocoding
- plugin-geolocation
- plugin-google
- plugin-home
- plugin-idme
- plugin-knowledge-base
- plugin-linkedin
- plugin-livekit
- plugin-media-processing
- plugin-meetings
- plugin-moderation
- plugin-mux
- plugin-object-storage
- plugin-observability
- plugin-photos
- plugin-podcast
- plugin-post
- plugin-realtime
- plugin-retro-gaming
- plugin-rom-discovery
- plugin-social
- plugin-sports
- plugin-support
- plugin-tmdb
- plugin-voice
- plugin-web3
- plugin-workflows
Planned (26)
plugin-auditplugin-blogplugin-checkoutplugin-commerceplugin-drmplugin-exportplugin-flowplugin-importplugin-ldapplugin-mailgunplugin-mediaplugin-oauth-providersplugin-pagesplugin-postmarkplugin-rate-limitplugin-reportsplugin-samlplugin-schedulerplugin-sendgridplugin-ssoplugin-subscriptionplugin-thumbplugin-transcoderplugin-twilioplugin-wafplugin-watermark
Guides
- Guide-Production-Deployment
- Guide-SSL-Setup
- Guide-Multi-Tenancy
- Guide-Security-Hardening
- Guide-Monitoring-Setup
- Guide-Backup-Restore
- Guide-Custom-Services
- Guide-Migration-from-v1
Architecture
- Architecture
- Architecture-Guardrails
- Service-Graph
- Compose-Generation
- Nginx-Generation
- Plugin-Architecture
Reference
Machine interface
-
JSON-Output,
--jsonenvelope -
Command-Registry,
nself help --json - Exit-Codes
- error-codes, Error Codes
-
Compat-V15,
NSELF_V15gate
Licensing
Security
Brand
Operations
- operations/release-cascade, Release Cascade
- operations/self-healing, Self-Healing Schema
- operations/redis-tuning, Redis Pool Tuning
- operations/meilisearch-warmup, MeiliSearch Warm-Up
- operations/jwt-rotation, JWT Key Rotation
- operations/windows-wsl2-setup, Windows / WSL2 Setup
- operations/gemini-oauth-reauth, Gemini OAuth Reauth
Contributing
Admin
- USER-ACTION-QUEUE, Pending Admin Actions
All commands (21)
- A: cmd-add · cmd-admin
- B: cmd-backup · cmd-build
- C: cmd-clean · cmd-config
- D: cmd-db · cmd-deploy · cmd-doctor
- E: cmd-exec
- I: cmd-init
- L: cmd-license · cmd-logs
- M: cmd-mcp
- R: cmd-remove · cmd-reset · cmd-restart
- S: cmd-start · cmd-status · cmd-stop
- U: cmd-update