---
title: How Deploys Work
description: What happens between a git push and your app being live, what gets cached, and why a build takes the time it does.
---

Most deploy problems are easier to fix once you know which step they happened in. This page walks through what ZevCloud does between your push and your app answering requests.

## The pipeline

```
push ──▶ match ──▶ clone ──▶ detect ──▶ build ──▶ start ──▶ route
```

**1. Match.** A push to your connected repository tells us which services to deploy. Only services on that repository *and* that branch are considered. If several services share one repository, we also compare the changed files against each service's Root Directory, so a commit that only touches `frontend/` doesn't rebuild your API. Changes to shared files outside every service folder rebuild all of them.

**2. Clone.** We fetch that exact commit. If an image for this service already exists for the same commit, we skip straight to the start step — this is why redeploying an unchanged commit is near-instant.

**3. Detect.** We read your **Root Directory** and work out how to build it:

- A `Dockerfile` there means you've already described your build, so we use it as-is. See [Using a Dockerfile](/guide/dockerfile).
- Otherwise we look for a manifest — `package.json`, `requirements.txt`, `pyproject.toml`, `go.mod`, `Gemfile`, `composer.json` — and hand the folder to [Nixpacks](https://nixpacks.com), which works out the language, the system packages it needs, and how to install and start it. Its [provider docs](https://nixpacks.com/docs/providers) list exactly which files each language is detected by, which is the quickest way to check why your project was detected as one thing rather than another.

You can see the resolved plan at the top of any build log, including the detected language and the exact install and start commands.

**4. Build.** The plan runs in three phases:

| Phase | What happens | Example |
|---|---|---|
| **Setup** | System packages your stack needs | Python, Node, a compiler when a dependency requires one |
| **Install** | Your dependencies | `npm ci`, `pip install -r requirements.txt` |
| **Build** | Your build command, if you have one | `npm run build` |

**5. Start.** The new container runs your start command and has to answer on the configured **Port** before it receives traffic.

**6. Route.** Traffic moves to the new container and the previous one stops. Your domain and certificate carry over untouched.

Until step 6, your previous version keeps serving traffic. You can [cancel](/guide/web-apps#cancelling-a-build) a queued or running deploy at any point before then, and your app carries on serving that previous version.

If the build queue is momentarily full when you deploy, the deploy is refused with *"The build queue is full right now. Try again in about N seconds."* Wait that long and deploy again.

> **Warning: Detection reads the Root Directory, not the repository**
>
> If the Root Directory points at a folder that doesn't exist, the clone still succeeds and detection finds nothing — the failure shows up as *"failed to detect the application type"*, which reads like a build-tool problem rather than a wrong path. Pick the folder from the dropdown instead of typing it.

## What gets cached

Each phase is a layer, and layers are reused until something they depend on changes. Your source code is copied in **last**, so a code-only change reuses everything above it.

| Layer | Rebuilt when |
|---|---|
| System packages | Your detected stack changes — a new dependency needs a compiler, you change runtime version |
| Dependencies | Your manifest or lockfile changes |
| Your code | Every commit (this is the cheap part) |

The practical shape of this: **the first build of a project is the slow one.** A first build that takes several minutes is normal, and later builds of the same project are usually well under a minute. A build that's slow *every time* means something is invalidating a layer on each run, and that's worth looking at.

## Why a build is slow

Work through these in order.

### Is it only the first build?

Compare durations in the **Deployments** tab. First build slow, subsequent builds fast is the system working correctly — there's nothing to fix.

### Does a dependency compile from source?

This is the most common cause of a build that's slow every time. Some packages ship only source code, so your build has to install a compiler and build them on every machine that hasn't done it before. Swapping to a prebuilt equivalent is usually a one-line change:

| Slow | Fast | Language |
|---|---|---|
| `psycopg2` | `psycopg2-binary` | Python |
| `mysqlclient` | `pymysql` | Python |
| `bcrypt` | `bcryptjs` | Node |
| `node-sass` | `sass` | Node |
| `sharp` pinned to an old version | current `sharp` | Node |

`psycopg2` is worth calling out: it pulls in a C compiler and PostgreSQL development headers before it can build, which adds several minutes on its own. `psycopg2-binary` is a drop-in replacement that installs in seconds, and both SQLAlchemy and Django accept it without any code change.

When you create a service, ZevCloud flags these in your manifest before you deploy.

### Is your lockfile changing on every commit?

If your lockfile is regenerated or reformatted on each commit, the dependency layer can never be reused. Commit a stable lockfile and leave it alone between dependency changes.

### Are you installing things you don't need at runtime?

Browser automation tools (Puppeteer, Playwright, Cypress) download 100–200 MB of browsers on install. If they're in `devDependencies` and your build doesn't run tests, they're pure cost. ZevCloud sets variables to skip those downloads by default on Node projects.

### Is the build doing work your app doesn't need?

Machine-learning frameworks (`torch`, `tensorflow`) are hundreds of megabytes. That's a real cost you may need to pay, but it's worth checking it's actually imported.

> **Tip: If you need full control**
>
> A [Dockerfile](/guide/dockerfile) lets you decide exactly what's installed and in what order. It's the right answer when your build has requirements the automatic detection can't infer.

## Reading a failed build

Open **Deployments → the failed deploy**. Match the last thing in the log to a phase above:

| Last thing you see | Phase | Usually means |
|---|---|---|
| `failed to detect the application type` | Detect | Root Directory is wrong, or the folder has no recognised manifest |
| A package manager error, `ERESOLVE`, a compiler error | Install | A dependency problem — read the first error, not the last |
| Your own build command failing | Build | Reproduce locally with the same command |
| Build succeeded, deploy failed on health checks | Start | Your app isn't answering on the configured Port |
| Deploy succeeded, every URL returns Bad Gateway | Route | The Port setting doesn't match what your app listens on |

The last two are the same root cause and are covered in detail under [Troubleshooting](/guide/troubleshooting#bad-gateway-or-the-service-shows-running-but-no-endpoint-responds).

## Things that surprise people

**Environment variables apply at build, not at restart.** A variable change reaches your app on the next deploy. Restarting a running container won't pick it up. The dashboard reminds you while you have unapplied changes.

**A restart is not a redeploy.** Restart reuses the existing image. Settings that affect how a container is built or routed — Port, build commands, variables — need a redeploy.

**Build timeouts scale with your plan.** A build that's cut off mid-way is a timeout, not a crash. Limits are listed under [Plans](/guide/plans).

**An app that keeps crashing on start is stopped.** After 10 crashes in a row on start, the app is stopped automatically and the team owner is emailed. The dashboard shows why. Fix the cause (the logs show it) and redeploy, which clears the stop. See [Crash-loop protection](/guide/web-apps#crash-loop-protection).

**Rolling back is usually instant.** Recent deployments whose build is still stored roll back without a rebuild. See [Rollback](/guide/web-apps#rollback).

**The Root Directory is also the auto-deploy filter.** Setting it to a subfolder means pushes that don't touch that folder won't deploy. That's usually what you want, and occasionally surprising when you expect a deploy and don't get one.
