Docs / Candor Deploy
Builds and configuration: detection, monorepos, ports
What happens between git push and a live URL — how Candor Deploy detects your stack, builds it with Nixpacks, handles monorepos, ports, and environment variables.
This page explains what happens between git push and a live URL, and every knob you have over it: how your stack is detected, how builds work, how monorepos are configured, what the port contract is, and how environment variables apply. If you haven’t deployed anything yet, start with the Quickstart.
How a push becomes a running app
Every deployment moves through the same four phases, visible in the app’s Deployments tab: queued → building → deploying → live.
- A build is triggered. Three things trigger one: a push to the app’s connected branch (the branch you set at creation, or your repo’s default), the Deploy a commit box in the Deployments tab (paste a commit SHA), or a Rollback of an earlier deployment. Creating an app with a repository connected also kicks off a first build immediately.
- The build runs. We fetch your repository at that exact commit and build a container image from it. Each build gets up to 2 CPU cores, 2 GB of memory, and 10 GB of disk, and has 15 minutes to finish before it’s stopped.
- The rollout happens. The new version starts alongside the old one and must accept connections on its port before traffic switches; only then is the old version retired. A failed new version never replaces a working old one.
The full build output — including exactly why a failed build failed — is kept with each deployment: click any entry in the deployment history to expand it. We keep the last 2,000 lines, which is the end of the log, where failures live. More on reading it in Troubleshooting.
How your app gets built
Nixpacks inspects your code and works out the build for you — Node, Python, Go, Ruby, PHP, and static sites all work without any configuration. Nixpacks’ own output is included in your build log, so when it can’t figure a repo out, you can see why.
A Dockerfile in your repository is ignored unless you ask for it. That surprises people, so here’s the reasoning: most Dockerfiles in most repos are there for local development or CI, not for deploying. They go stale — or never worked in the first place — because nothing ships from them, and the platforms those repos actually deploy to ignore them too. Quietly building yours would mean failing your deploy on code you don’t deploy with.
To build from your own Dockerfile, opt in with a .candorfile in your repository:
services:
web:
builder: dockerfile
That uses the Dockerfile in the directory being built. To name a different one, use dockerfile: — the path is relative to that same directory:
services:
web:
dockerfile: docker/Dockerfile.prod
Naming a dockerfile: opts you in on its own; you don’t need both lines. Either way the build log names the builder it used, so you never have to guess which one you got.
Which Node version you get
If your repo pins one — engines.node in package.json, an .nvmrc, or a .node-version — we use your pin and stay out of the way. If it pins nothing, you get Node 22, the current LTS, and the build log says so. Left alone, Nixpacks picks Node 18, which went end-of-life in April 2025; we fill that silence rather than hand you an unsupported runtime.
What we detect when you create an app
When you create an app from a repository, we read the repo before its first build and configure the service from what’s actually in it. Detection looks at dependency manifests first, and at your docker-compose.yml second — that file is you telling us how you already run the app locally, which beats any guess.
| We find | We conclude |
|---|---|
package.json with next | Next.js app |
package.json with express, fastify, koa, nest, or hono | Node app |
package.json with vite, react-scripts, or @angular/cli | Static site (built, then served) |
package.json with astro | Static site — unless the config names a Node adapter (@astrojs/node, or output: 'server' / 'hybrid'), which makes it a server app |
package.json with a start script | Node app |
pyproject.toml or requirements.txt | Python app (FastAPI, Django, Flask, Litestar, Starlette recognized) |
go.mod / Cargo.toml / composer.json / Gemfile | Go / Rust / PHP / Ruby app |
index.html | Static site |
Detection also spots the databases your code needs — Postgres, MySQL, or Redis drivers in the dependency list, or database images and connection URLs in your compose file — and can create the database and wire its connection string into your app’s environment for you. It uses the variable name your compose file already uses (falling back to DATABASE_URL / REDIS_URL), and it’s driver-aware: a SQLAlchemy project on psycopg 3 gets a postgresql+psycopg:// URL, because a plain one would crash at startup.
Detection never overrides you — it only fills blanks. When two things say something about the same setting, the more explicit one wins, always in this order:
- Your
.candorfile— a file in your repository, read at the exact commit being built. The most explicit thing you can do, and it travels with your code. - What you set in the portal — the fields in the “Deploy an app” form (root directory, and so on).
- What we detect, and our defaults — the stack, the databases, Node 22, port 8080, Nixpacks. The fallback when nothing above spoke.
Your repository’s own package.json start script sits in that bottom tier: it’s the default way Node apps start, so a .candorfile or a portal setting overrides it, and we only ever add a start command when your repo names none (below). One caveat beyond the ladder: repositories with more than 10,000 files are too large to analyze automatically; they build fine, but you configure them by hand.
The start command
An app has to be told how to start. Most repositories say so themselves — a start script in package.json, or a framework whose runtime is unambiguous — and we use exactly that. Two cases where a repository names no start command and we’d otherwise fail the build, so we supply a known-good one and show it in the build log:
| Your repo | Start command we supply |
|---|---|
Astro with @astrojs/node (a server app, no start script) | node ./dist/server/entry.mjs |
Next.js with output: 'standalone' (no start script) | node .next/standalone/server.js |
We only fill the gap: the moment your repo ships its own start script, we step back and use yours — checked fresh on every deploy, so a start we supplied never lingers once you write your own. To run something specific regardless, set it in your .candorfile:
services:
web:
start: node build/index.js
A .candorfile start: is your explicit choice and wins over everything, including a start script in your package.json.
Monorepos: several apps from one repository
One repository can back several services, up to your plan’s app and database limits. You don’t add them one at a time: Deploy reads the whole repository at once and offers every app it finds, together.
- Click Deploy on your dashboard. Pick the project to deploy into — an existing one, or New project named from the repo — then choose the repository and branch.
- Click Detect. We read the repository and work out its shape: a single app, or a monorepo of several apps and the databases they need.
- Review & deploy. Every app we recognized is listed on one screen, pre-filled and ready. Uncheck any you’re not deploying yet; the app marked Public is the one your custom domain attaches to, so move that badge if we guessed wrong. Found an app but couldn’t tell how to start it? That row asks for its start command right there, and Deploy stays disabled until you fill it in. One Deploy click ships them all into that project, wired to each other.
The Quickstart walks through the same screen step by step. From then on, one push to the branch rebuilds and redeploys every app connected to that repository and branch — the whole monorepo ships together.
Adding one more app to a project later doesn’t need the whole scan again: open the project, click + App, pick the same repository, and set its Root directory to the new app’s subdirectory — backend, apps/web, and so on. The name auto-fills from the repo; change it, since it becomes part of the app’s URL.
Details worth knowing:
- The builder is decided inside each app’s directory, so every subdirectory is configured on its own — and a
dockerfile:path in.candorfileis relative to that directory, not to the repository root. - Detection understands the common monorepo shape: it looks for deployables at the repo root, in top-level directories, and one level deep under
apps/,packages/, andservices/. - A Root directory you set by hand — the + App path above — must be a relative path inside the repo: letters, digits,
.,_,-, and/only, no...
The port
The contract is one line: listen on the port in the PORT environment variable. We set PORT in your app’s environment (it defaults to 8080), route HTTPS traffic from your app’s URL to that port, and health-check the app by opening TCP connections to it. An app that binds to some other hard-coded port will build fine and then never come live, because it never answers the health check. Workers skip all of this — they get no port and no URL.
Environment variables
The app’s Environment tab holds its configuration: one KEY=value per line (lines starting with # are ignored). Values are stored as a secret and never shown in logs.
Saving doesn’t restart anything — environment changes apply on the next deploy. Push a new commit, or use Deploy a commit with the same SHA; either restarts the app with the current environment. The portal reminds you with “Redeploy to apply” when you save.
The same tab has Attach a database, which writes a database’s connection URI into your app as DATABASE_URL (REDIS_URL for Redis) in one click — details in Databases on Deploy.
What a rebuild actually does
Every deploy — push, manual, or rollback — is a full build from source: your repository fetched fresh at the exact commit, a new image produced, and a zero-downtime rollout. Nothing is patched in place, and redeploying the same commit is a real deployment, not a no-op — which is precisely what makes it the way to apply environment changes. A rollback is the same machinery pointed at an older commit: it rebuilds that commit from source and rolls it out like any other deploy.
Something building that shouldn’t be failing? Troubleshooting covers the common failures, and we answer questions within one business day.
Something here wrong, unclear, or missing? Tell us — docs get fixed like bugs.