Skip to content
Back to cheatsheets

Cheatsheet

Sleipnir

A fast doctl wrapper for the DigitalOcean estate — droplets, apps, and a cost tally, in one command.

View on GitHub

Quick reference for every command, option, and behavior. sleipnir is a thin wrapper over doctl — it shells out, parses the JSON, and formats the result. doctl owns authentication; sleipnir never reads, stores, or logs your DO token.

For the narrative version see the README; for per-command detail in the terminal, run sleipnir <command> help.


At a glance

CommandWhat it doesOptions
lsDroplet table — name · IP · region · size · $/mo · status · tags--tag, --region, --json
ipPrint just a droplet’s public IPv4
sshSSH into a droplet by name-u, -i, -- <ssh args>
surveyWhole-estate view + estimated monthly cost--json
appsApp Platform apps — phase, age, components, live URL--json
deploysRecent deployments for an app-n, --json
logsRead an app’s logs-f, -n, --type
configResolved config + the active doctl context
installWrite a starter config--force
helpThe menu, or detail for one command
versionPrint the version

That is all of v1. Next up is v2 (safe composites — summon, open); see the ROADMAP.


Requirements & global behavior

  • Requires bash + doctl + jq. The doctl-backed commands check all three up front and fail with an actionable message; the meta commands (help, version, config) work on a machine with no doctl account at all.
  • Authentication is doctl’s job. Run doctl auth init once. If the active context is wrong, doctl auth switch --context <name>.
  • The active context is always visible. Estate-wide views carry a header naming the account and doctl context the numbers came from — acting on the wrong DigitalOcean account is the analog of pushing as the wrong GitHub user, so sleipnir makes “which account am I about to hit?” impossible to miss.
  • NO_COLOR — set it (NO_COLOR=1 sleipnir ls) to disable color. Output is also automatically plain when piped or redirected (not a TTY).
  • Two-level helpsleipnir help for the menu, sleipnir <command> help (or -h/--help) for one command.
  • Exit codes0 on success; 1 on error (missing dependency, doctl not authenticated, a name that doesn’t resolve, an unknown command).

Configuration

Read from $SLEIPNIR_CONFIG, defaulting to ${XDG_CONFIG_HOME:-~/.config}/sleipnir/config. It is a plain shell file, sourced at startup — see sleipnir.config.example. Precedence is SLEIPNIR_* environment variables > the config file > built-in defaults.

KeyDefaultUsed by
SLEIPNIR_REGIONnyc3default region for creates (v2)
SLEIPNIR_SSH_USERrootssh
SLEIPNIR_SSH_IDENTITY(unset)ssh — local private key, passed to ssh -i
SLEIPNIR_SSH_KEYS(unset)public-key fingerprints for doctl to attach on create (v2)
SLEIPNIR_TAGSsleipnirdefault tags on created resources (v2)
SLEIPNIR_VOLUME_PRICE_GIB0.10survey — block-storage price per GiB/month

SLEIPNIR_SSH_IDENTITY and SLEIPNIR_SSH_KEYS are different things: the first is a private key file on this machine that ssh uses to log in; the second is a list of public-key fingerprints identifying keys stored at DigitalOcean, for attaching to droplets at create time.

Keep it secret-free. doctl holds the API token; this file holds only defaults and profiles. sleipnir install writes it 0600.


ls

Every droplet on the active account, one row each: name · public IPv4 · region · size · estimated $/mo · status · tags, with a monthly total. (network — one API call)

sleipnir ls
sleipnir ls --tag vor            # only droplets carrying a tag
sleipnir ls --region nyc3        # only droplets in a region
sleipnir ls --tag web --region nyc3
sleipnir ls --json               # machine-readable rows
OptionEffect
--tag <tag>Only droplets carrying this tag
--region <slug>Only droplets in this region (nyc3, sfo3, …)
--jsonEmit JSON rows instead of the table

--json fields: name, ip, region, size, price, tags[], status

$ sleipnir ls

  droplets  · Brett Buskirk LLC · context brett

  rootroute-staging  64.225.22.218    nyc3  s-2vcpu-2gb  $18/mo  active  staging,web,rootroute
  vor-analytics      165.227.123.156  nyc3  s-2vcpu-4gb  $24/mo  active  plausible,analytics,vor

  2 droplets  · ~$42/mo

