Get your app live in minutes

Neo deploys your Docker app to any VPS over SSH. No agent to install, no platform to sign up for, no infrastructure to manage. If you have a Dockerfile and a server, you're ready.

Deploy in 3 commands
# 1. Point Neo at your server (installs Docker + Caddy automatically)
neo init root@your-ip-address

# 2. Deploy your project
neo deploy . --domain app.example.com

# 3. Get instant HTTPS (no DNS setup needed)
neo domain my-app --temp
# → https://my-app.your-ip-address.sslip.io
Set up .neo.yml for your project Run neo config init to scaffold a commented .neo.yml — it prompts for name, domain, and port (auto-detected from your Dockerfile EXPOSE) and stubs the rest. Already using docker-compose? Run neo config generate instead to auto-detect your app, databases, workers, env vars, and volumes.
SSH access & key files Neo connects over SSH using, in order: ssh-agent, then ~/.ssh/id_ed25519 / ~/.ssh/id_rsa. For a cloud .pem or a key at a non-standard path, pass it explicitly with -i (or --key):
Shell
neo init [email protected] -i ~/keys/server.pem
The key must be unencrypted (no passphrase) — if it has one, run ssh-add ~/keys/server.pem first, then neo init without -i. With no key available, Neo prompts for a password.
Activation (free, one-time) The first time you run a command, Neo asks for your email and issues a free license key instantly, then continues. You can also run neo activate up front. One key works on unlimited servers and devices.

What just happened?

Behind those 3 commands, Neo:

1 · Init
Connected via SSH, installed Docker + Caddy on your server
2 · Deploy
Built your Docker image locally, transferred it over SSH, started the container
3 · SSL
Configured Caddy as a reverse proxy with a Let's Encrypt SSL certificate
Zero config by default No config files required for the basics. When you need more control — workers, sidecars, volumes, environments — add a .neo.yml file. Keep reading for the full reference.

Installation

One command. Supports macOS (Apple Silicon & Intel), Linux (x86_64 & ARM64), and Windows.

curl -fsSL https://neo.vxero.dev/neo | sh Copied!
irm https://neo.vxero.dev/neo/windows | iex Copied!

Common Workflows

Real examples for the most common tasks.

Deploy a project with a Dockerfile

Shell
cd my-project
neo deploy . --domain app.example.com

Migrate from docker-compose

Shell
# Auto-generate .neo.yml from your existing docker-compose.yml
neo config generate

# Deploy everything (app + workers + sidecars)
neo deploy . --to production

Local development

Shell
# Run locally (wraps docker compose with Neo env loading)
neo dev

# Stop
neo dev down

Day-to-day operations

Shell
neo logs my-app                         # stream logs
neo env set my-app API_KEY=sk-123       # set env var (auto-restarts)
neo domain my-app blog.example.com      # set real domain
neo sync my-app                         # sync changes back to .neo.yml
Tip Neo auto-reads docker-compose.yml and .env files. If your project already has these, neo deploy . will extract env vars, ports, and service config automatically.

App Templates

Neo ships ten ready-to-run application templates. neo install turns one into a project folder on your machine — it does not install anything on a server. You get an ordinary project you can read, edit and commit, and then deploy like any other.

Shell
neo install ghost          # or just `neo install` for a picker
# → asks which folder (default ./ghost)

cd ghost
neo deploy                 # this is the step that reaches your server

What gets written

FileModeContents
docker-compose.yml0644The app image plus any bundled database, with ports and volumes wired up
.neo.yml0644Deploy config: name, domain, port
.env0600Generated secrets — database passwords, app keys
The scaffold is yours Secrets are generated fresh on each run, so installing the same template twice produces two independent projects. Commit docker-compose.yml and .neo.yml; keep .env out of git — it is written owner-only for that reason. Neo refuses to overwrite an existing docker-compose.yml or .neo.yml, so pick an empty folder.

Available templates

TemplateWhat it isPortBundled services
ghostPublishing platform / newsletter2368MySQL
wordpressWordPress80MySQL
giteaSelf-hosted Git3000PostgreSQL
plausiblePrivacy-friendly analytics8000PostgreSQL + ClickHouse
umamiWeb analytics3000PostgreSQL
chatwootCustomer engagement3000PostgreSQL + Redis
n8nWorkflow automation5678PostgreSQL
minifluxMinimalist RSS reader8080PostgreSQL
vaultwardenBitwarden-compatible password manager80
uptime-kumaUptime monitoring3001

Every image is pinned to an explicit version rather than :latest, so a redeploy can't quietly move you to a new major release. Upgrading is a deliberate edit to the tag in docker-compose.yml.

Bundled databases are for small workloads A template's database runs in a container on the same server as the app. That's fine for a blog or an internal tool. For anything with real user data, point the template at a managed database or a shared service instead — see Databases & data.

Project Config (.neo.yml)

The .neo.yml file declares your deployment configuration. Neo can deploy without one — it will infer the port from your Dockerfile, take the app name from the directory, and prompt for a domain — but that configuration then exists only in /etc/neo/state.json on the server. It isn't in your repo, so it can't be reviewed in a pull request, diffed against last week, or restored if the server is lost.

Commit a .neo.yml. Run neo config init to scaffold one, or neo config generate to convert an existing docker-compose.yml. Every individual field is optional; the file itself is what makes a deploy reproducible.

Everything past a basic deploy lives here environments (staging vs production), workers, release commands, env_encrypted, basic_auth, volumes, scale, health — none of these are reachable without a .neo.yml. Zero-config deploys exist for the first five minutes, not for the second deploy.

By default Neo builds from a Dockerfile in your project root. If yours lives elsewhere (e.g. Dockerfile.local or docker/Dockerfile), set the dockerfile: field or pass --dockerfile / -f to neo deploy. The build context is always the project root.

.neo.yml — Full example
name: my-app
domain: app.example.com
port: 8080
https: true

