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.
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 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 toproduction--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 avelastack.appname--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 to300;0gives 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 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 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 elsewhere, or see 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 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. The deploy says when either happens. vela 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, 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 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:
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:
$ vela env set STRIPE_SECRET_KEY
$ vela env import .env.production
$ vela env list After deploying
vela status- Show what is deployedvela logs- Tail the logs of a deployed appvela rollback- Put the previous release backvela destroy deployment- Remove a deployed environment
Deploy history
A linked 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 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 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.
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: 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.
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 shows how to skip them.