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.
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