# Deploy

Deploy the application to a previously provisioned server. Any SvelteKit project deploys, with or without a PocketBase backend.

### Syntax

```
$ vela deploy
```

```
$ vela deploy --server root@example.com --domain example.com
```

Deploy takes no positional arguments. It builds the app locally, uploads the release over SSH, runs any pending migrations against the server's database, restarts the services and health-checks the result. A release that fails its health check — no answer, a 5xx, or a 404 on the health path — is rolled back automatically, migrations included. A redirect or a 401 counts as healthy: something is answering.

### Targets

A **target** names a copy of the app, never a machine. `production` is the default; any name you choose, such as `staging`, is a separate copy on the same or a different server. `local` is the project on your own computer. `preview:<branch>` is a copy of one branch, removed when the branch is done — see [Previews](/deploy/previews).

The server behind a target is recorded the first time you name it, in `.vela/project.json`, so it isn't retyped on every command:

```
$ vela deploy --server root@example.com --domain example.com
$ vela deploy                                     # same server, remembered
$ vela deploy -t staging --server root@staging.example.com --domain staging.example.com
$ vela deploy -t staging                          # staging, remembered
```

Commit `.vela/project.json`. It carries the permanent app id that the server keys every release, env file and database on — a fresh id would land as a brand new app and orphan the old one. Use [`vela targets`](/targets) to list what a project is bound to.

A `--server` that differs from the recorded one would move the target to another machine, so a terminal asks first. In CI, or anywhere without a terminal, it's an error instead: drop `--server` to use the recorded server, or move the target on purpose from a terminal or by editing `.vela/project.json`. Whatever ran on the old server is left there.

### Options

- `-t, --target <target>` - Which copy of the app to act on, defaults to `production`
- `--server <ssh>` - Server this target runs on, recorded on first use
- `--domain <hosts>` - Hostname(s) to serve on, comma separated. Optional for a linked project, which gets a `velastack.app` name
- `--project <name>` - Override the project name
- `--health-path <path>` - Path the health check requests
- `--keep <count>` - How many old releases to keep on the server
- `--pb-version <version>` - PocketBase version to run
- `--no-build` - Deploy the existing build output without rebuilding
- `--lock-wait <seconds>` - How long to wait for another deploy, rollback or restore of the same target to finish before giving up, defaults to `300`; `0` gives up at once
- `--remote-db` / `--no-remote-db` - Render the build against the database on the server, over an SSH tunnel. On by default once the target has been deployed to with a backend.
- `-i, --identity <file>` - SSH private key to authenticate with
- `--ssh-port <port>` - SSH port
- `--accept-host-keys` - Trust an unknown host key on first connect (CI)

### Hostnames

A target is served on the hostname you give it with `--domain`, once DNS for that name points at the server. A project that is [linked](/link) to velastack.dev and deploys while logged in doesn't need one: its first deploy claims `<project>.velastack.app`, named targets get `<project>--staging.velastack.app`, and previews `<project>--<branch>.velastack.app`. The deploy prints the URL, and a new name goes live within a minute.

Both can be combined. With a domain of your own, that domain is primary and the managed name redirects to it. See [Domains](/deploy/domains) for how names are chosen, how your own domains are attached and what the server needs.

### Adapter

`vela deploy` runs the app as a Node server, so the project has to build with `@sveltejs/adapter-node`. New projects from the default template already do. A project on `@sveltejs/adapter-auto`, which is what `sv create` gives you, or with no adapter at all, is switched on its first deploy: the config and `package.json` are rewritten and the package is installed. At a terminal the deploy asks first, naming the files it will edit; answer no and nothing is changed or deployed. In CI, or anywhere without a terminal, it switches without asking. Commit the files the deploy lists, lockfile included, so every deploy builds the same way.

A project on `@sveltejs/adapter-static` or a hosting platform's adapter has chosen something else. The deploy leaves it alone and stops before building, showing the lines to change. Host a [static site](/static) elsewhere, or see [Serverless](/serverless) for the platforms.

### Backend

The deploy checks the project for a backend every time rather than reading a setting: a `data/` directory together with the PocketBase client in `package.json` (`pocketbase-sveltekit` or `@velastack/pocketbase`) means PocketBase. A project without them deploys as the app alone.

Add a backend with [`vela enable backend`](/enable/backend) and deploy again, and the same target gains PocketBase. Deploy after removing it and PocketBase is stopped; its database stays on the server until [`vela destroy deployment --purge`](/destroy/deployment). The deploy says when either happens. [`vela disable backend`](/disable/backend) also switches the adapter to static, so switch it back to `@sveltejs/adapter-node` before deploying again.

### Dependencies

The server installs the release's production dependencies with npm: `npm ci` when the project has a `package-lock.json`, `npm install` otherwise. The installed tree is cached on the server and keyed on the lockfile, so a deploy that changes no dependencies skips the install.

A `pnpm-lock.yaml`, `yarn.lock`, `bun.lock` or `bun.lockb` is uploaded too and is part of that key, so a change to it triggers a reinstall. npm cannot replay those lockfiles, though: versions are resolved from the ranges in `package.json` and may differ from yours, and the deploy warns about it. Commit a `package-lock.json` for a reproducible deploy:

