# 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](/docs/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`](/docs/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](/docs/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](/docs/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](/docs/static) elsewhere, or see [Serverless](/docs/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`](/docs/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`](/docs/destroy/deployment). The deploy says when either happens. [`vela disable backend`](/docs/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 build sees the target's environment too, not your `.env`: the target's three [env layers](/docs/env) are read into the build process, and a key set only in `.env` is blanked, so nothing of your development setup ends up in a production bundle. A `$env/static` import of such a key fails the deploy before anything is uploaded.

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`](/docs/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

With a backend, PocketBase names its verification and password reset emails after `APP_NAME`, which every deploy hands over from `site.name` in [`src/lib/site.ts`](/docs/cli/project-structure), and links them to the origin the target is served on. Both show as locked in the deployed [admin panel](/docs/admin); change the name in code, and set `APP_URL` with [`vela env set APP_URL --public`](/docs/env/set) if the emails should link somewhere other than the deployed origin — an app served directly on several hosts has to, since there is no single origin to derive.

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

### Environment variables

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

```
$ vela env set STRIPE_SECRET_KEY -t production
$ vela env set VELASTACK_API_KEY -t all
$ vela env list
```

On an instance's first deploy, the variables [`.env.example`](/docs/cli/environment-variables) documents that the target lacks are listed and, at a terminal, offered for setting before the build. There is no project config file: the domain and server are recorded in `.vela/project.json` on first use, and everything else has a default or a flag.

### After deploying

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

### Deploy history

A [linked](/docs/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`](/docs/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`](/docs/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](/docs/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](/docs/helpers/github-action) shows how to skip them.