Understanding .neo.yml: The File That Actually Describes Your Deployment

It's the single most important file in a Neo project, and the most commonly misunderstood — here's the mental model, not just the field list.

7 min read

Two files, two different jobs

The most common confusion is treating Dockerfile and .neo.yml as overlapping. They're not — they answer completely different questions:

  • Dockerfile — how do I build this app into an image? (dependencies, compiled assets, the runtime)
  • .neo.yml — how do I deploy that image? (which domain, which secrets, how many replicas, what runs before traffic switches)

A Dockerfile with no .neo.yml still deploys — Neo infers the port from EXPOSE and prompts for a domain. That works for a five-minute test. It stops working the moment you need a second environment, a background worker, or a secret that shouldn't live in plaintext — none of that has anywhere to go without the file.

Why "it just works without one" is a trap, not a feature

Deploying with zero config is real, and genuinely useful for a first try. But everything it configures lives only in /etc/neo/state.json on the server — not in your repository. That means:

  • It can't be reviewed in a pull request, because it was never a diff
  • It can't be diffed against last week, because there's no history
  • It's gone if the server is rebuilt, because nothing described it anywhere else

Committing a .neo.yml — even a three-line one — turns "however the server happens to be configured right now" into "whatever this file, checked into git, says." That's the entire value of the file: it makes a deployment a fact you can look at, not a fact you have to go ask the server for.

neo config init

Three lines is a completely valid .neo.yml. It's not a placeholder you're expected to fill in immediately — it's already doing its job.

It grows with the project, not before it

You don't need to learn the full schema to start. A realistic progression:

Day one:

name: shop
domain: shop.example.com

Week two, you add a queue:

workers:
  queue:
    command: php artisan queue:work

Month two, you add staging:

environments:
  staging:
    server: staging-box
    domain: staging.shop.example.com

Each addition is independent — adding workers: doesn't require understanding environments:, and neither requires understanding release: or env_encrypted:. You reach for a field when you have the problem it solves, not before.

The mistake that costs the most debugging time

Once environments: exists, root-level domain: and server: are ignored entirely — deploy uses each environment's own values instead. Neo detects this and refuses to run rather than silently ignoring your top-level fields:

✗ root-level domain:/domains: is ignored when environments: are defined
  → Move domains: into each environment block

This is caught before anything builds, specifically because "I set the domain but it didn't apply" is a genuinely confusing thing to debug without an explicit error pointing at the cause.

The one-sentence version

.neo.yml is not configuration overhead — it's the answer to "what exactly is deployed, and why," written down somewhere other than your memory of the last time you ran a command. For the complete field-by-field reference once you understand the shape of it, see A Complete .neo.yml Reference Walkthrough.

Try it yourself — Neo is free, with every feature unlocked.

Get started free