```
$ npm install --package-lock-only
```

The project's own `prepare` script is not run on the server. It is a development hook, and the tools it calls are dev dependencies, which are not installed there. Install scripts of the dependencies themselves still run.

### Prerendered pages

A prerendered page is rendered once, at build time, against whatever database the build can see. On a clean checkout that is an empty throwaway database, so those pages come out full of defaults.

Deploys render the build against the database being deployed to, tunnelled over the same SSH connection, as soon as there is one to render against. The build only ever reads, and the superuser credentials come off the server itself. Pass `--no-remote-db` to use a throwaway local database instead. The first deploy of a target, and the first after adding a backend, do that anyway, since there is no database on the server yet.

The app's name and URL in those pages don't come from the database at all: both are read from [`src/lib/site.ts`](/cli/project-structure), so canonical links and Open Graph image URLs are right on any build once `url` there is set. The origin SvelteKit gives those pages as `url.origin` is separate: prerendering has no request to take one from, so `vela build` sets it from the target's domain. Without a domain configured, it is SvelteKit's placeholder host and the build says so.

### App name and URL

The deploy warns when `url` in `src/lib/site.ts` is still a localhost address or doesn't match the domain it is deploying to.

With a backend, every deploy also sets the deployed PocketBase's application name to `site.name`, which is what its verification and password reset emails use. A name changed in the deployed [admin panel](/admin) only lasts until the next deploy. The first deploy of a target copies the sender name and address from your local database too, and sets PocketBase's app URL to the deployed domain.

### Two deploys at once

One deploy runs per target at a time. A second one — CI and a laptop, or two people — waits for the first to finish, up to `--lock-wait` seconds, then gives up with a message naming what it was waiting on. Rollbacks and restores take the same lock, so none of them can run under a deploy.

Release ids are stamped from the server's clock, so every machine agrees on which release is newer. A deploy that finishes waiting only to find a newer release already live is dropped rather than put in front of it: its upload is removed and the command fails, and what is live is not touched. Deploy again if that is not what you wanted. A release id looks like `20260910T141203Z-a3f9`.

### Configuration

Defaults can be committed to `velastack.config.ts` at the project root, so they don't have to be passed every time:

```ts
export default {
    project: 'my-app',
    deploy: {
        domain: 'example.com',
        healthCheckPath: '/',
        keepReleases: 5,
        buildAgainstRemote: false
    }
};
```

Every field is optional, and `velastack.config.js`, `.mjs` and `.json` are read too. Command line options win over the config file, which in turn wins over what was recorded on the last deploy.

### Environment variables

Production environment variables live on the server, not in the release. A deploy never reads, uploads or overwrites them. Manage them with [`vela env`](/env):

```
$ vela env set STRIPE_SECRET_KEY
$ vela env import .env.production
$ vela env list
```

### After deploying

- [`vela status`](/status) - Show what is deployed
- [`vela logs`](/logs) - Tail the logs of a deployed app
- [`vela rollback`](/rollback) - Put the previous release back
- [`vela destroy deployment`](/destroy/deployment) - Remove a deployed environment

### Deploy history

A [linked](/link) project records every deploy on velastack.dev: who deployed what, from which commit, to which hostname, and whether it succeeded. This needs an API key on the machine deploying — [`vela login`](/account/login) stores one, and `VELA_API_KEY` in the environment is what CI uses. A linked project without a key warns and deploys anyway; nothing about the deploy itself depends on velastack.dev being reachable.

Recording is also what registers the server and issues the `velastack.app` hostnames above. An unlinked project never touches the network beyond SSH.

### GitHub Action

Use [`velastack/action`](/helpers/github-action) to deploy from a GitHub workflow. It builds on the runner and hands the release to `vela deploy` over SSH, so a deploy from CI and a deploy from your laptop do the same thing. The action manages the SSH key itself.

```yaml
name: Deploy

on:
  push:
    branches: [main]

concurrency:
  group: deploy-prod
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: velastack/action@v1
        with:
          server: root@your-server
          ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
          domain: example.com
```

Provision the server and deploy once from your machine first — that first deploy is what mints the app id. Use a `concurrency` group so two pushes queue rather than race; the server serializes deploys of one target as well, so nothing is lost if two do overlap.

### Pull request previews

Several copies of the app can run on one server, each on its own target and internally on its own port. The action uses that to give every pull request a [preview](/deploy/previews): listen for pull request events, add `pull-requests: write` for the comment it keeps up to date, and pass an API key so the preview gets its `velastack.app` hostname.

```yaml
on:
  push:
    branches: [main]
  pull_request:
    types: [opened, synchronize, reopened, closed]

permissions:
  contents: read
  pull-requests: write

concurrency:
  group: deploy-${{ github.head_ref || github.ref_name }}
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: velastack/action@v1
        with:
          server: root@your-server
          ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
          api-key: ${{ secrets.VELA_API_KEY }}
          domain: example.com
```

A push to `main` deploys production. A pull request deploys `preview:<branch>` at `<project>--<branch>.velastack.app`, and closing it removes the preview again, data included. Pull requests from Dependabot or from forks carry no secrets; the [action page](/helpers/github-action) shows how to skip them.