On the cost figure: it is doctl’s list price for the droplet’s size, summed. It ignores volumes, snapshots, backups, and bandwidth overage — a glance, not an invoice. A droplet with no public interface shows in the IP column rather than dropping out of the table.


ip <name>

Print just the public IPv4 of a droplet — nothing else on stdout, so it drops straight into other commands. Exits non-zero if the name doesn’t resolve.

sleipnir ip vor-analytics
ssh root@$(sleipnir ip vor-analytics)
ansible-playbook -i "$(sleipnir ip web)," site.yml

Matching is exact first, then case-insensitive. Anything ambiguous is an error, never a guess:

SituationBehavior
One matchPrints the IPv4, exit 0
No matchError on stderr + a “did you mean” list of substring matches, exit 1
More than one matchError naming the count, exit 1 (DigitalOcean permits duplicate droplet names)
Match with no public IPv4Error — private-only droplet, exit 1

Errors go to stderr and diagnostics never touch stdout, so IP=$(sleipnir ip web) is safe to embed.


ssh <name>

Resolve a droplet name to its public IPv4 and connect. Sleipnir prints the target on stderr before handing the terminal to ssh, so you always see which host — and which account — you landed on.

sleipnir ssh vor-analytics
sleipnir ssh vor-analytics -u deploy                    # different user
sleipnir ssh vor-analytics -i ~/.ssh/do_ed25519         # explicit key
sleipnir ssh vor-analytics -- -L 8080:localhost:80      # port-forward
sleipnir ssh vor-analytics -- systemctl status caddy    # run and return
OptionEffect
-u, --user <user>Connect as this user (default: SLEIPNIR_SSH_USER, i.e. root)
-i, --identity <path>Private key to use, passed to ssh -i (default: SLEIPNIR_SSH_IDENTITY)
-- <ssh args>Everything after -- is handed to ssh untouched

Name resolution is identical to ip — exact, then case-insensitive, and an ambiguous name is an error rather than a guess.

Why pass-through args land after the host: OpenSSH re-enters its option loop once it has consumed the hostname, so ssh host -L 8080:localhost:80 really does set up the forward — while a remote command only works in that position. Putting everything after the host is what lets one -- slot serve both.


survey

The whole-estate view: droplets, App Platform apps, volumes, and firewalls in detail, plus a count of reserved IPs, databases, load balancers, and Kubernetes clusters — with an estimated monthly cost. (network — several API calls)

sleipnir survey
sleipnir survey --json
$ sleipnir survey

  survey  · Brett Buskirk LLC · context brett
  (network — several API calls)

  droplets         2  ~$42/mo
    rootroute-staging    64.225.22.218    nyc3   s-2vcpu-2gb     $18/mo  active
    vor-analytics        165.227.123.156  nyc3   s-2vcpu-4gb     $24/mo  active

  apps             3  (App Platform — not costed below)
    brett-buskirk-dev    ACTIVE       22m  https://brett-buskirk.dev
    day-one              ACTIVE        1d  https://dayone-sim.app
    rc-journey           ACTIVE        1d  https://rcjourney.cloud

  volumes          1  50 GiB · ~$5/mo
    vor-analytics-data      50 GiB  nyc3    → vor-analytics

  firewalls        2
    vor-analytics-firewall       succeeded  1 droplet
    rootroute-staging-firewall   succeeded  1 droplet

  reserved IPs     none
  databases        none
  load balancers   none
  kubernetes       none
  Spaces           not listable via doctl

  estimated  ~$47/mo  · droplets + block storage

What the cost figure covers

✅ includedDroplets — doctl’s list price for each size
✅ includedBlock storage — volume GiB × SLEIPNIR_VOLUME_PRICE_GIB (default $0.10)
❌ excludedApp Platform, databases, load balancers, Kubernetes
❌ excludedSnapshots, backups, bandwidth overage, reserved-IP idle charges

It is a glance, not an invoice — use it to notice that something is running, not to reconcile a bill. Volumes are the one line sleipnir cannot read a price for (doctl reports a droplet’s price but not a volume’s), which is why that rate lives in config where you can correct it.

”none” vs “unknown”

These mean different things and the view never conflates them:

  • none — doctl answered, and there are genuinely no such resources.
  • unknown — the lookup failed. Nothing is being claimed about what exists.

