Quick Start

Zero to live in
four commands.

No Kubernetes. No platform lock-in. Point neo at a server, run four commands, ship.
Any language, any Dockerfile — your VPS, your data.

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

A Dockerfile and a .neo.yml.

Neo can deploy from a Dockerfile alone — but then your domain, port and env live only on the server. Commit a .neo.yml and the deployment is described in the repo, reviewable and repeatable.

Your project structure

my-app/
├── Dockerfile         ← you already have this
├── .neo.yml           ← start here — neo config init
├── src/
│   └── ...
└── .env.production    ← optional, loaded by neo.yml

Without a .neo.yml, neo falls back to your Dockerfile’s EXPOSE and the directory name — fine for a first deploy.
Everything past that — environments, workers, release commands, encrypted env, basic auth — is declared here. Run neo config init to scaffold one.

Minimal .neo.yml

# That's all you need.
name: my-app
port: 3000

Port is auto-detected from EXPOSE in your Dockerfile — so you can skip port: entirely too.

# Add volumes so storage survives redeploys.
name: my-laravel-app

volumes:
  storage: /var/www/html/storage

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

dev:
  env_file: .env              # loads APP_KEY, secrets

environments:
  production:
    env_file: .env.production

Uses serversideup/php image. EXPOSE 8080 in Dockerfile — neo detects port automatically. Run neo dev locally, neo deploy --to production for prod.

# Rails: persist uploads and the sqlite db.
name: my-rails-app
port: 3000
env_file: .env.production

volumes:
  storage: /rails/storage

Rails 7+ uses RAILS_MASTER_KEY for credentials. Set it via neo env set my-rails-app RAILS_MASTER_KEY=... after first deploy.

# Django: media files + static assets.
name: my-django-app
port: 8000
env_file: .env.production

volumes:
  media: /app/media
  static: /app/staticfiles

Run python manage.py collectstatic and migrate in your Dockerfile CMD or as a startup script.

# .NET: standard port 8080 in dotnet/aspnet images.
name: my-dotnet-app
port: 8080
env_file: .env.production

env:
  ASPNETCORE_ENVIRONMENT: Production
  ASPNETCORE_URLS: http://+:8080

The mcr.microsoft.com/dotnet/aspnet base image defaults to port 8080 in .NET 8+. Set ASPNETCORE_URLS to match.

# WordPress: persist uploads + MySQL data.
name: my-wordpress
port: 80
env_file: .env.production

volumes:
  uploads: /var/www/html/wp-content/uploads

Use neo service create mysql my-db to provision a shared MySQL, then neo service link my-db my-wordpress to inject DB_* env vars automatically.

Deploy

Four steps. That's the whole thing.

Run these once per project. Redeploys after code changes are just neo deploy.

1

Install neo

One command. Supports macOS, Linux, and Windows.

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

Initialize your server

Point neo at a fresh Ubuntu, Debian, Fedora, or RHEL-family (CentOS / AlmaLinux / Rocky) VPS. Docker, Caddy, and the neo network are set up automatically.

$ neo init root@your-server-ip
✓ Connected (Ubuntu 24.04, 4GB RAM, 2 CPU)
✓ Docker 27.1 installed
✓ Caddy 2.9 running (ports 80, 443)
✓ Server ready
3

Add .neo.yml to your project

Two lines max. Or skip it entirely — neo reads EXPOSE from your Dockerfile and uses the directory name.

name: my-app
port: 3000
4

Deploy

neo builds your image, transfers it over SSH, starts the container, passes the health check, and provisions SSL — all in one command.

$ neo deploy
Building image locally...
Transferring to server...
Starting container...
Health check passed
SSL certificate issued
✓ my-app is live
https://my-app.your-ip-address.sslip.io

After your first deploy, redeploys are just neo deploy — zero downtime, same four seconds.

Configuration

Full .neo.yml by framework.

Every field annotated. Copy, paste, remove what you don't need. Every field is optional.

name: my-node-app              # container name  (default: directory name)
domain: api.example.com        # SSL auto-provisioned via Let's Encrypt + Caddy
port: 3000                    # must match EXPOSE and what your server listens on
env_file: .env.production      # secrets: DB_URL, API keys, JWT_SECRET, etc.

env:                           # non-secret defaults committed to .neo.yml
  NODE_ENV: production
  PORT: "3000"
  LOG_LEVEL: info
  TZ: UTC

volumes:                       # Docker volumes — survive every redeploy
  uploads: /app/uploads         # user-uploaded files
  data: /app/data               # sqlite db or local flat files

workers:                       # same image, different entrypoint command
  cron:
    command: node dist/cron.js  # scheduled jobs runner
  worker:
    command: node dist/worker.js # queue consumer (BullMQ, etc.)

environments:
  production:
    domain: api.example.com
    env_file: .env.production
  staging:
    domain: api-staging.example.com
    env:
      NODE_ENV: staging
      LOG_LEVEL: debug

Never commit secrets to .neo.yml. Put JWT_SECRET, DATABASE_URL, API keys in .env.production (git-ignored) and reference it via env_file.

