Automate complete releases
Describe targets, process managers, pipelines, verification, streamed artifacts, environment synchronization, DNS, TLS, and global edge ingress through typed provider seams.
A deploy pipeline for Bun projects on your own servers. A Target is anywhere you can run a command and copy a file — a DigitalOcean Droplet over SSH, a Linode box, your own laptop. Two ops, four words: exec and upload. Zero ssh2 npm dependency — the bundled sshTarget shells out to the system ssh + rsync binaries.
The default pipeline for a Bun project on Linux: prepare → upload → install → build → link → restart → verify. Releases live in releases/<id>/, a current symlink swaps atomically, rollback(id) re-points the symlink and restarts without re-uploading.
import { createDeployer, sshTarget, systemdManager } from '@absolutejs/deploy';
const deployer = createDeployer({
appName: 'my-app',
target: sshTarget({
host: 'droplet-1.example.com',
user: 'deploy',
identity: '~/.ssh/id_ed25519',
}),
source: { kind: 'directory', root: './' },
env: { PORT: '3000', DATABASE_URL: process.env.DATABASE_URL! },
processManager: systemdManager({ user: 'deploy' }),
verify: { kind: 'http', url: 'http://localhost:3000/health' },
});
const release = await deployer.deploy();
console.log(`Deployed ${release.releaseId} in ${release.durationMs}ms`);The normalized InfrastructureProvider contract covers declared capabilities, node identity and state, inventory, idempotent provisioning, termination, and regional placement across five clouds.
| Provider | Import | Surface |
|---|---|---|
| DigitalOcean | @absolutejs/deploy/digitalocean-infrastructure | Droplets and regional placement |
| Google Cloud | @absolutejs/deploy/gcp | Immutable templates and managed operations |
| Hetzner | @absolutejs/deploy/hetzner-infrastructure | Cloud servers and locations |
| Linode | @absolutejs/deploy/linode-infrastructure | Instances and regional placement |
| Vultr | @absolutejs/deploy/vultr-infrastructure | Instances and regional placement |
Releases are immutable inputs whose transitions remain observable and recoverable.
Global ingress normalizes provider resources while preserving TLS termination at the regional edge.
Managed previews make temporary environments explicit resources with ownership and garbage collection.
A Target is just { exec(cmd, opts?), upload(local, remote, opts?), close?() }. Two are bundled. Provider-native control planes that do not fit the exec-and-upload shape use typed adapters and package entry points, while infrastructure providers expose a shared reconciliation contract.
// localTarget — runs in a local directory. Tests + same-box workflows.
import { localTarget } from '@absolutejs/deploy';
const target = localTarget({ root: '/srv/my-app', env: { ... } });
// sshTarget — shells out to system ssh + rsync. Zero ssh2 dep. Works
// against ANY VPS where the controller machine has ssh + rsync in PATH
// (mac / linux / WSL all do).
import { sshTarget } from '@absolutejs/deploy';
const target = sshTarget({
host: 'droplet-1.example.com',
user: 'deploy',
port: 22,
identity: '~/.ssh/id_ed25519',
rsync: true, // false → scp fallback
sshFlags: ['-o', 'ServerAliveInterval=30'],
});
// A Target is just { exec(cmd, opts?), upload(local, remote, opts?), close?() }.
// Anything that satisfies that contract is a valid target. Provider-specific
// adapters that DON'T fit (Cloudflare Workers API, Fly Machines API, AWS
// Fargate task-run) ship as sibling packages.The default pipeline is a plain array of DeployStep objects — splice in your own, drop ones you don't want, replace the lot. Each step receives a DeployContext with { target, source, releaseId, releasePath, currentPath, env, hooks, annotations, dryRun }.
// Default pipeline for a Bun project on Linux:
// prepare → upload → install → build → link → restart → verify
//
// Override entirely or splice your own step in:
import { defaultBunPipeline, type DeployStep } from '@absolutejs/deploy';
const customSteps: DeployStep[] = [
...defaultBunPipeline(),
{
name: 'warm-cache',
run: async (ctx) => {
await ctx.target.exec(`curl -s http://localhost:${ctx.env.PORT}/warm-cache`);
},
},
];
createDeployer({ /* ... */ steps: customSteps });How files-on-disk become a running process is pluggable. bareManager is the zero-dep default (nohup + pid file). systemdManager generates a templated unit pointing at current/ and runs the daemon-reload + restart dance. Anything that implements { reload, stop?, status? } against a Target works — wrap PM2, supervisord, runit, or @absolutejs/runtime as needed.
// bareManager (default) — 'nohup bun run start &' + pid file. Zero
// remote dependency.
import { bareManager } from '@absolutejs/deploy';
createDeployer({ /* ... */ processManager: bareManager() });
// systemdManager — generate + install a systemd unit pointing at current/.
// daemon-reload + restart. The production answer for VMs.
import { systemdManager } from '@absolutejs/deploy';
createDeployer({
/* ... */
processManager: systemdManager({
user: 'deploy',
group: 'deploy',
execStart: '/usr/local/bin/bun run start',
restart: 'always',
}),
});
// Or implement your own (PM2, supervisord, runit, @absolutejs/runtime):
const customManager: ProcessManager = {
reload: async (target, ctx) => { /* ... */ },
stop: async (target, ctx) => { /* ... */ },
status: async (target, ctx) => 'running',
};HTTP / TCP / custom probes with retries. The deploy is only successful after verify passes — a green deploy that nobody can reach is a yellow deploy. Recommend always wiring one.
// HTTP verify — most common.
verify: {
kind: 'http',
url: 'http://localhost:3000/health',
retries: 30,
intervalMs: 1_000,
expectStatus: 200,
};
// TCP verify — for non-HTTP services.
verify: { kind: 'tcp', host: 'localhost', port: 3000, retries: 30 };
// Custom verify — escape hatch.
verify: {
kind: 'custom',
check: async (ctx) => {
const result = await ctx.target.exec('healthcheck.sh');
return result.exitCode === 0;
},
};Per-release metadata persists as releases/<id>/.deploy-meta.json: commit SHA, ref, message, author, arbitrary tags. dryRun: true logs the plan without mutating the target — verify pipeline shape from CI before flipping a real current symlink. If a deploy fails on verify (slow health check) but the release is intact on disk, resumeReleaseId restarts from the dead step.
// Per-release metadata stored at releases/<id>/.deploy-meta.json.
// Surfaces on DeployResult.annotations and via deployer.readReleaseMeta(id).
const release = await deployer.deploy({
annotations: {
commitSha: 'abc1234',
ref: 'refs/heads/main',
message: 'fix: handle null in checkout',
author: 'alex@example.com',
tags: { ci: 'github-actions', env: 'production' },
},
});
// Resume a failed release — restart from the dead step. Useful when the
// deploy fails on 'verify' (slow health check) but the release is intact
// on disk.
const resumed = await deployer.deploy({ resumeReleaseId: failedReleaseId });
// Dry-run — log the plan without mutating the target. Verify pipeline
// shape from CI before flipping a real 'current' symlink.
await deployer.deploy({ dryRun: true });Rollback re-points current at a previous release and restarts — no re-upload, no re-install, no re-build. prune({ keep: N }) drops the oldest releases. Every deploy + rollback auto-cleans a dangling current.next from a prior crash.
// Rollback — re-points 'current' at a previous release and restarts.
// No re-upload, no re-install, no re-build. Fast.
const previous = (await deployer.listReleases()).at(-2);
if (previous) await deployer.rollback(previous);
// Housekeeping — keep last N releases on disk.
await deployer.prune({ keep: 5 });
// Inspect a release's metadata.
const meta = await deployer.readReleaseMeta(releaseId);
// → { releaseId, annotations, status, failedStep?, completedSteps, ... }These playbooks show where this package fits, how to verify the combined system, and what changes before production.
Current package surface
Import surface · click to copy
Outcomes
Describe targets, process managers, pipelines, verification, streamed artifacts, environment synchronization, DNS, TLS, and global edge ingress through typed provider seams.
Provision or reuse infrastructure from DigitalOcean, Hetzner, Cloudflare, and other adapters without coupling the release model to one provider.
Hardening checklist
Follow in order