If a priced lookup fails, the total is an undercount and the footer says so explicitly rather than quietly dropping a line item:

  estimated  ~$42/mo  · droplets only (block storage unreadable)

Spaces are absent by design: doctl exposes only spaces keys, with no bucket listing, and sleipnir does not call the DO API directly to fill gaps in doctl’s coverage.


apps

Every App Platform app on the active account: name · deployment phase · time since the last deploy · components · region · tier · live URL. (network — one API call)

sleipnir apps
sleipnir apps --json
$ sleipnir apps

  App Platform  · Brett Buskirk LLC · context brett

  brett-buskirk-dev  ACTIVE    14m  static:1  nyc  starter  https://brett-buskirk.dev
  day-one            ACTIVE     1d  static:1  nyc  starter  https://dayone-sim.app
  rc-journey         ACTIVE     1d  static:1  nyc  starter  https://rcjourney.cloud

  3 apps  · age is time since the last deployment · sleipnir deploys <app> for history

--json fields: name, id, region, tier, url, phase, deployed (epoch), runtime (count of executing components), components (compact summary)

Components are counted by kind — svc · static · worker · job · fn. An app with no svc, worker, or job has no runtime, which is what logs keys off.


deploys <app>

Recent deployments, newest first — short id · phase · age · cause (usually the triggering commit).

sleipnir deploys day-one
sleipnir deploys day-one -n 25
sleipnir deploys day-one --json
OptionEffect
-n, --limit <count>How many to show (default: 10)
--jsonMachine-readable rows
$ sleipnir deploys day-one -n 3

  deployments · day-one · Brett Buskirk LLC · context brett

  d86e5c57  ACTIVE         1d  commit 70595d7 pushed to github.com/brett-buskirk/day-one/tree/main
  6e30b8ce  SUPERSEDED     1w  commit 06e9b75 pushed to github.com/brett-buskirk/day-one/tree/main
  58f148ee  SUPERSEDED     1w  commit ccfa00b pushed to github.com/brett-buskirk/day-one/tree/main

Takes an app name. doctl apps list-deployments demands a UUID and rejects a name outright (invalid uuid) — sleipnir resolves it for you, using the same matching rules as ip.


logs <app> [component]

Read an app’s logs. Hands off to doctl, so -f streams until you interrupt it.

sleipnir logs day-one
sleipnir logs day-one -f                  # follow
sleipnir logs day-one -n 100              # last 100 lines
sleipnir logs day-one web --type build    # one component, explicit type
OptionEffect
-f, --followStream new lines as they arrive
-n, --tail <lines>Show only the last N lines
--type <type>build · deploy · run (default: chosen for you — see below)

The log-type default

doctl asks for run logs unless told otherwise. But an app with no service, worker, or job has no runtime, and that request hangs on a websocket that never delivers — no error, no output, just a stall until you kill it. For those apps sleipnir asks for build logs instead, and says so:

$ sleipnir logs day-one
  → day-one build logs · Brett Buskirk LLC · context brett (no runtime component — showing build logs)

Pass --type to override. Note that starter-tier apps have no deploy logs at all — DigitalOcean returns a clear 400 saying so, which sleipnir passes straight through.


install

Write a starter config to the resolved config path, with your current defaults filled in. Refuses to overwrite an existing config unless you pass --force — that file is hand-tuned.

sleipnir install
sleipnir install --force    # overwrite an existing config
OptionEffect
-f, --forceOverwrite an existing config file

If doctl is authenticated, your SSH-key fingerprints are prefilled from the account — public identifiers, not secrets, and they never leave the machine. The file is written 0600 and contains no credentials: doctl owns your DO token.


config

Show the resolved configuration: which file was loaded (if any), the defaults in force, and the doctl context + account sleipnir would act on. Works without doctl installed.

sleipnir config
$ sleipnir config

  sleipnir config

    file        /home/you/.config/sleipnir/config (loaded)
    region      nyc3
    ssh user    root
    ssh key     (ssh decides)
    doctl       brett
    account     Brett Buskirk LLC

help / version

sleipnir help          # the command menu
sleipnir help ls       # detail for one command
sleipnir ls help       # same thing
sleipnir ls --help     # and so is this
sleipnir version