---
title: Web Applications
---

Deploy web applications from a Git repository. ZevCloud supports any language or framework that can run in a container.

## Supported Frameworks

ZevCloud uses [Nixpacks](https://nixpacks.com) to auto-detect and build your application:

| Framework | Auto-Detected | Default Port |
|-----------|---------------|-------------|
| Next.js | ✅ | 3000 |
| React / Vue (Vite) | ✅ | Served as a static site (no port) |
| Express / Node.js | ✅ | 3000 |
| NestJS | ✅ | 3000 |
| FastAPI | ✅ | 8000 |
| Django / Flask | ✅ | 8000 |
| Go | ✅ | 8080 |
| Ruby on Rails | ✅ | 3000 |
| PHP / Laravel | ✅ | 8000 |
| Rust | ✅ | 8080 |
| Static HTML | ✅ | 80 |

## Deploying from GitHub

### Public Repositories

1. Add Service → Web Application → **Public Repository**
2. Paste the repository URL (e.g., `https://github.com/user/repo`)
3. Select the branch
4. Configure and deploy

### Private Repositories

1. Add Service → Web Application → **Import from GitHub**
2. If not connected, click **"Connect GitHub"** to install the ZevCloud GitHub App
3. Select your repository from the list
4. Select the branch
5. Configure and deploy

> **Tip: GitHub Organizations**
>
> You can connect multiple GitHub accounts and organizations. Each installation gives ZevCloud access to the repos you choose.

## Build Configuration

ZevCloud auto-detects most settings, but you can customize:

| Setting | Description | Example |
|---------|-------------|---------|
| **Build Command** | Command to build your app | `npm run build` |
| **Install Command** | Command to install dependencies | `npm ci` |
| **Start Command** | Command to start your app | `npm start` |
| **Output Directory** | For static builds | `dist`, `.next`, `build` |
| **Root Directory** | If your app is in a subdirectory | `packages/web` |
| **Port** | Port your app listens on | `3000` |

These can be set during creation or updated in **Service Settings**. Root and output directories can be written with or without a leading slash: `dist` and `/dist` mean the same folder. Root Directory is a dropdown of the folders in your repository, so you pick the path rather than typing it — a mistyped one fails the build several minutes in, with an error about the build tool rather than the path.

If your repository has its own `Dockerfile`, ZevCloud uses it instead of building your app automatically. See [Using a Dockerfile](/guide/dockerfile).

### Command rules

Install, build and start commands can chain steps with `&&` or `||`, for example `npm ci && npm run build`. They can't contain:

- `;`, a single `|` or a single `&`
- redirects (`>` or `<`)
- backticks or parentheses
- `$VARIABLES` outside single quotes

If a step needs any of these, put the logic in a script (for example a `start` script in `package.json`) and run that script as the command. Saving a command that breaks these rules fails with an error naming the character, such as *"Your start command can't contain ..."*.

Your app still sees every environment variable at runtime. Read them in your code (for example `process.env.PORT`) rather than in the command.

## Static Sites & SPAs

Client-rendered apps (Vite, React, Vue, plain HTML) don't need a server process. Their build produces a folder of static files — the right way to serve them is as a **static site**.

When you create a service from a Vite project, ZevCloud detects this and pre-selects static serving. You can also change it any time in **Service Settings → Build & Deploy**:

| Setting | What it does |
|---------|--------------|
| **Static site** | Builds your app, then serves the output folder as files. No server process, no port to configure. |
| **SPA (Single Page Application)** | Serves `index.html` for unmatched routes, so client-side routing (React Router, Vue Router) works on page refresh and direct links. |
| **Publish Directory** | The folder your build produces. Vite uses `dist`; Create React App uses `build`. |

> **Tip: Which mode do I need?**
>
> If your app runs entirely in the browser and talks to APIs, use **Static site + SPA**. If your app has its own server (Next.js SSR, Express, an API), leave static off and set the Port instead.

Common symptoms and their fix:

- **Deploy succeeds but the site is blank or 404s** — the Publish Directory doesn't match your build output. Check what folder your build command creates.
- **Pages 404 on refresh or direct links** — enable the **SPA** toggle so unmatched routes fall back to `index.html`.
- **Deploy fails with a port or health check error on a Vite/React app** — enable **Static site**. A client-rendered app has no server listening on a port, so port-based deploys can't succeed.

Changes take effect on the next deploy — the dashboard offers a redeploy when you save.

## Monorepos

Create one service per app. How you point each service at its app depends on whether the repository is a **workspace**.

### Independent folders

If each app has its own `package.json` (or `requirements.txt`, `go.mod`) and they don't depend on each other, set each service's **Root Directory** to its folder:

```
repo/
  frontend/    → service 1, Root Directory: frontend
  backend/     → service 2, Root Directory: backend
```

Nothing else to configure. This is the common case.

### Workspaces (pnpm, Yarn, npm, Turborepo, Nx)

If your repository has a `turbo.json`, a `pnpm-workspace.yaml`, an `nx.json`, or a `workspaces` field in the root `package.json`, the apps share dependencies through the repository root. **Pointing the Root Directory at the app folder will fail the install**, because dependencies declared as `workspace:*` can only be resolved from the root.

Leave the Root Directory at `/` and filter the build to one app instead:

```bash
# pnpm + Turborepo
pnpm install --frozen-lockfile
pnpm turbo run build --filter=web...
pnpm --filter web start

# npm workspaces
npm ci
npm run build --workspace=web
npm start --workspace=web

# Yarn workspaces
yarn install
yarn workspace web build
yarn workspace web start
```

The trailing `...` in a Turborepo filter builds that app's own workspace dependencies first. Without it, shared packages the app imports won't be built.

When you select a repository, ZevCloud detects the workspace and offers these commands filled in with your package name — one click applies them.

> **Warning: Workspaces redeploy on every push**
>
> Because the Root Directory is `/`, every push to the branch redeploys the service. That's unavoidable: a change anywhere in a workspace can affect any app in it.

### Auto-deploy is folder-aware

For services with a Root Directory set, pushes only redeploy the services whose folder contains changed files. A commit that only touches `frontend/` won't rebuild your backend. Changes to shared files outside every service folder — a root `package.json`, a shared `packages/` directory — redeploy all services from that repository, since they can affect any build.

## Environment Variables

Set environment variables in the **Variables** tab of your service:

```
DATABASE_URL=postgres://user:pass@host:5432/db
NODE_ENV=production
API_KEY=sk_live_...
```

Variables marked as **secret** are encrypted at rest and hidden in the UI.

## Auto-Deploy on Push

When you push to the configured branch, ZevCloud automatically:

1. Receives a GitHub webhook
2. Clones the latest code
3. Builds using Nixpacks
4. Deploys with zero-downtime rollout
5. Sends an email notification (success or failure)

Disable auto-deploy in **Service Settings → Build & Deploy**.

## Build Logs

View real-time build logs in the **Deployments** tab. Each deployment shows:

- Git commit message and SHA
- Build duration
- Success or failure status
- Detailed build output

[How Deploys Work](/guide/how-deploys-work) explains each build phase, what's cached between deploys, and how to read a failed build back to the step it failed in.

## Resource Limits

Each plan provides different resource limits:

| Plan | Memory | CPU | Build Timeout |
|------|--------|-----|---------------|
| Free | 512 MB | 1 core | 10 min |
| Starter | 2 GB | 1 core | 20 min |
| Pro | 4 GB | 2 cores | 30 min |
| Business | 8 GB | 4 cores | 60 min |

## Persistent volumes (beta)

A deploy builds a fresh container from your code, so anything your app wrote to its own filesystem is gone after the next deploy or restart. That is the right default for code, and the wrong one for data: SQLite files, user uploads, generated reports, a local cache you want to keep.

A **persistent volume** is a folder that survives deploys and restarts. You pick the path inside the container, and whatever your app writes there stays.

**To attach one:** open the service, go to the **Volumes** tab, enter the folder your app writes to (for example `/app/data` or `/app/uploads`) and click **Attach volume**. The service restarts once so the folder is mounted. From then on every deploy and restart finds the same files there.

A few things to know:

- **Paid plans only.** Each plan includes a total allowance in GB across all volumes on one service. The Volumes tab shows how much is used; we email you when a service gets close to its allowance, and the dashboard turns amber at 80%. Nothing is paused when you reach it, but writes fail once the disk is actually full, so give yourself room.
- **Up to three volumes per service.** One for data and one for uploads covers most apps. Mount paths cannot overlap.
- **The mount hides what was there.** An empty volume mounted at `/app/data` covers any `/app/data` that came from the build. Mount at a folder your app creates at runtime, not one that ships in your repository. You cannot mount over `/app` itself or the container's system folders.
- **Removing a volume deletes its files.** There is no undo, and the service restarts afterwards. Download anything you need first (the Terminal tab can `tar` a folder, or your app can export it).
- **One service, one volume.** Volumes are not shared between services. If two services need the same files, use a [storage bucket](/guide/storage) instead.

## Outbound network restrictions

To keep the platform clean for everyone, customer containers are not allowed to make outbound connections on a few specific ports. The host blocks them at the firewall, so any code in your app that tries to reach these ports will time out.

- **SMTP (25, 465, 587, 2525)** — direct mail submission. Use a transactional email API (Resend, Mailgun, Postmark, SendGrid) over HTTPS instead. Their REST/HTTP endpoints are not blocked.
- **IRC (6667, 6697)** — typical command-and-control channels.
- **Mining-pool stratum (3333, 4444, 5555, 7777, 14444)** and **Bitcoin RPC (8332, 8333)**.

Everything else (HTTPS to third-party APIs, your database, Redis, CDNs, webhooks) works normally. If you need to send email, switch to your provider's HTTPS API.

## Rollback

If a deploy causes problems, roll back to an earlier one from the **Deployments** tab.

- **Instant rollback.** Recent deployments whose build is still stored roll back instantly. Nothing is rebuilt: the stored build starts again, usually within seconds.
- **Older deployments.** Older builds may no longer be stored. Apps built with a [Dockerfile](/guide/dockerfile) rebuild the old commit instead, which takes as long as a normal build. Other apps can't roll back that far: push a revert commit to the branch and let it deploy.

## Cancelling a build

A queued or running deployment can be cancelled from the **Deployments** tab. Your app keeps serving its previous version, so cancelling is safe at any point.

## Crash-loop protection

If your app crashes 10 times in a row while starting, ZevCloud stops it automatically and emails the team owner. The dashboard shows why the app was stopped.

To recover, find the cause in the **Logs** tab (it is usually the last few lines before each crash), fix it, and redeploy. A successful redeploy clears the stop.
