v0.5.1 · MIT · Linux server, macOS or Linux laptop

No-downtime deploys on bare metal.

Buy a server, install Vela, deploy from your laptop over SSH. One binary does both ends. No containers, no orchestrator, no platform bill: the new release takes traffic only after its health check passes.

curl -fsSL https://raw.githubusercontent.com/raskell-io/vela/main/install.sh | bash

Get started View on GitHub

Fig. 1 One deploy

~/my-app
$ vela deploy ./target/release/my-app
deploying my-app to root@203.0.113.7
  uploading artifact...
  activating...
deployed my-app to root@203.0.113.7
Requests reach the proxy, which sends them to the live release. During a deploy the laptop uploads the new release, the server health-checks it, then the proxy moves traffic from the old release on port 10001 to the new one on port 10002 and the old one drains.203.0.113.7 · vela serverequestsmy-app.example.comproxy:443 · TLSrelease …0912:10001stoppedrelease …1430:10002liveyour laptopscp + sshrelease tarballGET /health
  1. Upload
  2. Start on a fresh port
  3. Health check
  4. Swap the proxy
  5. Drain the old release
Requests always flow to a release that has answered its health check. Watch them swing from :10001 to :10002.
In productionapp.cyanea.bio Phoenix, sequentialapp.archipelag.io Phoenix, blue-green

What a deploy does

The old release keeps serving until the new one proves itself.

Fig. 2 The deploy sequence

  1. Upload

    The client tars your binary or BEAM release and copies it with scp. The server unpacks it into a new, read-only release directory.

  2. Start on a fresh port

    Vela runs pre_start (your migrations), then starts the new release on a random port between 10000 and 20000 with PORT and VELA_DATA_DIR set.

  3. Health check

    It asks your health path up to 30 times, a second apart. Anything but a 200 kills the new instance; the old one never noticed.

  4. Swap the proxy

    The built-in proxy points the domain at the new port and the current symlink moves. Requests in flight finish where they started.

  5. Drain the old release

    The old instance gets drain seconds, then SIGTERM, then SIGKILL. The last five releases stay on disk for vela rollback.

Two strategies

Overlap when you can, take turns when you write to SQLite.

Fig. 3 Two instances over time

Blue-green default

Both instances run for a moment, so no request ever finds nothing listening. Use it for anything that keeps its state elsewhere.

Sequential

The old instance stops before the new one opens the database, so two writers never share a SQLite file. Your data directory survives every deploy.

serving health checking draining time →

In the binary

The parts you would otherwise glue together.

Reverse proxy
Routes by domain, passes WebSockets through, redirects HTTP to HTTPS. Built on hyper, inside the daemon.
TLS by itself
Let's Encrypt over ACME HTTP-01, renewed 30 days before expiry. Or a Cloudflare Origin certificate, with optional mTLS for Authenticated Origin Pulls.
SSH as the control plane
The client runs scp and ssh with the keys you already have. No tokens, no deploy API to secure.
Rust and Elixir
Compiled binaries or BEAM releases. When cross-compiling is a pain, remote = true builds on the server from git archive.
Postgres and NATS
Declare them in Vela.toml. Vela installs and provisions them and injects DATABASE_URL or NATS_URL.
Secrets and rollback
Per-app secrets in a mode 0600 file, referenced as ${secret:KEY}. vela rollback goes back one release.
Backups
Scheduled copies of app data, secrets and pg_dump output, to a local path or S3. SQLite is checkpointed first.
Supervision
Restarts apps that crash and restores them when the daemon restarts. vela setup writes a hardened systemd unit.
Status and logs
vela status --json probes every health path live; vela logs my-app -f tails output from your laptop.

Vela.toml

One file in your repo.

[app]
name = "my-app"
domain = "my-app.example.com"

[deploy]
server = "root@your-server.example.com"
type = "binary"              # or "beam" for Elixir
binary = "my-app"
health = "/health"
strategy = "blue-green"      # or "sequential" for SQLite apps
pre_start = "bin/migrate"    # failure aborts the deploy

[env]
DATABASE_PATH = "${data_dir}/my-app.db"
SECRET_KEY_BASE = "${secret:SECRET_KEY_BASE}"

[services.postgres]
version = "17"
databases = ["my_app_prod"]

Every option, and server.toml →

Your app

Four things it has to do.

  1. Listen on $PORT. Vela picks the port; read it from the environment.
  2. Answer its health path. Return 200 within 30 seconds of starting.
  3. Shut down on SIGTERM. Finish what you are doing; SIGKILL follows after a timeout.
  4. Keep files in $VELA_DATA_DIR. Databases and uploads there survive every deploy; release directories are read-only.

Scope

One server, on purpose.

Vela deploys to a single machine. It does not schedule across a cluster, and it is not trying to. If one good server runs your product, Vela is the whole platform.

Next: isolation between apps.

Apps on the same server are not yet separated by Linux namespaces or cgroups, and memory and CPU limits in [resources] are not enforced yet. Process isolation and resource limits are the next milestones. Follow along in the releases.

Fig. 4 The whole platform

       .--------------------------------------.
      /                                      /|
     +--------------------------------------+ |
     |  vela serve              :80   :443  | |
     |  ..................................  | |
     |  proxy    my-app.example.com  -> 02  | |
     |  ..................................  | |
     |  [01] my-app    :10001    stopped    | |
     |  [02] my-app    :10002    live       | |
     |  [--] postgres  :5432     up         | |
     |  ..................................  | |
     |  /var/vela/apps/my-app/data          | +
     |                                      |/
     +--------------------------------------+
        |                              |
       ===                            ===

Your first deploy takes about ten minutes.

Get started Read the docs