name: my-laravel-app
domain: myapp.example.com
port: 8080                    # serversideup/php image exposes 8080
env_file: .env.production      # APP_KEY, DB_PASSWORD, MAIL_*, etc.

env:
  APP_ENV: production
  APP_DEBUG: "false"
  LOG_CHANNEL: stderr
  SESSION_DRIVER: database
  CACHE_STORE: database
  QUEUE_CONNECTION: database
  DB_CONNECTION: sqlite
  TRUSTED_PROXIES: "*"          # required — Caddy is a reverse proxy

volumes:
  storage: /var/www/html/storage
  database: /var/www/html/database

workers:
  queue:
    command: php artisan queue:work --tries=3 --max-time=3600 --memory=256
  scheduler:
    command: php artisan schedule:work

environments:
  production:
    domain: myapp.com
    env_file: .env.production
  staging:
    domain: staging.myapp.com
    env:
      APP_DEBUG: "true"

For PostgreSQL/MySQL: neo service create postgres my-dbneo service link my-db my-laravel-app (injects DATABASE_URL automatically).

name: my-rails-app
domain: myapp.example.com
port: 3000
env_file: .env.production

env:
  RAILS_ENV: production
  RAILS_SERVE_STATIC_FILES: "true"
  RAILS_LOG_TO_STDOUT: "true"

volumes:
  storage: /rails/storage

workers:
  sidekiq:
    command: bundle exec sidekiq -C config/sidekiq.yml

Set RAILS_MASTER_KEY after first deploy: neo env set my-rails-app RAILS_MASTER_KEY=your_key_here

name: my-django-app
domain: myapp.example.com
port: 8000
env_file: .env.production

env:
  DJANGO_SETTINGS_MODULE: myapp.settings.production
  PYTHONUNBUFFERED: "1"

volumes:
  media: /app/media
  static: /app/staticfiles

workers:
  celery:
    command: celery -A myapp worker -l info --concurrency=4

Run collectstatic and migrate in your Dockerfile CMD or entrypoint script.

name: my-dotnet-app
domain: myapp.example.com
port: 8080

env:
  ASPNETCORE_ENVIRONMENT: Production
  ASPNETCORE_URLS: http://+:8080

volumes:
  keys: /app/keys

The keys volume is critical — ASP.NET Data Protection keys must survive redeploys or all user sessions are invalidated.

name: my-wordpress
domain: blog.example.com
port: 80

volumes:
  uploads: /var/www/html/wp-content/uploads
  plugins: /var/www/html/wp-content/plugins
  themes: /var/www/html/wp-content/themes

Database: neo service create mysql my-dbneo service link my-db my-wordpress

Full reference in docs →
Commands

Every command Neo provides.

All commands use the same pattern: resolve the server, connect via SSH, operate, and save state.

neo
Launch the interactive TUI dashboard. Browse servers, apps, and actions without memorizing flags.
neo init <host>
Initialize a new server. Validates the OS (Ubuntu 24.04+ or Debian required), installs Docker, starts Caddy, creates the neo network, and saves server state.
neo deploy .
Build from a local Dockerfile, transfer the image via SSH, start the container, and configure routing. Auto-reads env vars from docker-compose.yml, .env, or .neo.yml. Supports --env KEY=VAL, --env-file .env, and --to <env> flags.
neo env <app>
View all environment variables for an app. Secrets are automatically masked in output.
neo env set <app> K=V
Set or update environment variables. The container restarts automatically with the new config.
neo env unset <app> KEY
Remove an environment variable. The container restarts automatically.
neo env import <app> .env
Bulk import variables from a .env file.
neo list
List all running apps on the current server with their status, domains, workers, and ports.
neo logs <app>
Stream live container logs from the remote server. Supports --tail, --follow, and --worker <name> flags.
neo domain <app> <domain>
Assign or update a domain. Caddy provisions the TLS certificate automatically.
neo stop|start|restart <app>
Control the lifecycle of an app and its workers on the remote server.
neo update <app>
Pull the latest image for an app and redeploy it.
neo remove <app>
Stop and remove an app container, its services, and its Caddy routing entry.
neo sync <app>
Sync server state back to .neo.yml. Domain changes, env vars, HTTPS toggles — all captured.
neo servers
List all configured servers. Add, switch, or remove servers from the local config.
neo use <name>
Switch the active server. All subsequent commands target this server unless overridden with --server.
neo ssh
Open an interactive SSH session to the current server.
neo backup <app>
Backup an app's data volumes to a compressed archive. NEO+
neo restore <app> <file>
Restore an app from a backup archive. NEO+
neo volumes
List all Docker volumes on the current server.
neo version
Show the current version and check for updates.
neo upgrade
Self-update neo to the latest version.
Server Requirements
Neo supports Ubuntu 24.04+, Debian, Fedora 39+, and CentOS / RHEL / AlmaLinux / Rocky 9+ on the target server. Older releases and other distros are not supported. neo init will detect your OS and show an error if unsupported.

That's it.

Any server. Any language. Your data stays on your machine.

curl -fsSL https://neo.vxero.dev/neo | sh Copied!
irm https://neo.vxero.dev/neo/windows | iex Copied!
Full docs → Back to landing →