Every deploy follows the same sequence. Understanding it helps you debug issues and write apps that work well with Vela.
vela deploy ./target/release/my-app
│
├─ 1. Read Vela.toml
│ Parse manifest, resolve server address
│
├─ 2. Create tarball
│ tar czf the artifact (file or directory)
│
├─ 3. Upload via scp
│ scp tarball → server:/tmp/vela-deploy-<app>.tar.gz
│
├─ 4. Activate (ssh → server)
│ │
│ ├─ 4a. Generate release ID (timestamp: 20260305-143022)
│ ├─ 4b. Extract tarball → /var/vela/apps/<app>/releases/<id>/
│ ├─ 4c. Register app config (upsert app.toml, persist env)
│ ├─ 4d. Provision services (Postgres, NATS — if [services] configured)
│ ├─ 4e. Run pre_start hook (if configured; abort on failure)
│ ├─ 4f. Start new instance on random port (with injected service env vars)
│ ├─ 4g. Run health check (retry up to 30 times, 1s apart)
│ │
│ ├─ On health check success:
│ │ ├─ 4h. Update proxy routing (domain → new port)
│ │ ├─ 4i. Update current symlink → new release
│ │ ├─ 4j. Drain old instance (wait drain_seconds)
│ │ ├─ 4k. Stop old instance
│ │ ├─ 4l. Run post_deploy hook (if configured; log-only on failure)
│ │ └─ 4m. Clean up old releases (keep last 5)
│ │
│ └─ On health check failure:
│ ├─ Kill new instance
│ └─ Old instance keeps running (nothing changed)
│
└─ 5. Done
Print success or failure
/var/vela/apps/my-app/
├── app.toml # Parsed from Vela.toml
├── data/ # Persistent (never touched by deploys)
│ └── my-app.db # Your SQLite database
├── releases/
│ ├── 20260305-140000/ # Previous release (kept for rollback)
│ │ └── my-app
│ └── 20260305-143022/ # Current release
│ └── my-app
└── current -> releases/20260305-143022
Time ──────────────────────────────────────────►
Old instance ████████████████████░░░░ (draining, then stopped)
New instance ░░░░████████████████████
▲ ▲
start │ │ health check passes,
│ traffic swaps
│
health checking
Both instances run briefly. Zero downtime. Use for stateless apps.
Time ──────────────────────────────────────────►
Old instance ████████████████████
New instance ░░░░████████████████████
▲ ▲
stop │ │ start + health check
│
~1s blip
Old stops before new starts. Sub-second downtime. Use for SQLite apps.
Your app must expose an HTTP endpoint that returns 200 when ready.
GET http://localhost:{PORT}/health → 200 OK
Vela checks this endpoint:
- Interval: every 1 second
- Timeout: 5 seconds per attempt
- Retries: 30 attempts (30 seconds total)
If your app needs more startup time, this will be configurable in a future version.
- Don't return 200 until your app is actually ready (DB migrations done, caches warmed)
- For Phoenix apps, use
Phoenix.Endpoint.HealthCheckor a simple plug - For Rust apps, a basic
/healthroute returning200 OKis enough
Two optional hooks run during the deploy sequence:
Runs after extraction (step 4d), before the new instance starts. Use for database migrations, asset compilation, or validation.
[deploy]
pre_start = "bin/my_app eval 'MyApp.Release.migrate()'"If the hook exits non-zero, the deploy aborts immediately. The old instance stays running.
Runs after traffic switches to the new instance (step 4k). Use for cache warming, notifications, or cleanup.
[deploy]
post_deploy = "curl -X POST https://hooks.slack.com/..."If the hook fails, the failure is logged but the deploy is not rolled back (traffic already switched).
Both hooks run with the same environment variables as the app (secrets, manifest env, PORT, VELA_DATA_DIR) and use the release directory as the working directory.
Rolling back switches to the previous release:
vela rollback my-appThis reactivates the previous release directory, restarts the process, and swaps the proxy. Same health check flow applies.
When [build] remote = true is set in your Vela.toml, the deploy flow changes:
vela deploy
│
├─ 1. Read Vela.toml (no artifact argument needed)
│
├─ 2. Upload source via git archive
│ git archive HEAD → tarball → scp to server
│
├─ 3. Build on server (ssh)
│ Extract source → run build command with build env vars
│
├─ 4. Package build output
│ tar the build directory → move to deploy location
│
└─ 5. Activate (same as steps 4a-4m above)
Only committed files are uploaded. Build artifacts (node_modules, target/) are excluded automatically by git archive.
When [services] is configured in your Vela.toml, Vela provisions services before starting your app:
- Postgres: Installed via apt, databases and users created with generated passwords.
DATABASE_URLinjected. - NATS: Binary downloaded, config generated, started as a supervised process.
NATS_URLinjected.
Services are provisioned once and reused across deploys. Credentials are persisted in /var/vela/services/.
- Listen on
$PORT— Vela assigns a random port via thePORTenv var - Health endpoint — Return 200 at the path you configure in
Vela.toml - Graceful shutdown — Handle
SIGTERMto drain in-flight requests - Use
$VELA_DATA_DIR— Store SQLite databases and persistent files here, not in the release directory