basic_auth:
  user: admin
  password: s3cr3t
  bypass:
    - /api/*
    - /webhooks/*

ssl:
  certificate: certs/fullchain.pem
  private_key: certs/privkey.pem

dockerfile: Dockerfile

env:
  APP_ENV: production
  LOG_CHANNEL: stderr
env_file: .env.production

volumes:
  data: /var/www/html/database          # flat string → named Docker volume
  storage: /var/www/html/storage

workers:
  queue:
    command: php artisan queue:work --tries=3
  scheduler:
    command: php artisan schedule:work

sidecars:
  redis:
    image: redis:7-alpine
    volumes:
      cache: /data
  builder:
    build:
      context: ../builder
      dockerfile: builder/Dockerfile
    volumes:
      output: /output

dev:
  env_file: .env                    # auto-load local .env
  env:
    APP_ENV: local
    APP_DEBUG: "true"
  volumes:
    data: ./local-data             # override bind-mount path

hooks:
  pre_build: npm run build
  post_deploy: echo "Deployed!"

environments:
  production:
    domain: app.example.com
    env_file: .env.production
  staging:
    domain: staging.example.com
    env:
      APP_DEBUG: "true"
Generating from a large compose file

neo config generate is best-effort and lossy for big multi-service compose files — always review the result. It deploys one app (the build: service) plus internal sidecars, so extra dev services (SMTP, SSO, mocks, object storage) become port-less sidecars. Bind mounts (./data:/…) are dropped, only the first env_file is kept, a sidecar's ports/healthcheck/entrypoint are not migrated, and the build context is always the project root. The command prints a warning for each thing it drops.

name & domain

KeyTypeDefaultDescription
namestringDirectory nameApp name used for container naming (app-{name})
domainstringPromptDomain name for Caddy routing
portintegerDockerfile EXPOSEPort your app listens on inside the container
dockerfilestringDockerfileDockerfile path relative to the project root (context stays the root). --dockerfile overrides.
serverstringCurrent serverPin deployment to a specific server by name
.neo.yml
name: my-app
domain: app.example.com
port: 3000
server: production

env & env_file

Environment variables for your app container. These are merged with other sources during deploy.

KeyTypeDescription
envmapKey-value pairs set as container environment variables
env_filestringPath to a .env file (relative to .neo.yml)
env_encryptedstringPath to an encrypted env file — see env_encrypted
.neo.yml
env:
  APP_ENV: production
  LOG_CHANNEL: stderr
  CACHE_DRIVER: redis
env_file: .env.production  # secrets go here (not committed)
Security Never put secrets (API keys, passwords, database credentials) directly in .neo.yml. Use env_file pointing to a .env file that is gitignored, or set them via neo env set.
${VAR} interpolation Values in .neo.yml may reference other variables as ${VAR} or ${VAR:-default}. On deploy they are resolved from the merged env (env block, env_file, --env) then the OS environment; an unset or empty value uses the :-default when given, otherwise the reference is left as-is. This applies to the env map and to basic_auth, so secrets can live in a gitignored env_file.

release New

Commands that run inside the new container, on the server, after it passes its health check but before Caddy switches traffic to it. This is where database migrations and cache warming belong.

.neo.yml
release:
  - php artisan migrate --force
  - php artisan config:cache

environments:
  dev:
    release:                          # replaces the top-level list
      - php artisan storage:link
      - php artisan migrate --force
A failure rolls the deploy back If a release command exits non-zero, the new container is removed and the old one keeps serving traffic — the deploy aborts instead of putting a broken version live. That is what makes migrate --force safe to automate.
SituationBehaviour
Scaled app (scale: 3)Runs once, in the first new replica — migrations must not run concurrently
--env-onlyRuns against the live container (there is no staging container on that path), so a failure is reported, not rolled back
--allRuns per environment before that environment's traffic switch; one environment failing doesn't affect the others
These must exit A release command is a one-off task, not a process. Anything long-running (a queue worker, a server) belongs in workers: or in the image's own CMD — putting it here stalls every deploy.

Not to be confused with hooks, which run on your machine (npm build, Slack notifications) and cannot see the deployed container.

dockerfile

Point Neo at a Dockerfile that isn't at the project root — a docker/ folder, a per-environment build file — instead of passing --dockerfile on every deploy.

.neo.yml
dockerfile: ./docker/Dockerfile     # used by every environment

environments:
  dev:
    dockerfile: ./docker/Dockerfile.dev   # overrides the top-level one
  production:
    # no dockerfile: here — inherits ./docker/Dockerfile
    server: prod-box

Resolution order, highest first:

OrderSource
1--dockerfile flag
2environments.<env>.dockerfile
3top-level dockerfile:
4./Dockerfile

Paths are relative to the project root (where .neo.yml lives), and the build context is always the project root regardless of where the Dockerfile sits — so COPY paths stay relative to the project, not to the docker/ folder.

--all needs one Dockerfile neo deploy --all builds a single image and ships it to every environment. If two environments name different Dockerfiles, Neo stops and lists the conflict rather than shipping one build as if it were both. Deploy them individually with neo deploy --to <environment>, or pass --dockerfile to force one for the whole run.

env_encrypted New

Built for Laravel's encrypted environment files. Run php artisan env:encrypt, commit the .env.encrypted it produces, and point env_encrypted: at it — neo decrypts at deploy time, so secrets live in git while the key lives in your password manager. neo env encrypt / neo env decrypt are there for when artisan isn't handy (CI, a machine without PHP); they read and write the exact same format.

Shell
# In your Laravel project — key is printed once, save it
php artisan env:encrypt

# No PHP on this machine? Same file, same format
neo env encrypt

# Read it back locally
php artisan env:decrypt
neo env decrypt --stdout
.neo.yml
env_encrypted: .env.encrypted   # committed; decrypted at deploy

environments:
  production:
    env_encrypted: .env.production.encrypted
  staging:
    env_encrypted: .env.staging.encrypted

If .env.encrypted exists and you have no env_file and no plaintext .env in the project, neo picks it up automatically — no config needed.

Where the key comes from

On deploy, neo looks for the key in this order and stops at the first hit:

OrderSourceUse for
1--env-key flagOne-off deploys
2NEO_ENV_KEYCI
3LARAVEL_ENV_ENCRYPTION_KEYCI that already sets it for Laravel
4~/.neo/keys.jsonYour machine — saved after the first prompt
5Interactive promptFirst deploy; offers to save the key
Shell
# Save the key once — later deploys never prompt
neo env key set my-app base64:xxxxx
neo env key list
neo env key forget my-app

# CI
NEO_ENV_KEY=base64:xxxxx neo deploy --to production
What this does and does not protect Encryption covers the file in your repo and on your laptop. The decrypted values are still sent to the server as normal container env vars and stored in /etc/neo/state.json (root-only) so redeploys keep them. Saved keys sit in ~/.neo/keys.json in plain text, mode 0600 — treat that file like an SSH key. Lose the key and the file cannot be recovered; there is no backdoor.
Cipher support AES-256-CBC (Laravel's default), AES-128-CBC, AES-256-GCM and AES-128-GCM all decrypt. neo env encrypt writes AES-CBC with an HMAC-SHA256 MAC, matching env:encrypt. A wrong key or a tampered file fails the MAC check and aborts the deploy rather than shipping partial config.

volumes

Persistent Docker volumes that survive redeploys. Each volume is created as {app-name}-{volume-name}.

.neo.yml
volumes:
  data: /var/www/html/database           # flat string → named Docker volume
  storage: /var/www/html/storage
  logs: /var/log/myapp:/var/log/app       # host:container → bind mount on server
  uploads:                                # structured form (optional)
    path: /app/uploads
    mount: /mnt/ssd/uploads               # pin to host path on server
FormatExampleDescription
Flat stringdata: /app/dataNamed Docker volume mounted at container path
Bind mountlogs: /host:/containerHost directory mounted into container
Structuredpath: + mount:Named volume with optional host path override

Volumes declared here are also available to workers (automatically shared) and sidecars (via name reference). During neo dev, volumes are auto bind-mounted to ./{volume-name} in your project directory.

workers

Background worker containers that use the same image, env vars, and volumes as the main app — but run a different command.

.neo.yml
workers:
  queue:
    command: php artisan queue:work --tries=3 --max-time=3600
  scheduler:
    command: php artisan schedule:work
  websocket:
    command: php artisan reverb:start
KeyTypeDescription
commandstring RequiredCommand to run instead of the default CMD

Workers are deployed with blue-green rollout alongside the main app. View logs with:

Shell
neo logs my-app --worker queue

Workers removed from .neo.yml are automatically deleted on the next deploy.

sidecars

Sidecar containers run alongside your app on the same Docker network but have their own image. They are not exposed publicly via Caddy.

Pre-built image

.neo.yml
sidecars:
  redis:
    image: redis:7-alpine
    volumes:
      cache: /data
    env:
      MAXMEMORY: 256mb

Built from Dockerfile

.neo.yml
sidecars:
  builder:
    build: ../builder           # string form: path to build context
  custom:
    build:                       # object form: context + dockerfile
      context: ..
      dockerfile: docker/custom/Dockerfile
    volumes:
      output: /output            # shares app volume named "output"
    env:
      WORKER_MODE: "true"
KeyTypeDescription
imagestringPre-built Docker image to pull
buildstring or objectBuild context path, or {context, dockerfile}
volumesmapVolume name → container path. Names matching app volumes are shared automatically
envmapSidecar-specific environment variables
commandstringOverride the container's CMD
Shared volumes If a sidecar volume name matches an app volume name (e.g. both declare output), they share the same Docker volume. This is how sidecars like build services can write files that the main app serves.

ssl

Bring your own SSL certificate. The cert and key are uploaded to the server and loaded into Caddy on deploy.

.neo.yml
ssl:
  certificate: certs/fullchain.pem
  private_key: certs/privkey.pem
KeyTypeDescription
certificatestring RequiredPath to PEM certificate file (relative to .neo.yml)
private_keystring RequiredPath to PEM private key file (relative to .neo.yml)

Can also be set per-environment (see environments) or via CLI:

Shell
neo domain my-app app.example.com --cert fullchain.pem --key privkey.pem

https New

Control whether Caddy serves your app over HTTPS with auto-provisioned certificates.

.neo.yml
https: true   # Caddy auto-provisions SSL via Let's Encrypt
ValueBehavior
trueHTTPS with auto-SSL (Let's Encrypt). Requires domain DNS to be pointed at the server.
falseHTTP only. Use when behind a CDN that handles SSL itself.
Not set (default)First deploy uses HTTP. Enable HTTPS later with neo domain --temp or this config.

edge_https — behind Cloudflare Flexible SSL

When a CDN terminates TLS at its edge and talks HTTP to your origin (Cloudflare's Flexible SSL mode), set edge_https: true. The origin serves plain HTTP, but Neo injects forwarded headers so your app still generates correct https:// URLs.

.neo.yml
edge_https: true   # HTTP origin, but app sees X-Forwarded-Proto: https

Neo forwards X-Forwarded-Proto: https, X-Forwarded-Ssl: on, and X-Forwarded-Port: 443 to the container. The CLI equivalent is neo domain my-app --cloudflare-flexible. See Cloudflare Flexible SSL for the full setup.

basic_auth New

Protect your app with HTTP Basic Authentication at the proxy layer. Caddy handles the challenge — the app container is never reached for unauthenticated requests.

.neo.yml
basic_auth:
  user: ${NEO_BASIC_AUTH_USER:-admin}   # ${VAR} resolved at deploy; :-default optional
  password: ${NEO_BASIC_AUTH_PASSWORD}   # from env_file / --env / OS env — keep the secret out of git
  bypass:
    - /api/*
    - /webhooks/*
KeyTypeDescription
basic_auth.userstring RequiredUsername for the HTTP Basic Auth challenge. Supports ${VAR} / ${VAR:-default}.
basic_auth.passwordstring RequiredPassword. Neo bcrypt-hashes it before passing to Caddy. Supports ${VAR} interpolation so you can keep the secret in an env_file.
basic_auth.bypasslistPath patterns excluded from authentication (e.g. /api/*, /webhooks/*). Supports glob-style wildcards.
Proxy-layer auth Basic auth runs entirely inside Caddy. Blocked requests never reach your app container — no extra application code required, and no performance impact on your running process.
Reference secrets with ${VAR} basic_auth values are interpolated at deploy from the merged env (env block, env_file, --env) then the OS environment, so put the password in a gitignored env_file and reference it as ${NEO_BASIC_AUTH_PASSWORD}. Use ${VAR:-default} for a fallback. Basic auth is also saved to server state, so neo domain and neo caddy update keep the protection instead of dropping it.
Security Reference the password via ${VAR} from a gitignored env_file rather than hardcoding it in .neo.yml. Always pair basic_auth with https: true — credentials sent over plain HTTP are not encrypted. (The resolved password is stored in the server's state.json, same as other service secrets.)

Auth not being enforced?

Caddy applies config through its admin API, so there is no reload step in a healthy setup — a deploy takes effect immediately. If the browser still isn't asking for credentials, check what the proxy is actually serving:

Shell
# What Caddy is really serving — AUTH column shows basic or none
neo caddy routes

# Rebuild every route from server state (all apps, or just one)
neo caddy reload
neo caddy reload --app my-app

neo caddy reload rewrites domains, upstream, HTTP/HTTPS mode and basic auth from /etc/neo/state.json. It is the repair tool for a proxy that has drifted from state — after an interrupted deploy, a manual docker change, or a route left over from an older Neo version. neo caddy update is a different thing: it updates the Caddy image, not the routes.

dev New

Local development settings used exclusively by neo dev. Completely ignored during neo deploy.

KeyTypeDescription
dev.env_filestringPath to a .env file loaded for local dev (e.g. .env)
dev.envmapDev-only environment variable overrides
dev.portintegerOverride the local port for neo dev
dev.volumesmapOverride or add dev-only bind mounts
.neo.yml
dev:
  env_file: .env                      # loads your local .env (APP_KEY, secrets, etc.)
  env:
    APP_ENV: local
    APP_DEBUG: "true"
    APP_URL: http://localhost:8080
  volumes:
    data: ./local-data               # short form: override local path for top-level volume
    cache: ./tmp/cache:/tmp/cache    # full form: dev-only bind mount

Env priority for neo dev (highest wins):

  1. dev.env — dev-only overrides
  2. dev.env_file — dev .env file
  3. env — top-level shared vars
  4. env_file — top-level env file
  5. Auto-loaded .env from project root

Volumes defined in the top-level volumes: section are automatically bind-mounted to the project directory during neo dev. Use dev.volumes to override local paths or add dev-only mounts.

Tip Supports ${VAR} interpolation in the dev: section. Variables are resolved from the merged env map, then from your OS environment.

hooks New

Shell commands that run locally at specific points in the deploy lifecycle.

KeyWhenDescription
hooks.pre_buildBefore docker buildRun build steps, generate assets, lint checks
hooks.post_deployAfter deploy completesSend notifications, run smoke tests, tag releases
.neo.yml
hooks:
  pre_build: npm run build && php artisan test
  post_deploy: curl -X POST https://hooks.slack.com/... -d '{"text":"Deployed!"}'

Hooks run in your local shell with NEO_APP_NAME, NEO_DOMAIN, and NEO_ENVIRONMENT environment variables available.

environments

Named deployment targets that override top-level config. Deploy to a specific environment with --to.

.neo.yml
environments:
  production:
    domain: app.example.com
    server: prod-server
    https: true
    env_file: .env.production
    ssl:
      certificate: certs/prod/fullchain.pem
      private_key: certs/prod/privkey.pem
  staging:
    domain: staging.example.com
    server: staging-server
    env:
      APP_DEBUG: "true"
      LOG_LEVEL: debug
Shell
# Deploy to production
neo deploy --to production

# Deploy to staging
neo deploy --to staging

# If multiple environments exist and no --to flag, Neo prompts
neo deploy

Environment fields override top-level: domain, server, port, https, env, env_file, env_encrypted, ssl, basic_auth, volumes.

Tip Define volumes inside each environment to isolate databases and storage per environment. See Per-environment volumes for details.

Deploying Apps

Neo builds your Docker image, transfers it to the server, starts the container, and configures routing — all in one command.

Shell
# Deploy current directory
neo deploy .

# Deploy with explicit options
neo deploy . --domain app.example.com --env APP_KEY=base64:... --to production

# Deploy a different directory
neo deploy ./my-project

Build strategies

Neo automatically chooses the best build strategy based on your environment.

StrategyWhenHow
Local buildDocker is running locallyBuilds on your machine with --platform linux/amd64, then transfers via docker save | ssh docker load
Remote buildNo local DockerPackages source as tar.gz, uploads via SCP, builds on server with docker build
Tip Remote builds are faster when building for the same architecture as the server (both x86_64). Local builds cross-compile which can be slow on Apple Silicon → x86_64.

Env var priority

Environment variables are merged from multiple sources. Higher priority overrides lower.

PrioritySourceExample
1 (highest)--env CLI flagneo deploy --env LOG=debug
2--env-file CLI flagneo deploy --env-file .env
3.neo.yml envDeclared in config
4.neo.yml env_filePath to a .env
5.neo.yml env_encryptedDecrypted .env.encrypted
6docker-compose.ymlAuto-detected from compose
7 (lowest)Server state (redeploy)Previous deployment values

Zero-downtime deployment

Neo uses blue-green deployment on redeploys:

1

Start the new container alongside the old one (app-{name}-next)

2

Wait for the new container's health check to pass

3

Switch Caddy routing to the new container — traffic switches instantly

4

Stop and remove the old container

5

Rename the new container to the canonical name

Health check failure If the health check fails, the old container keeps serving traffic and the new one is removed. No downtime, no broken deploys.

Domains & SSL

Caddy handles reverse proxying and TLS certificate management. Three ways to set up domains:

Shell
# Set a custom domain
neo domain my-app app.example.com

# Assign a temporary sslip.io domain with instant SSL
neo domain my-app --temp

# Use a custom SSL certificate
neo domain my-app app.example.com --cert fullchain.pem --key privkey.pem

# Toggle the origin route mode (without changing the domain)
neo domain my-app --https              # HTTPS at the origin (auto-SSL)
neo domain my-app --http-only          # HTTP only at the origin
neo domain my-app --cloudflare-flexible # HTTP origin behind Cloudflare Flexible SSL

Auto SSL (sslip.io) New

Get instant HTTPS without configuring DNS. The --temp flag assigns {app}.{ip}.sslip.io which auto-resolves to your server.

Shell
$ neo domain ghost --temp

  Temporary domain ready!

    URL:  https://ghost.your-ip-address.sslip.io

  sslip.io resolves to your server IP automatically.
  SSL certificate auto-provisioned via Let's Encrypt.

  Replace with a real domain later:
    neo domain ghost blog.example.com

Custom certificates

Upload your own TLS certificates. Useful for wildcard certs, internal CAs, or when using a cert from a specific provider.

Shell — CLI
neo domain my-app app.example.com --cert /path/to/fullchain.pem --key /path/to/privkey.pem
.neo.yml — Config
ssl:
  certificate: certs/fullchain.pem
  private_key: certs/privkey.pem

The certificate is uploaded to /etc/neo/certs/{app}/ on the server and loaded into Caddy.

Wildcard HTTPS New

Serve *.example.com from a single certificate. Neo configures Caddy for free wildcard certificates two ways — pick based on whether your subdomains are known up front or fully dynamic.

Option A — ACME DNS-01 (neo caddy dns)

Provisions a real Let's Encrypt wildcard certificate up front by proving domain ownership through a DNS record. Requires a DNS provider API token (currently Cloudflare). The token is read from a local environment variable and copied to the server as a root-only file — it is never typed interactively.

Shell
# Build a DNS-enabled Caddy, store the token, enable wildcard SSL
CLOUDFLARE_API_TOKEN=... neo --server prod caddy dns example.com --app my-app

# Bind the wildcard to an app afterwards (if --app was omitted)
neo domain my-app "*.example.com" --add
FlagDescription
--provider <name>DNS provider for DNS-01 (default: cloudflare)
--token-env <var>Local env var holding the API token (default: provider-specific, e.g. CLOUDFLARE_API_TOKEN)
--app <app>Bind the wildcard *.domain to an app after setup

Option B — Guarded on-demand TLS (neo caddy ondemand)

For unbounded, dynamic tenant subdomains where you can't pre-list every hostname. Caddy issues a real certificate for each subdomain on its first request, gated by an ask URL your app controls — it must return 200 only for allowed hostnames.

Shell
neo --server prod caddy ondemand example.com --app my-app --replace-domains
FlagDescription
--app <app>App to bind the wildcard to (auto-derives the ask URL from the app)
--ask-url <url>URL Caddy calls to allow/deny issuance (required when --app is omitted)
--replace-domainsReplace the app's extra domains with the wildcard
Independent trees coexist Both commands merge into Caddy's TLS config and are idempotent. You can run multiple wildcard trees on one server — e.g. *.example.com for production and *.staging.example.com for staging — each getting its own certificate. Re-run once per tree.
Set up before binding Binding a plain *. hostname with neo domain is guarded — it requires DNS-01 or on-demand TLS to be configured first, otherwise Caddy can't issue the certificate.

Cloudflare Flexible SSL New

If your app sits behind Cloudflare in Flexible SSL mode — HTTPS between the browser and Cloudflare, plain HTTP from Cloudflare to your origin — serve the origin over HTTP while still telling the app it's on HTTPS. This prevents redirect loops without disabling TLS at the edge.

Shell
# Switch an existing app's origin route to Cloudflare Flexible mode
neo domain my-app --cloudflare-flexible
.neo.yml — equivalent config
edge_https: true

Neo sets an HTTP-only origin route and injects the following headers so the app generates correct https:// URLs:

HeaderValue
X-Forwarded-Protohttps
X-Forwarded-Sslon
X-Forwarded-Port443
Redirect loop? "Too many redirects" behind Cloudflare almost always means you're on Flexible SSL but the origin expects HTTPS. Switch the origin to HTTP with --cloudflare-flexible (or edge_https: true). --cloudflare-flexible cannot be combined with --https.

Deployment Versions New

Every deploy records the git commit it was built from, so “which code is running?” has an answer. Before this, an image tag carried only a timestamp: two deploys of different commits looked identical, and redeploying the same commit looked like a new version.

Shell
$ neo list
NAME               DOMAIN                 PORT   STATUS      VERSION
 shop             shop.example.com       8080   running     v1.4.2 (a1b2c3d)
 api              api.example.com        3000   running     9f01e2a *      # * = built from uncommitted changes

One app in detail

Shell
$ neo status shop

  shop  ●  on production

  Deployment
    Version      v1.4.2 (a1b2c3d)
    Commit       a1b2c3d4e5f6789...
    Branch       main
    Image        neo-shop:20260818-045536-a1b2c3d
    Deployed     2026-08-18T04:55:36Z  by alan@macbook
    Env digest   sha256:6fb787f3a6ad231e

The env digest fingerprints the environment the build ran with, so the same commit deployed twice with different configuration is distinguishable — otherwise “the code didn’t change, why did behaviour?” is unanswerable.

History

Shell
$ neo deploys shop

DEPLOYED             VERSION            BY
 just now           v1.4.2 (a1b2c3d)   alan@macbook
  3h ago             9f01e2a            ci@github-actions
  2d ago             v1.4.1 (7c4d2b1)   alan@macbook          image pruned

History lives on the server, in /etc/neo/deploys/<app>.jsonl, so everyone who deploys shares one record — your laptop, a colleague’s, and CI. It survives a fresh clone, and it is append-only, so two simultaneous deploys cannot overwrite each other’s entries. Entries marked image pruned are informational: Neo keeps only the last few images, so those builds are no longer on disk.

Inside the container

Each deploy injects variables describing itself, so the running container can identify its own build:

Shell
$ neo run shop -- printenv | grep NEO_GIT
NEO_GIT_COMMIT=a1b2c3d4e5f6789...
NEO_GIT_SHORT_COMMIT=a1b2c3d
VariableExample
NEO_DEPLOYMENT_ID20260818-045536-a1b2c3d
NEO_GIT_COMMITfull sha
NEO_GIT_SHORT_COMMITa1b2c3d
NEO_GIT_BRANCHmain
NEO_GIT_TAGv1.4.2 (only when the commit is tagged)
NEO_DEPLOYED_ATRFC3339 timestamp
Wire them into anything These are set before .neo.yml interpolation runs, so you can reference them in your own config — Neo needs no knowledge of the tool you’re feeding:
.neo.yml
env:
  SENTRY_RELEASE: "${NEO_GIT_COMMIT}"
A value you set explicitly always wins.

Not using git?

Nothing requires it. A scaffolded template or a source tarball simply shows no version. In CI, where a shallow checkout often has no usable repository, Neo falls back to NEO_GIT_COMMIT, GITHUB_SHA or CI_COMMIT_SHA.

Deploying uncommitted changes Neo warns, records the deployment as dirty, and marks it with * everywhere it is shown. The recorded commit then describes only part of what shipped — which is how “production is broken but git says the fix is in” happens.

Checking a deploy from CI

Shell
# fail the job if the running commit isn't the one we just built
[ "$(neo status shop --json | jq -r .deployment.commit)" = "$GITHUB_SHA" ] || exit 1

Syncing State New

CLI commands like neo domain and neo env set update server state but don't modify .neo.yml. Use neo sync to write server state back to your config.

Shell
# Preview changes (dry run)
$ neo sync my-app --dry-run

  Syncing my-app: server → .neo.yml (dry run)

  ~ domain        example.com → my-app.1.2.3.4.sslip.io
  ~ https         false → true
  + env.NEW_VAR   hello

  3 change(s) detected. Run without --dry-run to apply.

# Apply changes
$ neo sync my-app

  Write 3 change(s) to .neo.yml? (y/n): y
  ✓ .neo.yml updated (3 changes)

What gets synced

FieldDirection
domainServer → .neo.yml
portServer → .neo.yml
httpsServer → .neo.yml
env varsServer → .neo.yml (full replace)
volumesServer → .neo.yml (additions only)
workersServer → .neo.yml

Per-Environment Volumes

When you define volumes inside each environment block (instead of top-level), Neo creates separate Docker volumes per environment. This ensures production and staging never share a database or storage directory.

.neo.yml — Isolated volumes
environments:
  production:
    env_file: .env.production
    volumes:
      data: /var/www/html/database
      storage: /var/www/html/storage
  staging:
    env_file: .env.staging
    volumes:
      data: /var/www/html/database
      storage: /var/www/html/storage

How naming works

Neo namespaces volumes by combining the app name and environment:

EnvironmentVolume nameDescription
production{app}-production-dataProduction database
production{app}-production-storageProduction uploads & cache
staging{app}-staging-dataStaging database
staging{app}-staging-storageStaging uploads & cache
Top-level vs per-environment Volumes defined at the top level of .neo.yml are shared across all environments. Move them inside each environments block to isolate them. This is especially important for databases — you don't want staging writes hitting your production SQLite file.

When to use which

ScenarioApproach
Database files, user uploadsPer-environment volumes (isolated)
Shared build artifacts, static assetsTop-level volumes (shared)
Single environment onlyEither — no difference

Team Access (SSH Keys)

Give teammates their own access to a server with neo key — no shared passwords, no copying private key files, no GitHub. Everyone uses their own key; the admin authorizes or revokes each person individually.

Two roles

RoleWho they areCommands they run
AdminOwns the server — ran neo init (or attached it) and already has access.neo key add, neo key list, neo key remove
TeammateWants access to the admin's server. Starts with nothing configured.neo key show, then neo attach (or neo deploy)

The only thing that passes between people is the teammate's public key — safe to send over Slack or email. Private keys never leave their owner's machine.

The admin needs server access first neo key add, list, and remove all run on the server, so the admin must have it in their own config before authorizing anyone. Fresh server → neo init <user@host>. Server that was set up on another machine → neo attach <user@host>. Until then, those commands fail with "no server selected — run: neo init".
Shell
# [Teammate] Generate + print your public key to send to the admin
neo key show

# [Admin] Authorize a teammate's public key on the server
neo key add "ssh-ed25519 AAAAC3Nz... [email protected]"

# Target a specific server (otherwise uses the current one,
# or asks which when you have more than one configured)
neo key add --server production "ssh-ed25519 AAAAC3Nz... [email protected]"

# [Admin] List authorized keys (marks your own)
neo key list

# [Admin] Revoke a key by its number from the list
neo key remove 2

Step-by-step workflow

0

[Admin] Have the server in your config. If you ran neo init on this machine it already is. If it was set up elsewhere, run neo attach [email protected] first — that is what lets neo key add reach the server.

1

[Teammate] Run neo key show — no server or config needed yet. Neo generates a key at ~/.neo/neo_ed25519 if one doesn't exist, then prints the single-line public key.

2

[Teammate → Admin] Send the key. It looks like ssh-ed25519 AAAAC3Nz... name@host. Safe to share — it's a public key.

3

[Admin] Run neo key add "<key>". Neo appends it to ~/.ssh/authorized_keys on the server and prints the server: line (e.g. [email protected]) to send back to the teammate.

4

[Teammate] Connect. Run neo attach [email protected] to register the server locally — the dashboard and every command then work. (Deploy only? Skip attach and put server: [email protected] in .neo.yml, then neo deploy.) The teammate never runs neo init — that's the admin's job and would wipe the server's apps.

.neo.yml — teammate's config
name: my-app
domain: app.example.com
server: root@your-server-ip   # full user@host — the @ skips local config
The server: value must be user@host Use the full user@host (e.g. [email protected]), not a bare name. The @ is what lets a teammate connect without the server being registered in their ~/.neo/config.json. A bare name like server: production is looked up in local config and fails with "no server selected — run: neo init" for someone who never ran neo init. neo key add prints the exact line to copy.
Commands other than deploy Only neo deploy reads server: from .neo.yml. For any other command, pass the same address with the global flag — neo logs my-app --server [email protected]. To make the bare neo dashboard and all commands work without a flag, register the server once with neo attach [email protected] — it adds the server to ~/.neo/config.json with no setup and no risk to a live server (see Attaching an existing server).

Attaching an existing server

neo attach registers a server that was already set up by someone else into your local config, so the dashboard, neo list, neo logs, and every other command work — without a --server flag on each call. Unlike neo init, it never installs Docker or Caddy and never overwrites the server's state, so it is safe to run against a live server with apps deployed.

Shell
# Add an already-initialized server to your local config
neo attach [email protected]

# Then everything works with no flag
neo list
neo logs my-app
attach vs init Run neo init only on a fresh server — it installs Docker + Caddy and writes a new state file, which would wipe the app registry of a server that is already running apps. To join a server someone else already initialized, always use neo attach.

Listing keys

neo key list shows every authorized key on the server with a number, and marks your own key with (you):

Shell
$ neo key list

  Authorized keys:

  #1   alan@macbook  (you)
  #2   [email protected]
  #3   ...Xk9mRpQ2aZ

  Remove a key with: neo key remove <number>

Revoking access

Run neo key remove <number> with the number from neo key list. Neo will not let you remove your own key — doing so would lock you out.

Shell
neo key remove 2   # revokes key #2
Idempotent add Running neo key add with a key that is already present is a no-op — the key is not duplicated in authorized_keys.

Domain Redirects New

Redirect any domain to another URL — no app, sidecar, or service required. Neo configures Caddy directly, so the redirect is live in seconds with auto-SSL provisioned for the source domain.

Shell
# 301 permanent redirect (default)
neo redirect add vxero.dev vxero.com

# 302 temporary redirect
neo redirect add old.api.com new.api.com --temporary

# List all active redirects
neo redirect list

# Remove a redirect
neo redirect remove vxero.dev

How it works

DetailBehaviour
Default redirect type301 permanent. Pass --temporary for a 302.
Path preservationThe full request path is forwarded. vxero.dev/blog redirects to vxero.com/blog.
Auto-SSLCaddy automatically provisions a Let's Encrypt certificate for the source domain — point DNS first.
Scheme normalisationThe destination is always rewritten to https://. Pass https:// explicitly if needed.
Conflict detectionAdding a redirect for a domain already claimed by an app returns an error. Remove the domain from the app first.

Example output

Shell
$ neo redirect add vxero.dev vxero.com

  ✓ Redirect active!

  From   vxero.dev
  To     https://vxero.com
  Type   301 permanent
  SSL    auto-provisioned by Caddy

  Paths are preserved: vxero.dev/blog → vxero.com/blog
DNS must be pointed first Caddy provisions SSL via Let's Encrypt, which requires the source domain to resolve to your server's IP before you run neo redirect add. Use neo redirect add ... --temporary to test without waiting for DNS propagation (no SSL for the source domain until DNS is correct).

Shared Services

A shared service is a database or cache container that runs at the server level, not inside any single app. Multiple apps can connect to the same instance — useful on small VMs where spinning up a separate database per app wastes hundreds of MB of RAM.

Supported types

TypeDefault imageWhat it provisions
mysqlmysql:8Database + dedicated user, injects DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD
mariadbmariadb:11Same as MySQL — drop-in compatible
postgrespostgres:16Database + dedicated role, injects DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD
redisredis:7-alpineShared cache/queue, injects REDIS_HOST, REDIS_PORT

Usage

Shell
# Create a shared service (runs once on the server)
neo service create mysql
neo service create postgres
neo service create redis

# See all services and their status
neo service list

# Show connection details anytime (host, port, user, password, URL)
neo service info mysql

# Other operations
neo service logs mysql
neo service start|stop|restart mysql
neo service remove mysql
Retrieve connection info anytime neo service create prints the host, port, database, username, and password. Lost them? Run neo service info <name> to reprint everything — including a ready-to-use connection URL. The credentials are kept in server state, so they are never a one-time reveal. Wire them into your app with neo service link, neo env set, or your .neo.yml env: block.
Shared services are single-node All data lives on one VPS with no replication or automated backups. Shared services are best for development, staging, or low-stakes side projects. For production with real user data, use a managed database (Neon, PlanetScale, Supabase, RDS) — see the Databases & Data section below.

Databases & Data

Neo can run databases in Docker via shared services or bundled templates. This works well for small projects, but production workloads with real user data should use managed database services.

Three approaches

ApproachHowBest forData safety
Bundled Template includes DB (e.g. Ghost + MySQL) Hobby projects, testing You manage backups
Shared service neo service create mysql Multi-app VPS, saving RAM You manage backups
Managed (external) Neon, PlanetScale, Supabase, RDS Production with real users Provider handles backups, replication, failover

When to use what

ScenarioRecommendation
Side project, blog, personal siteBundled or shared service — simple, all on one VPS
3–5 apps on one $12/mo VPSShared service — one Postgres for all apps saves ~200MB RAM each
SaaS with paying customersManaged database — automated backups, point-in-time recovery, replication
Regulatory / compliance requirementsManaged database — encryption at rest, audit logs, certifications
Data you cannot afford to loseManaged database — Docker volumes are not a backup strategy

Why managed databases for production

Docker databases on a single VPS have real limitations that managed services solve:

CapabilityDocker on VPSManaged service
Automated backupsManual (neo backup)Daily, with retention policy
Point-in-time recoveryNot availableRestore to any second
Replication / failoverNot availableAutomatic
Transaction-safe snapshotsVolume tarballs onlyWAL-based, consistent
Security patchesManual image updatesApplied automatically
MonitoringNoneSlow queries, connections, disk
ScalingVertical only (bigger VPS)Read replicas, auto-scaling
Important neo backup creates volume-level tarballs — it stops the container, tars the volume, and restarts. This is not transaction-safe. For Docker databases, run mysqldump or pg_dump before backup for consistency.

Recommended managed providers

ProviderDatabaseFree tier
NeonPostgreSQL512MB storage, autoscaling
PlanetScaleMySQL5GB storage, 1B reads/mo
SupabasePostgreSQL500MB storage, 2 projects
Redis CloudRedis30MB, 1 database
DigitalOceanPostgres / MySQL / RedisFrom $15/mo
AWS RDSAll major engines750hr/mo free tier (12 months)

Connecting to an external database

Point your app at a managed database by setting the connection string:

Shell
# Set database URL (app restarts automatically)
neo env set my-app DATABASE_URL=postgres://user:[email protected]/mydb

# Or set individual vars
neo env set my-app \
  DB_HOST=db.neon.tech \
  DB_PORT=5432 \
  DB_DATABASE=mydb \
  DB_USERNAME=user \
  DB_PASSWORD=secret
.neo.yml
env:
  DB_CONNECTION: pgsql
env_file: .env.production  # contains DATABASE_URL (not committed to git)
Tip Keep database credentials in .env.production (gitignored) and reference it via env_file in .neo.yml. Never commit secrets to your repository.

Backing up Docker databases

If you do run databases in Docker, supplement Neo's volume backups with database-native dumps:

Shell — via SSH
# MySQL dump
neo ssh
docker exec svc-mysql mysqldump -u root -p'$PASS' --all-databases > /var/backups/mysql-$(date +%F).sql

# PostgreSQL dump
docker exec svc-postgres pg_dumpall -U postgres > /var/backups/pg-$(date +%F).sql

# Download to your laptop
scp root@your-server:/var/backups/mysql-2026-03-23.sql ./backups/

Server Management

Shell
# List servers
neo servers

# Switch active server
neo use production

# Remove a server from config
neo servers remove old-server

# SSH into server
neo ssh

# Target a specific server for any command
neo list --server staging

Firewall (CrowdSec)

Neo ships an optional firewall powered by CrowdSec — a free, open-source intrusion-prevention engine. It watches your logs, auto-bans brute-force attackers with an nftables bouncer, and pulls in a community-maintained blocklist of known-malicious IPs. Everything runs on your server; nothing is sent to Neo.

Install

Shell
# Install CrowdSec + the nftables bouncer on the active server
neo firewall install

Install is idempotent — running it again on an already-protected server is a no-op. Neo records the firewall state in /etc/neo/state.json.

Manage decisions

Shell
# Service status + active decision count
neo firewall status

# List every active decision (ban)
neo firewall list

# Manually ban / unban an IP or range
neo firewall block 203.0.113.10
neo firewall unblock 203.0.113.10

# Update the engine, bouncer, and community blocklists
neo firewall update
CommandWhat it does
firewall installInstalls CrowdSec + nftables bouncer on the server
firewall statusShows service health and how many IPs are currently banned
firewall listLists active decisions (IP, type, origin, duration, reason)
firewall block <ip>Manually bans an IP or CIDR range
firewall unblock <ip>Removes a ban
firewall updateUpdates the engine, bouncer, and blocklists
Auto-bans work out of the box Once installed, CrowdSec detects and blocks brute-force attempts (SSH, HTTP, etc.) automatically — no configuration needed. Use block/unblock only when you want to manage an IP by hand.

Stealth Mode

Stealth mode hides your server from IP-based discovery. When enabled, Neo removes Caddy's catch-all welcome page, so direct requests to the server's IP get no response — only your configured domains serve traffic. This stops scanners from fingerprinting the box.

Shell
# Toggle stealth on (removes the IP welcome page)
neo stealth

# Run again to toggle it back off (restores the welcome page)
neo stealth
Domains keep working Stealth only affects raw IP access. Apps served on real domains are unaffected — visitors reach them exactly as before.

CLI Reference

CommandDescription
neoInteractive TUI dashboard
neo init <host>Initialize a fresh server (Docker + Caddy)
neo attach <host>Add an already-initialized server to local config (no setup, no overwrite)
neo config initScaffold a new .neo.yml for the project (--yes for defaults)
neo config generateGenerate .neo.yml from docker-compose.yml
neo deploy [path]Build and deploy a Dockerfile project
neo install [app]Scaffold a bundled app template into a folder
neo listList apps on the server
neo status [app]Server health, or full detail for one app
neo deploys <app>Deployment history — what shipped, when, by whom
neo logs <app>Stream container logs
neo domain <app> [domain]Set domain. --temp for sslip.io, --cert/--key for custom SSL, --https/--http-only to switch origin mode, --cloudflare-flexible for Cloudflare Flexible SSL
neo caddy dns <domain>Wildcard HTTPS via ACME DNS-01 (free wildcard cert)
neo caddy ondemand <domain>Guarded on-demand wildcard TLS for dynamic subdomains
neo sync <app>Sync server state back to .neo.yml
neo env <app>View env vars (secrets masked)
neo env set <app> K=VSet env var (auto-restarts)
neo env unset <app> KEYRemove env var
neo env import <app> .envBulk import from file
neo start|stop|restart <app>App lifecycle control
neo update <app>Pull latest image and redeploy
neo remove <app>Remove app and routing
neo key showGenerate (if needed) and print your Neo public key
neo key add <pubkey>Authorize a teammate's public key on the server
neo key listList authorized keys on the server (marks your own)
neo key remove <number>Revoke a key by its number from neo key list
neo redirect add <from> <to>Redirect a domain to a URL (301 by default). Pass --temporary for 302.
neo redirect listList all active domain redirects
neo redirect remove <from>Remove a domain redirect
neo service create [type]Create shared service (prints connection info)
neo service info <svc>Show connection details (host, port, user, password, URL)
neo serversList configured servers
neo servers remove <name>Remove server from config
neo use <name>Switch active server
neo sshSSH into current server
neo versionShow version, check for updates
neo upgradeSelf-update to latest version
neo backup <app>Backup data volumes
neo volumesList Docker volumes on server

Global flags

FlagDescription
--server <name|user@host>Target a specific server — by config name, or a full user@host to connect without registering it locally

Server Requirements

RequirementDetails
OSUbuntu 24.04+, Debian (any version), Fedora 39+, or CentOS / RHEL / AlmaLinux / Rocky 9+. The package manager is auto-detected (apt for Debian/Ubuntu, dnf for RPM-based distros).
AccessRoot SSH access with key-based authentication
Ports80 (HTTP), 443 (HTTPS) must be open
RAMMinimum 512MB. 1GB+ recommended for builds.
Unsupported Older releases (Ubuntu 22.04 / 20.04, Fedora 38 and below, CentOS 7), plus Alpine and Arch, are not supported. neo init detects the OS and version from /etc/os-release and shows a clear error if unsupported.

What Neo installs on your server

When you run neo init, Neo touches exactly two things on your VM:

InstalledWhy
DockerRuns all containers — your app, workers, sidecars, shared services
CaddyReverse proxy, handles HTTPS and domain routing

That's it. Neo does not install PHP, Python, Go, Node.js, Ruby, Java, or any other language runtime. It does not install database clients, build tools, package managers, or any other system software.

Your VM stays clean Everything your app needs — language runtimes, dependencies, binaries — lives inside your Docker image. The host VM stays minimal: just the OS, Docker, and Caddy. If you want to install additional tools directly on the server, that's entirely up to you — Neo won't interfere. SSH in with neo ssh and do whatever you need.

What Neo is not

Neo is deliberately focused. Before adopting it, it's worth being clear about what it does not do.

Not supportedWhy / what to use instead
Clusters & multi-node orchestration Neo manages one app on one server per deploy. It does not spread containers across multiple machines, balance load between nodes, or coordinate distributed state. Use Kubernetes or Nomad if you need that.
Docker Swarm Neo does not use or interact with Docker Swarm mode. Every container is a standalone docker run — no Swarm services, no overlay networks, no replicas.
Auto-scaling Neo does not add or remove servers based on load. Scaling means either upgrading your VPS vertically (more RAM/CPU) or deploying the same project to an additional server manually.
Blue/green or canary deploys Neo does zero-downtime deploys by starting the new container before stopping the old one. It does not split traffic between versions or support staged rollouts.
Managed databases Neo can run databases in Docker containers but does not manage replication, failover, or automated backups for them. For production data, use a managed service (Neon, Supabase, RDS).
The point Neo is for developers who want to own their server without managing Kubernetes. If your app runs on one VM and you want git push-style deploys with zero infrastructure overhead, Neo is the right tool. If you need multi-region failover, cluster scheduling, or dynamic scaling, you need a different tool.

Architecture

Neo is a single Go binary that runs on your local machine. It has no server-side component — everything happens over SSH.

Container naming

ContainerPatternExample
Appapp-{name}app-ghost
Workerapp-{name}-worker-{worker}app-ghost-worker-queue
Sidecarsvc-{name}-{sidecar}svc-ghost-redis
Shared servicesvc-{name}svc-mysql
Caddyneo-caddyneo-caddy

Docker network

All containers join the neo Docker network. Containers reference each other by name (e.g. svc-mysql:3306).

Volume naming

Volumes are prefixed with the app name: {app}-{volume}. For example, an app named ghost with a volume content creates ghost-content.