Remote server management over SSH. Deploy Docker apps, manage Caddy reverse proxy, and handle backups — all from your laptop. No agent required.
▀▄▀ █ █ █▀▀ █▀█ █▀█ ┃ █▄ █ █▀▀ █▀█
█ █ ▀▄▀ ██▄ █▀▄ █▄█ ┃ █ ▀█ ██▄ █▄█
⚡ neo v0.22.0
Like Kamal — you run it locally, it SSHes into your VPS and manages everything remotely.
Your Mac / Laptop (runs neo)
│
│ SSH (key-based auth)
│
▼
Remote VPS (any provider)
└── Docker
├── neo-caddy (reverse proxy + auto SSL)
├── app-plausible (your app)
├── app-ghost (your app)
├── svc-postgres (bundled database)
└── ... all on "neo" Docker network
Coding with Claude, Cursor, or another AI agent? Install the Neo skills so your
agent knows how to drive neo correctly — commands, .neo.yml config, deploy flow,
and gotchas — instead of guessing.
👉 github.com/solutionforest/neo-skill
The skills teach an agent the same things a fluent neo user knows, so it can init
servers, write .neo.yml, deploy, and troubleshoot as well as it knows the tool.
Quick install (auto-detects Claude Code, Cursor, Copilot, Windsurf, Cline):
git clone https://github.com/solutionforest/neo-skill.git
cd neo-skill
./install.sh /path/to/your/projectClaude Code users can add it as a plugin instead:
claude plugin add /path/to/neo-skillSee the neo-skill README for per-tool manual steps.
macOS and Linux:
curl -fsSL https://neo.vxero.dev/neo | shBuilding from source? See Build With Docker below.
neo now builds through Docker by default. make build and make build-all do not use your host Go toolchain.
Build a local binary for your current platform:
cd neo
make build
./bin/neo --helpCross-compile all release binaries through Docker:
cd neo
make build-allIf you want to build and run the CLI as a container image:
cd neo
make image-buildThat builds vxero/neo:latest from the local Dockerfile.
Run it with your SSH keys, local neo config, and current working directory mounted:
docker run --rm -it \
-v "$HOME/.ssh:/root/.ssh:ro" \
-v "$HOME/.neo:/root/.neo" \
-v "$PWD:/workspace" \
-w /workspace \
vxero/neo:latestOr use the Make target:
make docker-runExample: run the containerized CLI against a project directory and deploy it:
docker run --rm -it \
-v "$HOME/.ssh:/root/.ssh:ro" \
-v "$HOME/.neo:/root/.neo" \
-v "/path/to/your-app:/workspace" \
-w /workspace \
vxero/neo:latest deploy . --domain app.example.comNotes:
make buildwritesbin/neousing thegolang:1.24-alpineDocker image.make build-allwrites cross-platform binaries todist/through the same Dockerized build flow.~/.sshis mounted read-only so the container can use your existing SSH keys.~/.neois mounted so configured servers persist between container runs."$PWD:/workspace"lets you runneo deploy .against the project on your host machine.
⚠️ Experimental / testing (測試) build. Neo Desktop is a macOS menu-bar (tray) app that wraps the neo CLI — live CPU/RAM/disk gauges, per-app start/stop/restart, a log viewer, and integrated SSH/container terminals. It is early and under active development: expect rough edges, breaking changes, and missing features. TheneoCLI remains the supported interface — don't rely on the GUI for production work yet.
Caveats before you build or run it:
- macOS only right now (Apple Silicon + Intel). No Windows/Linux builds.
- Unsigned builds. Release
.dmgs are ad-hoc signed unless a signing certificate is configured, so macOS Gatekeeper blocks the first launch. Either right-click the app → Open, or clear the quarantine flag:xattr -dr com.apple.quarantine "/Applications/Neo Desktop.app" - Not the Docker build path. Unlike the CLI (
make build), the desktop app builds with your host toolchains — Rust, Node, and Go. - It reuses your existing neo config (
~/.neo) and SSH auth through a bundledneo-bridgesidecar, so servers you added with the CLI show up automatically.
Prerequisites: Rust (via rustup), Node 18+, and Go 1.23+.
cd apps/desktop
# 1. Build the neo-bridge sidecar for your Mac's arch.
# Apple Silicon (M1/M2/M3):
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 \
go build -trimpath -ldflags "-s -w" \
-o src-tauri/binaries/neo-bridge-aarch64-apple-darwin ../../cmd/neo-bridge
# Intel Macs: use GOARCH=amd64 and the -x86_64-apple-darwin suffix instead.
# 2. Install frontend dependencies.
npm ci
# 3. Build the app bundle (.app + .dmg land in src-tauri/target/release/bundle/).
npm run tauri buildPrefer a hot-reloading dev window? Build the sidecar (step 1) first, then:
cd apps/desktop
npm install
npm run tauri devSidecar filename must match your Rust host triple — check it with
rustc -vV | grep host. Tauri resolvesbinaries/neo-bridgetobinaries/neo-bridge-<triple>at build time; a missing or mismatched suffix fails the build. See.github/workflows/desktop-release.ymlfor the canonical multi-arch build.
$ neo ▀▄▀ █ █ █▀▀ █▀█ █▀█ ┃ █▄ █ █▀▀ █▀█
█ █ ▀▄▀ ██▄ █▀▄ █▄█ ┃ █ ▀█ ██▄ █▄█
⚡ neo v0.22.0
No servers configured.
? Server SSH address
│ root@159.65.100.42
? SSH password ← only if no SSH keys found
│ ••••••••••
? Server name (leave empty to auto-detect)
│ production
Initializing server...
✓ Connected (Ubuntu 24.04 LTS, 4GB RAM, 2 CPU)
✓ System packages updated
✓ Docker 27.1.1 installed
✓ Docker network "neo" created
✓ Caddy 2.9.1 running (ports 80, 443)
✓ State initialized
First run detects no servers and drops you straight into setup — asks for SSH password if no keys are found, runs apt update && upgrade, then installs Docker and Caddy. After init completes, you land in the interactive dashboard.
Every time you run neo, you get a full TUI hub:
▀▄▀ █ █ █▀▀ █▀█ █▀█ ┃ █▄ █ █▀▀ █▀█
█ █ ▀▄▀ ██▄ █▀▄ █▄█ ┃ █ ▀█ ██▄ █▄█
⚡ neo v0.22.0
Server: production (root@159.65.100.42)
? What would you like to do?
> Servers 2 configured
Applications 3 apps, 2 running
Deploy Project deploy local repo
Install App scaffold a template into a folder
Quit
Servers
─────────────────────────────────────────────────
● production root@159.65.100.42 (active)
staging root@staging.mysite.com
? ›
> Add New Server
Switch Server
← Back
Apps on production
──────────────────────────────────────────────────────
● plausible analytics.mysite.com running
● gitea git.mysite.com running
○ ghost blog.mysite.com stopped
3 apps · 2 running · 1 stopped
? Select an app to manage
> ● plausible analytics.mysite.com
● gitea git.mysite.com
○ ghost blog.mysite.com
← Back
plausible ghcr.io/plausible/community-edition:v2.1.4
https://analytics.mysite.com
Status: ● running
└ postgres
? Action for plausible
> View Logs
Restart
Stop
Change Domain
Update Image
Remove
← Back
neo install doesn't touch a server — it scaffolds a ready-to-deploy project into a folder: a docker-compose.yml (the app image plus its bundled databases), a .neo.yml, and a .env with generated secrets. You then neo deploy it like any other project, and can edit the files first.
$ neo install plausible ? Folder to scaffold Plausible Analytics into
│ ./plausible
? Domain for Plausible Analytics (optional)
│ analytics.mysite.com
╭──────────────────────────────────────────────╮
│ ✓ Plausible Analytics scaffolded │
│ │
│ Folder: ./plausible │
│ docker-compose.yml · .neo.yml · .env │
│ │
│ Next steps: │
│ cd ./plausible │
│ neo deploy │
╰──────────────────────────────────────────────╯
$ cd plausible && neo deploy # builds/runs on your server, with auto-SSL$ neo deploy --domain myapp.example.com Deploying my-app
Server: production (159.65.100.42)
Domain: myapp.example.com
Port: 8080
→ Docker detected locally — building on this machine
✓ Image built locally
✓ Image transferred to server
✓ New container started
✓ Health check passed
✓ SSL certificate issued for myapp.example.com
╭────────────────────────────────────────────╮
│ ✓ my-app is live! │
│ │
│ URL: https://myapp.example.com │
│ │
│ Redeploy after changes: │
│ neo deploy │
╰────────────────────────────────────────────╯
Redeployments use zero-downtime blue-green swaps — the new container starts alongside the old one, gets health-checked, then Caddy switches traffic instantly:
Redeploying my-app
...
✓ New container started
✓ Health check passed
✓ Traffic switched to new version (myapp.example.com)
If the new container fails the health check, it's automatically rolled back:
✗ New container failed health check — rolled back
→ Old version still running. Debug with: neo logs my-app
No local Docker? No problem — neo uploads your source and builds on the server:
→ No local Docker — building on server
✓ Source packaged (12.4 MB)
✓ Source uploaded
→ Building image on server (this may take a while)...
✓ Image built on server
$ neo list Server: production (159.65.100.42)
NAME DOMAIN STATUS IMAGE
──────────────────────────────────────────────────────────────────────────────
● plausible analytics.mysite.com running ghcr.io/plausible/community-edition:v2.1.4
● gitea git.mysite.com running gitea/gitea:1.22-rootless
○ ghost blog.mysite.com stopped ghost:5-alpine
3 apps · 2 running · 1 stopped
$ neo install ? Choose an app to scaffold
> chatwoot Open-source customer engagement platform
ghost Professional publishing platform
gitea Lightweight self-hosted Git service
miniflux Minimalist and opinionated RSS feed reader
n8n Workflow automation tool
plausible Privacy-friendly Google Analytics alternative
umami Simple, fast, privacy-focused web analytics
uptime-kuma Self-hosted monitoring tool
vaultwarden Lightweight Bitwarden-compatible password manager
wordpress The world's most popular CMS
$ neo servers Servers
─────────────────────────────────────────────────
● production root@159.65.100.42 (active)
staging root@staging.mysite.com
$ neo use staging ✓ Switched to server "staging"
$ neo start ghost ✓ ghost started
$ neo stop plausible ✓ plausible stopped
$ neo restart gitea ✓ gitea restarted
$ neo update ghost ✓ Image pulled
✓ ghost updated and running
$ neo remove ghost ? Remove ghost? Data volumes will be kept. Yes, remove
✓ ghost removed. Data volumes preserved on server.
$ neo logs plausible --tail 20 -f [2026-03-18 14:32:01] Plausible running on port 8000
[2026-03-18 14:32:02] Connected to PostgreSQL
[2026-03-18 14:32:02] Connected to ClickHouse
[2026-03-18 14:32:05] GET /api/health 200 1ms
[2026-03-18 14:32:10] POST /api/event 202 3ms
...
$ neo domain plausible stats.newdomain.com ✓ Domain updated — https://stats.newdomain.com
$ neo volumes Volumes on production (159.65.100.42)
──────────────────────────────────────────────────────
plausible-events plausible docker volume
plausible-db plausible docker volume
plausible-events-ch plausible docker volume
gitea-data gitea docker volume
gitea-db gitea → /mnt/ssd/gitea-db
ghost-content ghost docker volume
ghost-mysql ghost docker volume
$ neo volumes mount gitea-db /mnt/ssd/gitea-db ? Mount gitea-db to /mnt/ssd/gitea-db? This will briefly stop gitea. Yes, proceed
✓ Data copied
✓ gitea-db mounted to /mnt/ssd/gitea-db
$ neo backup plausible ✓ Backup created: /var/backups/neo/plausible-20260318-143200.tar.gz (2.3G)
? Download backup to local machine? No
→ Copy manually:
→ scp root@159.65.100.42:/var/backups/neo/plausible-20260318-143200.tar.gz .
$ neo restore plausible /var/backups/neo/plausible-20260318-143200.tar.gz ? Restore plausible from backup? This will overwrite current data. Yes, restore
✓ plausible restored successfully
$ neo ssh
# → Opens SSH session to root@159.65.100.42
$ neo ssh --server staging
# → Opens SSH session to root@staging.mysite.com| Command | Description |
|---|---|
| Dashboard | |
neo |
Interactive TUI — servers, apps, deploy, install |
| Setup | |
neo init <user@host> |
Bootstrap a server (Docker, Caddy, state) |
neo init <user@host> --name staging |
Name the server |
| Apps | |
neo install |
Pick a template and scaffold it into a folder |
neo install <app> |
Scaffold a specific template |
neo deploy [path] |
Deploy local Dockerfile project |
neo deploy --domain <d> --port <p> |
Deploy with domain and port |
neo list |
List all apps on current server |
neo start <app> |
Start a stopped app |
neo stop <app> |
Stop a running app |
neo restart <app> |
Restart an app |
neo update <app> |
Pull latest image and redeploy |
neo remove <app> |
Remove app (keeps data volumes) |
neo destroy |
Tear down everything on a server |
neo prune |
Remove unused images/containers/volumes |
neo status |
Show app/service status |
neo dev [down] |
Run the current project locally via Docker |
| Logs & Domain | |
neo logs <app> |
Show last 100 log lines |
neo logs <app> -f |
Stream logs in real-time |
neo logs <app> --tail 50 |
Custom tail count |
neo domain <app> <domain> |
Set/change domain (auto-SSL via Caddy) |
neo redirect add <from> <to> |
Redirect a domain without deploying an app |
neo deploys <app> |
Deployment history — build, commit, who, when |
| Proxy (Caddy) | |
neo caddy routes |
Show the routes Caddy is actually serving, incl. basic-auth state |
neo caddy reload [--app <app>] |
Rewrite routes from server state (fixes drift between proxy and state) |
neo caddy update |
Pull the latest Caddy image and recreate the proxy |
| Env & Config | |
neo env <app> |
List/set/unset/import env vars |
neo config init |
Scaffold a new .neo.yml |
neo config generate |
Generate .neo.yml from docker-compose.yml |
neo sync [app] |
Sync server state back to .neo.yml |
| Services & Data | |
neo service create/list/link/unlink/remove |
Shared MySQL/Postgres/Redis/MariaDB instances |
neo db <app> [shell] |
Interactive database browser or raw shell |
neo volumes |
List all volumes on server |
neo volumes mount <vol> <path> |
Mount volume to host path (e.g., external SSD) |
neo backup <app> |
Backup app data volumes |
neo restore <app> <file> |
Restore from backup |
| Security | |
neo firewall install/status/block/unblock/list |
CrowdSec intrusion prevention |
neo stealth |
Hide server from IP-based discovery |
neo key add |
Authorize an SSH key on a server |
| Servers | |
neo servers |
List all configured servers |
neo use <name> |
Switch active server |
neo ssh |
SSH into current server |
neo run <app> -- <cmd> |
Run a command inside an app container (-w worker, -i interactive) |
neo tunnel <app> |
Forward a remote port to your machine |
| Licensing & Meta | |
neo activate [key] |
Activate neo (free, required before use) |
neo license status/deactivate |
License management |
neo ask |
Interactive skill assistant |
neo version |
Show version, check for updates |
neo upgrade |
Self-update the binary |
| Flag | Description |
|---|---|
--server <name> |
Target a specific server for this command |
--help |
Show help for any command |
10 production-ready templates. neo install <app> scaffolds each into a folder — a docker-compose.yml (app + bundled databases), a .neo.yml, and a .env with generated secrets — that you then ship with neo deploy:
| App | Category | Bundled Services |
|---|---|---|
| Plausible | Analytics | PostgreSQL, ClickHouse |
| Umami | Analytics | PostgreSQL |
| Ghost | Blogging | MySQL |
| WordPress | CMS | MySQL |
| Gitea | Dev Tools | PostgreSQL |
| n8n | Automation | PostgreSQL |
| Uptime Kuma | Monitoring | — (SQLite built-in) |
| Miniflux | Reading | PostgreSQL |
| Vaultwarden | Security | — (SQLite built-in) |
| Chatwoot | Support | PostgreSQL, Redis |
Each template auto-generates secrets into .env, wires the app to its bundled databases in docker-compose.yml, and sets a health check in .neo.yml. Edit any of it before you deploy.
Neo is a local CLI — no daemon, no agent. Every operation runs over SSH:
neo install ghost # local — scaffold only
↓ writes ./ghost/{docker-compose.yml, .neo.yml, .env}
cd ghost && neo deploy
↓ (local — Charm TUI)
build/pull image, read .neo.yml
↓ (SSH to remote server)
docker run -d --name app-ghost --network neo ...
curl -X POST localhost:2019/config/... (Caddy route)
↓ (local — Charm TUI)
✓ Ghost is live at https://blog.mysite.com
- SSH Executor: Persistent connection, key-based auth (ssh-agent, ed25519, rsa)
- Docker: All container management via
dockerCLI over SSH - Caddy: Reverse proxy with optional auto-SSL via Admin API (port 2019, localhost-only on server)
- Config:
~/.neo/config.json— multi-server local config - State:
/etc/neo/state.json— per-server remote state
Neo deploys apps as HTTP-only by default. This lets you verify the app is running before dealing with DNS and SSL certificates.
Once your DNS A record points to the server, enable HTTPS from the dashboard:
neo # open dashboard
→ select app → Enable HTTPS
Caddy then automatically provisions a Let's Encrypt certificate.
Laravel / frameworks behind a reverse proxy: when HTTPS is enabled, your app receives requests over HTTP from Caddy internally. Add
$middleware->trustProxies(at: '*')inbootstrap/app.phpso Laravel reads theX-Forwarded-Proto: httpsheader and generates correct asset URLs.// bootstrap/app.php ->withMiddleware(function (Middleware $middleware): void { $middleware->trustProxies(at: '*'); })
Every deploy records the commit it built and passes it into the container as environment variables:
| Variable | Example |
|---|---|
NEO_GIT_COMMIT |
a1b2c3d4e5f6… (full SHA) |
NEO_GIT_SHORT_COMMIT |
a1b2c3d |
NEO_GIT_BRANCH |
main |
NEO_GIT_TAG |
v1.4.2 — only when the commit itself is tagged |
NEO_DEPLOYMENT_ID |
20261009-150212-a1b2c3d |
NEO_DEPLOYED_AT |
2026-10-09T07:02:12Z |
Reference them from .neo.yml like any other variable — SENTRY_RELEASE: "${NEO_GIT_COMMIT}" — and check them with neo status <app>, neo deploys <app>, or neo run <app> -- printenv NEO_GIT_COMMIT.
Each deploy writes the commit it just built. A value you set yourself in .neo.yml env:, an env file, or --env wins over the injected one. neo deploy --env-only restarts the same image, so it keeps the previous build's values.
Before v0.26.12 a redeploy kept the first deploy's values: they were saved in server state and never overwritten, so
neo statusshowed the new commit whileprintenvin the container showed the old one. The workaround —neo env unset <app> NEO_GIT_COMMIT NEO_GIT_SHORT_COMMIT NEO_GIT_BRANCH NEO_GIT_TAG NEO_DEPLOYMENT_ID NEO_DEPLOYED_ATbefore each deploy — is no longer needed. The next deploy replaces the stale values.
Neo changes Caddy routes through Caddy's admin API, and every change — on any app — makes Caddy load a whole new config. That happens on every neo deploy, neo domain, neo caddy reload, and redirect change.
By default, Caddy closes every open WebSocket the moment its config is replaced. On a shared server that meant deploying one app disconnected the WebSocket clients of every app — browsers, realtime dashboards, IoT devices such as OCPP chargers.
Since v0.26.13, every route Neo writes sets Caddy's stream_close_delay to 24h. Connections that are open when the config changes keep running; new connections use the new config straight away.
What this does and doesn't cover:
- Covered: any WebSocket or other upgraded HTTP connection that passes through
neo-caddy(ports 80/443), whatever runs behind it — including a second proxy that forwards to a message broker. - Delayed, not removed: Caddy has to release old configs eventually, so a connection that is still open 24 hours after a config change is closed then. Clients should reconnect, as they would after any network blip.
- Not covered: redeploying the app that holds the connection. Neo replaces that container, so its clients disconnect and reconnect. Nor does Caddy ever see traffic that doesn't pass through it, such as AMQP between two containers on the
neonetwork. - Upgrading: routes pick up the setting the next time they are written. After
neo upgrade, runneo caddy reloadonce at a quiet time — that one run still drops current connections, because the routes it replaces don't have the setting yet.
By default a redeploy is blue-green: Neo starts app-<name>-next next to the running container, switches traffic once it's healthy, then removes the old one. If the new version fails, the old one keeps serving. That's right for web apps, but wrong for anything that owns its data directory:
- Two copies share one volume. For a few seconds the old and new container both run on the same volume — two brokers or databases using one data folder at once.
- The hostname changes. Docker gives each new container a random hostname, and some images keep their data under it. RabbitMQ names its node
rabbit@<hostname>and stores data per node, so every redeploy came up as a fresh, empty node: vhosts, users, durable queues and messages seemed to vanish. The old data was still on the volume, under the old node name. - The old container is killed. It was removed with
docker rm -f, with no chance to flush to disk.
Set strategy: recreate for these apps:
# .neo.yml for a RabbitMQ app
name: e-hub-rabbitmq
port: 15672
strategy: recreate # stop the old container, then start the new one
# hostname: rabbitmq # optional — defaults to the app name under recreate
volumes:
data: /var/lib/rabbitmqWith strategy: recreate, Neo:
- stops the old container first, giving it up to 60 seconds to shut down cleanly, then starts the new one under the same name — never two at once;
- gives the container a fixed hostname (the app name, or
hostname:), so RabbitMQ keeps the same node name and data; - keeps that hostname on every path that recreates the container:
neo deploy,--env-only,neo env set/unset/import,neo update,neo volumes mount.
The trade-off: the app is down while the new container starts, and if it fails there is no old version to fall back to. Neo then says so, exits non-zero, and keeps the failed container so neo logs <app> works. Fix and redeploy. strategy: recreate can't be combined with scale:, and both strategy: and hostname: can be set per environment.
Switching an existing RabbitMQ app to
strategy: recreate. The first deploy with it changes the node name once, torabbit@<app-name>, so it starts empty. Do it before the data matters, or export definitions first (rabbitmqctl export_definitions) and import them after. If you already setRABBITMQ_NODENAME, the node name doesn't change.
Don't run stateful services as sidecars. A deploy recreates every sidecar in
.neo.yml, even unchanged ones, so a broker or database sidecar restarts on every app deploy. Run it as its own app withstrategy: recreate(a separate.neo.yml), or as a shared service (neo service create).
$ neo init root@159.65.100.42 # default → "production"
$ neo init root@staging.mysite.com --name staging
$ neo use staging
✓ Switched to server "staging"
$ neo deploy --server production # or target per-commandNeo supports these Linux distributions on your remote server:
| OS | Minimum Version |
|---|---|
| Ubuntu | 24.04+ |
| Debian | any |
| Fedora | 39+ |
| CentOS / RHEL | 9+ |
| AlmaLinux | 9+ |
| Rocky Linux | 9+ |
Package manager is auto-detected: apt for Debian/Ubuntu, dnf for RPM-based distros.
make build # Dockerized build → bin/neo
make build-all # Dockerized cross-compile → dist/
make image-build # Build runtime container image → vxero/neo:latest
make test # go test ./...
make fmt # gofmt -w .Test neo against 13 Linux distros without a real VPS. Each distro runs in a Docker container with SSH + Docker-in-Docker:
make sandbox # test all distros
make sandbox-supported # supported distros only (full test suite)
make sandbox-unsupported # unsupported distros only (rejection test)
make sandbox-distro DISTRO=debian-12 # single distro
make sandbox-list # show distro matrix
make sandbox-keep # keep containers alive after tests
make sandbox-down # tear down everythingSupported distros run a full 9-phase integration test: server init, template install (Uptime Kuma), app lifecycle (start/stop/restart/logs), env vars, domain routing, volumes, update/remove, and a Docker build+deploy cycle.
Unsupported distros verify that neo init correctly rejects them.
For production-like testing with real networking and SSL:
make build-neotest
./bin/neotest --token $DIGITALOCEAN_TOKENneo/
├── cmd/
│ ├── neo/main.go # CLI entry point
│ ├── neo-bridge/ # Sidecar used by Neo Desktop (see below)
│ ├── neotest/ # DigitalOcean integration test runner
│ └── neosandbox/ # Docker sandbox test runner
├── commands/ # ~40 command files → 40 top-level commands
│ ├── root.go # Root command + shared helpers
│ ├── dashboard.go # Interactive TUI (main menu, servers, apps)
│ ├── init.go # Server bootstrap
│ ├── install.go # App wizard
│ ├── deploy.go # Deploy local projects
│ ├── list.go / status.go # App table / status
│ ├── servers.go # Multi-server + use
│ ├── manage.go # start/stop/restart/remove/update
│ ├── logs.go # Container log streaming
│ ├── domain.go / redirect.go # Caddy route + redirect management
│ ├── env.go / envfile.go # Env var management
│ ├── service.go # Shared MySQL/Postgres/Redis/MariaDB
│ ├── volumes.go # Volume list + mount
│ ├── backup.go # Backup + restore
│ ├── firewall.go / stealth.go # CrowdSec + discovery hiding
│ ├── db.go / dev.go / sync.go # DB browser, local dev, state→yaml sync
│ ├── key.go / tunnel.go # SSH key auth, port forwarding
│ ├── license.go # Activation + license management
│ ├── ssh_cmd.go # Quick SSH
│ ├── exec_unix.go # syscall.Exec (unix)
│ └── exec_windows.go # os/exec fallback (windows)
├── internal/
│ ├── ssh/executor.go # Persistent SSH client
│ ├── remote/
│ │ ├── docker.go # Docker commands over SSH
│ │ ├── caddy.go # Caddy Admin API over SSH
│ │ └── crowdsec.go # CrowdSec firewall over SSH
│ ├── config/config.go # ~/.neo/config.json
│ ├── state/state.go # /etc/neo/state.json (remote)
│ ├── license/ # License API client, offline cache
│ ├── sandbox/ # Docker sandbox test matrix + runner
│ ├── testinfra/ # DigitalOcean integration test infra
│ ├── app/
│ │ ├── manifest.go # YAML manifest parser
│ │ ├── registry.go # Embedded template registry
│ │ ├── generate.go # Secret value generator
│ │ └── templates/*.yml # 10 app manifests
│ ├── ui/
│ │ ├── banner.go # ASCII logo
│ │ ├── cards.go # Box cards
│ │ ├── spinner.go # Braille spinner
│ │ ├── progress.go # Progress bar + status bullets
│ │ └── styles.go # Lipgloss styles
│ └── bridge/ # Vxero SaaS migration API — retained,
│ # not wired to any command
├── apps/desktop/ # Neo Desktop (Tauri GUI) — experimental
├── Makefile
├── go.mod / go.sum
└── .gitignore
| Package | Version | Purpose |
|---|---|---|
spf13/cobra |
v1.8.1 | CLI framework |
charmbracelet/huh |
v1.0.0 | Interactive prompts |
charmbracelet/lipgloss |
v1.1.0 | Terminal styling |
charmbracelet/x/term |
v0.2.1 | Terminal size/state helpers |
golang.org/x/crypto/ssh |
v0.31.0 | SSH client |
gopkg.in/yaml.v3 |
v3.0.1 | YAML parsing |
Vxero Neo is source-available software, licensed under the Elastic License 2.0 (ELv2). The source is public — read it, audit it, self-host it, modify it for your own use, and contribute back — see CONTRIBUTING.md. Security issues go through private reporting, never a public issue.
Neo is free, but activation is required — every user activates a license
key before use (neo activate). There is no paid tier today; all features are
unlocked for any valid license.
Under ELv2 you may not:
- provide Neo to third parties as a hosted or managed service;
- move, change, disable, or circumvent the license-key functionality;
- remove or obscure the licensing, copyright, or other notices.
Note: "source-available" is not the same as "open source" (OSI). The code is public, but usage is restricted by the terms above.
Copyright © 2026 Solution Forest Limited. All rights reserved.