RAMM
A Bun library for server management and deployment automation in TypeScript. Write your infrastructure logic in real code — no YAML, no DSL, no templating language to fight with.
Why not Ansible?
Ansible is great, but it comes with a cost: you write infrastructure in YAML with Jinja2 templates. The moment your logic gets non-trivial — conditionals, loops, dynamic values — you're fighting the format instead of solving the problem.
RAMM takes a different approach: your deployment scripts are just TypeScript. Full language, real abstractions, IDE support, type checking, any npm package you need. If you know JS, you already know how to write RAMM scripts.
The trade-off is explicit: Ansible gives you a huge module ecosystem and declarative guarantees. RAMM gives you simplicity and the full power of a real programming language. For developers who want to own their deployment code without learning a separate tool, RAMM is the better fit.
How it works
The core pattern is buildAndRunOverSsh: write a script that configures your server (server.ts), then run it from your local machine (client.ts). RAMM compiles the server script with bun build and executes it on the remote host — all commands inside run on the server.
client.ts → [bun build] → server.ts runs on remote
(local machine) (all execCommand calls are local to the server)
Installation
bun add ramm
Quick Start
client.ts — runs on your local machine:
import { buildAndRunOverSsh, Context } from "ramm";
const ctx = new Context({ user: "root", address: "1.2.3.4" });
await buildAndRunOverSsh({
entrypoint: "./server.ts",
context: ctx,
});
server.ts — compiled and executed on the remote server:
import {
installPodman,
createPodmanCommand,
runPodmanContainerService,
setupNftable,
printBlock,
} from "ramm";
printBlock("Firewall");
await setupNftable({
allowedIpV4: ["1.2.3.4"],
allowedPorts: [443],
});
printBlock("Container");
await installPodman();
const cmd = createPodmanCommand({
name: "app",
command: "myregistry.io/app:latest",
envs: [{ name: "PORT", value: "3000" }],
});
await runPodmanContainerService("app", cmd);
API
Context
Describes the server connection.
const ctx = new Context({
user: "root",
address: "1.2.3.4",
sshKey: "~/.ssh/id_ed25519",
sudo: false,
userspace: false,
});
ctx.getAddress();
Commands
execCommand(command, props?, context?)
Runs a command locally. Throws on non-zero exit code.
await execCommand("systemctl restart app");
await execCommand("mkdir -p /opt/app", {}, ctx);
execCommandMayError(command, props?, context?)
Same as execCommand but does not throw — returns the result with exit code.
const result = await execCommandMayError("command -v podman", {}, ctx);
if (result.spawnResult.exitCode !== 0) {
}
execCommandOverSsh(command, context)
Runs a command on a remote server over SSH.
await execCommandOverSsh("systemctl restart app", ctx);
Copies files to the remote server using rsync.
await copyFilesOverSsh("./dist/", "/opt/app/dist", ctx);
Build & Deploy
buildAndRunOverSsh({ entrypoint, context })
Compiles a TypeScript script with bun build and runs it on the remote server. This is the primary usage pattern.
await buildAndRunOverSsh({
entrypoint: "./server.ts",
context: ctx,
});
Passes variables from the local environment into the server script. Call passVarsClient before buildAndRunOverSsh, then passVarsServer inside the server script.
await passVarsClient({ DB_URL: process.env.DB_URL }, ctx);
await buildAndRunOverSsh({ entrypoint: "./server.ts", context: ctx });
const vars = await passVarsServer();
console.log(vars.DB_URL);
Packages
installSystemPackage(package, context?)
Installs a system package. Auto-detects the OS and uses apt or dnf. Skips if already installed.
await installSystemPackage("nftables");
await installSystemPackage({
name: "nginx",
command: "nginx",
});
Installs Bun on a remote server.
await installBunOverSsh(ctx);
Podman
installPodman(context?)
Installs Podman and creates the ramm network. Skips if already installed.
await installPodman();
createPodmanCommand(options)
Builds a podman run command string from structured options.
const cmd = createPodmanCommand({
name: "app",
command: "myregistry.io/app:latest",
networks: ["ramm"],
replace: true,
background: true,
envs: [{ name: "PORT", value: "3000" }],
volumes: [{ from: "/data", to: "/data" }],
});
runPodmanContainer(name, command, context?)
Runs a container. Skips if already running with the same command. Recreates if the command changed.
await runPodmanContainer("app", cmd);
runPodmanContainerService(name, command, context?)
Runs a container and registers it as a systemd service for autostart on reboot.
await runPodmanContainerService("app", cmd);
loginPodman(address, login, password, context?)
Authenticates with a container registry.
await loginPodman("registry.example.com", "user", "password");
addNftPodmanRule(context?)
Adds an nftables rule to allow Podman network traffic through the firewall.
await addNftPodmanRule();
Systemd
createSystemdService(name, content, context?)
Writes a unit file, enables and starts the service.
await createSystemdService(
"app.service",
`[Unit]
Description=My App
[Service]
ExecStart=bun run /opt/app/server.js
Restart=always
[Install]
WantedBy=multi-user.target`
);
createSystemdUnit(name, content, context?)
Writes a unit file and reloads systemd. Does not start the service.
startSystemdUnit(name, context?) / restartSystemdUnit(name, context?) / enableSystemdUnit(name, context?) / reloadSystemd(context?)
Systemd unit management.
await reloadSystemd();
await enableSystemdUnit("app.service");
await startSystemdUnit("app.service");
getSystemdPathToUnit(name, context?)
Returns the path to the unit file — /etc/systemd/system/ or ~/.config/systemd/user/ for userspace context.
Firewall (nftables)
setupNftable({ allowedIpV4, allowedPorts, context? })
Creates an inet ramm nftables table. Closes all ports except the ones listed, rate-limits SSH, saves the config, and enables autostart.
await setupNftable({
allowedIpV4: ["1.2.3.4"],
allowedPorts: [80, 443],
});
SSH Keys
createAndAddSshKey(filePath, comment, context)
Creates an ed25519 key (if it doesn't exist) and adds the public key to authorized_keys on the server.
await createAndAddSshKey("~/.ssh/deploy_key", "deploy", ctx);
addSshKeyToUse({ key, fingerprint, filePath, server, context? })
Saves a private key locally, adds the fingerprint to known_hosts, and configures ~/.ssh/config.
saveSshFingerptint(filePath, context)
Fetches the server's fingerprint and saves it to a file.
addKeyToHostConfig(pathToHost, address, pathToKey, context?)
Adds a Host block to the SSH config.
Files
writeFile(path, content, context?)
Writes a file. Skips if the content is already the same.
await writeFile("/etc/app/config.json", JSON.stringify(config));
writeFileStrUniq(path, str, context?)
Appends a string to a file only if it is not already present.
await writeFileStrUniq("~/.bashrc", 'export PATH="$PATH:/opt/bin"');
Cron
createCron({ time, pathToFile, context? })
Adds a crontab entry. Skips if the entry already exists. Updates the schedule if the file is already in crontab with a different time.
await createCron({
time: "0 3 * * *",
pathToFile: "/opt/scripts/backup.sh",
});
Utilities
printBlock(name)
Prints a named block to the console — useful for structuring deployment output.
printBlock("Database");
normalizePath(path)
Expands ~/ to an absolute path.
normalizePath("~/.ssh/config");