# The complete guide to doctl, the DigitalOcean CLI

> Learn doctl with practical tutorials for Droplets, firewalls, DNS, databases, App Platform, Kubernetes, storage, Functions, AI services, and automation.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-11-01 | Topics: [CLI](https://flaviocopes.com/tags/cli/) | Canonical: https://flaviocopes.com/doctl/

DigitalOcean has a command line tool called **doctl**. You can use it to create a server, deploy an application, manage a database, or find out which resources are running in your account.

Once you know the commands, you can put them in a script. A deployment that took several trips through the dashboard becomes something you can repeat from your terminal.

In this guide we'll start with installation and authentication, then build a small web server. From there we'll work through the other services doctl can manage, including App Platform, Kubernetes, storage, and AI services.

You can follow the web server walkthrough from beginning to end. The later sections are independent examples. You don't need to create a database or Kubernetes cluster to learn how to manage a Droplet.

The command names and flags here follow DigitalOcean's [current doctl reference](https://docs.digitalocean.com/reference/doctl/reference/), checked in October 2026. Some newer commands need a recent installation. Resource availability, permissions, and prices also depend on your account and region.

**Creating resources can cost money.** Check the selected plan before running a `create` command, and use the cleanup section when you're finished. Turning a server off does not stop its charges.

## What can you do with doctl?

A **Droplet** is DigitalOcean's name for a virtual server. That's probably the first thing people associate with DigitalOcean, but doctl covers most of the platform.

| What you want to manage | Command group | Examples |
| --- | --- | --- |
| Virtual servers | `compute droplet`, `compute droplet-action` | Create, resize, restart, snapshot, delete |
| Server access | `compute ssh-key`, `compute ssh` | Upload public keys, connect over SSH |
| Networking | `vpcs`, `compute firewall`, `compute load-balancer` | Private networks, traffic rules, load balancing |
| DNS and certificates | `compute domain`, `compute certificate` | DNS records, managed TLS certificates |
| Storage | `compute volume`, `nfs`, `spaces keys` | Block volumes, shared filesystems, object-storage keys |
| Applications | `apps` | Deploy from a spec, inspect deployments, stream logs |
| Managed databases | `databases` | PostgreSQL and other engines, users, pools, replicas |
| Containers | `registries`, `registry` | Registry credentials, repositories, image tags |
| Kubernetes | `kubernetes` | Clusters, node pools, kubeconfig, upgrades |
| Functions | `serverless` | Deploy functions, invoke them, inspect executions |
| Secrets | `secrets` | Store key-value pairs, retrieve values, manage versions |
| Monitoring and security | `monitoring`, `security` | Resource alerts, uptime checks, posture scans |
| AI infrastructure | `serverless-inference`, `dedicated-inference` | Model requests and dedicated endpoints |
| AI data and evaluation | `knowledge-base`, `vector-databases`, `evaluation`, `signals` | Retrieval data, evaluations, collected interaction data |
| Hosted coding agents | `harness-runtime` | Create sessions, send prompts, inspect sandboxes |
| Organization and billing | `projects`, `account`, `balance`, `invoice` | Group resources, inspect quotas, retrieve billing details |

doctl talks to DigitalOcean's APIs. For jobs inside a server, you still use tools such as SSH, your package manager, and your database client.

```mermaid
flowchart TD
  accTitle: Where doctl fits in a deployment
  accDescr: Use doctl with account credentials to create or inspect resources through DigitalOcean APIs. Then use a service-specific client for work inside the resource or with its data.
  A[Your terminal] --> B[doctl]
  B --> C[DigitalOcean APIs]
  C --> D[Create or inspect resources]
  D --> E[Use the appropriate client]
  E --> F[SSH, kubectl, or an S3 client]
```

For example, doctl can create a database cluster. You use `psql` to run SQL against the PostgreSQL database inside it. Likewise, doctl creates a Kubernetes cluster, then `kubectl` manages its workloads.

## Install doctl

On macOS, use [Homebrew](https://flaviocopes.com/homebrew/):

```bash
brew install doctl
```

If it's already installed, update it:

```bash
brew upgrade doctl
```

On Ubuntu with Snap:

```bash
sudo snap install doctl
```

The Snap package is isolated. Grant these connections when you need SSH, Kubernetes configuration, or Docker authentication:

```bash
sudo snap connect doctl:ssh-keys :ssh-keys
sudo snap connect doctl:kube-config
sudo snap connect doctl:dot-docker
```

On Windows, or Linux without Snap, download the archive for your operating system and CPU from the [official releases](https://github.com/digitalocean/doctl/releases). Extract it and put the `doctl` executable in a directory on your `PATH`.

Windows users can run doctl from PowerShell. The shell scripts below use Bash syntax, so use WSL or adapt the variable assignments and line continuations for PowerShell.

Check that your terminal can find the program:

```bash
doctl version
```

If you get `command not found`, check your `PATH` and open a new terminal after changing it.

## Authenticate with DigitalOcean

You need a DigitalOcean account. If you need to create one, you can use [my DigitalOcean referral link](https://flaviocopes.com/go/digitalocean). It's an affiliate link, so eligible signups can earn me account credit. Check the signup page for the current offer.

Recent doctl installations support browser authentication:

```bash
doctl auth login --context tutorial
```

Your browser opens DigitalOcean's authorization page. Choose the account or team and approve the permissions you want doctl to have.

doctl saves the resulting credentials locally. With browser authentication, it can refresh the short-lived access token when needed. See the [authentication reference](https://docs.digitalocean.com/reference/doctl/reference/auth/login/) for scope selection and browser options.

Alternatively, create a personal access token in DigitalOcean's **Applications & API** settings, then enter it at the prompt:

```bash
doctl auth init --context tutorial
```

Give the token the permissions needed for your work. A read-only token is useful for an inventory script. Creating a Droplet needs additional permissions, and a scoped token may also need permission to look up associated resources.

Check that authentication worked:

```bash
doctl account get
```

This reads your account details without creating anything.

### Switch between accounts and teams

An authentication **context** is a named set of credentials. You might have one for personal work and another for a client.

List them and choose the active one:

```bash
doctl auth list
doctl auth switch --context tutorial
```

You can also choose the context for one command:

```bash
doctl compute droplet list --context tutorial
```

I would make the context explicit in scripts that change infrastructure. It makes it harder to run a command against the wrong account.

You can remove locally saved credentials for a context:

```bash
doctl auth remove --context tutorial
```

Removing a context does not delete the cloud resources created with it. Revoke a personal access token through DigitalOcean's token settings when you no longer need it.

### Authenticate in automation

For a CI job, put a personal access token in the CI platform's secret store and expose it as `DIGITALOCEAN_ACCESS_TOKEN` for that job.

doctl reads this environment variable, so the job can run commands without an interactive login:

```bash
doctl compute droplet list --output json
```

Don't paste a token into a checked-in script. Avoid putting its literal value in `--access-token`, where it can end up in shell history or process arguments.

The local doctl configuration also contains credentials. Keep it out of Git and don't paste it into a support ticket.

## Learn the command structure

Most commands follow this pattern:

```text
doctl service resource operation [arguments] [flags]
```

For example:

```bash
doctl compute droplet list
```

`compute` is the service group, `droplet` is the resource, and `list` is the operation.

Some groups are shallower. Listing managed databases is:

```bash
doctl databases list
```

You can ask for help at any level:

```bash
doctl --help
doctl compute --help
doctl compute droplet --help
doctl compute droplet create --help
```

The last command explains the required arguments, supported flags, and examples for creating a server. It also helps when an online tutorial uses commands your installed version doesn't have.

For tab completion in your current Bash session:

```bash
source <(doctl completion bash)
```

For Zsh, initialize its completion system and load doctl's definitions:

```bash
autoload -Uz compinit
compinit
source <(doctl completion zsh)
```

To make completion permanent, follow your shell's completion-directory setup. doctl can also generate Fish and PowerShell completion scripts.

## Read output and extract values

The default output is a table. That's convenient for reading, but scripts should avoid splitting it by spaces.

Choose only the columns you need:

```bash
doctl compute droplet list --format ID,Name,PublicIPv4,Status
```

Remove column headings when you want a raw value:

```bash
doctl compute droplet get 123456789 --format PublicIPv4 --no-header
```

`123456789` is an illustrative resource ID. Use an ID from your account whenever an example shows one. Many resources use UUIDs instead of numeric IDs.

For structured output, use JSON:

```bash
doctl compute droplet list --output json
```

With [jq](https://flaviocopes.com/jq-command/), you can turn that response into a small inventory:

```bash
doctl compute droplet list --output json |
  jq '.[] | {id, name, region: .region.slug, status}'
```

JSON field names can differ from table column names. `PublicIPv4` is a table formatter name; the JSON response represents network interfaces under `networks`.

To find each public IPv4 address:

```bash
doctl compute droplet list --output json |
  jq -r '.[] | .name as $name | .networks.v4[] |
    select(.type == "public") | [$name, .ip_address] | @tsv'
```

Don't assume every command returns an array or supports `--format`. Inspect its JSON and help before building a script around it.

## Choose a region, image, and server size

We'll create a small Ubuntu server running Nginx. Before creating it, we need to choose where it runs and how much hardware it gets.

DigitalOcean uses short identifiers called **slugs** for regions, images, and plans.

List the regions:

```bash
doctl compute region list
```

List operating system images:

```bash
doctl compute image list-distribution
```

List Droplet sizes:

```bash
doctl compute size list
```

A size includes CPU, memory, disk, and transfer information. Check its current price and regional availability before choosing it.

We'll use `nyc3`, `ubuntu-24-04-x64`, and `s-1vcpu-1gb` in the examples. Use alternatives from the lists if these aren't available for your account.

The Ubuntu slug identifies an operating system image, so it needs to be specific. It doesn't mean your doctl installation must match that Ubuntu release.

## Upload an SSH public key

SSH uses a public and private key pair. Your private key stays on your computer. DigitalOcean installs the public key on the new server.

If you need help creating a key pair, see the [free SSH course](https://flaviocopes.com/courses/ssh/).

List the keys already in your account:

```bash
doctl compute ssh-key list
```

If the public key you want to use isn't there, upload it:

```bash
doctl compute ssh-key import tutorial-laptop \
  --public-key-file "$HOME/.ssh/id_ed25519.pub"
```

Copy its ID from the output. In this walkthrough, set a shell variable to your actual key ID:

```bash
SSH_KEY_ID=12345678
```

Adding a key to your DigitalOcean account makes it available for new Droplets. It doesn't automatically install it on all your existing servers.

## Organize the tutorial resources

A **project** groups resources in the dashboard. It helps you find everything related to one application or environment.

Create one for this walkthrough and capture its ID:

```bash
PROJECT_ID=$(doctl projects create \
  --name doctl-tutorial \
  --purpose 'Web Application' \
  --environment Development \
  --format ID \
  --no-header)
```

You can list projects at any time:

```bash
doctl projects list
```

Also create a tag for our server:

```bash
doctl compute tag create doctl-tutorial
```

Tags are useful when a firewall or monitoring rule should follow a group of Droplets. Projects and tags organize resources, but don't replace account permissions or network security rules.

## Prepare the web server's first boot

You can pass a script or a [cloud-init](https://cloudinit.readthedocs.io/) configuration to a Droplet. cloud-init handles initialization when the machine first boots.

Save this as `cloud-init.yaml` in your working directory:

```yaml
#cloud-config
package_update: true
packages:
  - nginx
write_files:
  - path: /var/www/html/index.html
    permissions: '0644'
    content: |
      <!doctype html>
      <html lang="en">
        <meta charset="utf-8">
        <title>doctl tutorial</title>
        <h1>Hello from DigitalOcean</h1>
      </html>
runcmd:
  - [systemctl, enable, --now, nginx]
```

This updates the package index, installs Nginx, and writes a small page. The file contains no credentials, so you can keep it in Git.

Don't use user data to distribute long-lived secrets. Treat it as infrastructure configuration that administrators and platform tooling may be able to inspect.

## Create the firewall before the server

We'll allow HTTP connections from anywhere and SSH connections only from your public IPv4 address.

Find your current public IPv4 through your network settings or a trusted IP-check service. Set it here with `/32`, which means one IPv4 address:

```bash
ADMIN_CIDR=203.0.113.10/32
```

That address is reserved for documentation. **Replace it with your real public IPv4**, or the firewall will block your SSH connection.

Now create a firewall attached to the `doctl-tutorial` tag:

```bash
FIREWALL_ID=$(doctl compute firewall create \
  --name doctl-tutorial-web \
  --tag-names doctl-tutorial \
  --inbound-rules "protocol:tcp,ports:22,address:$ADMIN_CIDR protocol:tcp,ports:80,address:0.0.0.0/0" \
  --outbound-rules 'protocol:tcp,ports:all,address:0.0.0.0/0 protocol:udp,ports:all,address:0.0.0.0/0 protocol:icmp,address:0.0.0.0/0' \
  --format ID \
  --no-header)
```

Notice the rule syntax. Commas separate fields within a rule. Spaces separate complete rules, so the whole rule list needs quotes.

The outgoing rules allow package downloads, DNS lookups, and other IPv4 traffic. We're leaving IPv6 disabled in this small example. If you enable it later, review the firewall's IPv6 rules too.

The firewall applies to Droplets with the selected tag. Creating it first avoids intentionally exposing a new server before adding its traffic restrictions.

Check the firewall:

```bash
doctl compute firewall get "$FIREWALL_ID"
```

Cloud firewalls filter network traffic. You still need to secure SSH and keep the operating system updated. My [Linux server security course](https://flaviocopes.com/courses/linux-server-security/) covers that work.

## Create the Droplet

We can now create the server with our SSH key, project, tag, and first-boot configuration:

```bash
DROPLET_ID=$(doctl compute droplet create doctl-web \
  --region nyc3 \
  --image ubuntu-24-04-x64 \
  --size s-1vcpu-1gb \
  --ssh-keys "$SSH_KEY_ID" \
  --project-id "$PROJECT_ID" \
  --tag-name doctl-tutorial \
  --user-data-file cloud-init.yaml \
  --enable-monitoring \
  --wait \
  --format ID \
  --no-header)
```

`--wait` waits for DigitalOcean's provisioning operation. It doesn't mean Nginx is already serving the page or cloud-init has finished installing packages.

Inspect the server:

```bash
doctl compute droplet get "$DROPLET_ID"
```

Extract its public address:

```bash
DROPLET_IP=$(doctl compute droplet get "$DROPLET_ID" \
  --format PublicIPv4 \
  --no-header)
```

Then connect over SSH:

```bash
doctl compute ssh "$DROPLET_ID" \
  --ssh-user root \
  --ssh-key-path "$HOME/.ssh/id_ed25519"
```

Check the host key fingerprint through a trusted channel before accepting a new SSH host key. doctl's API credentials don't replace SSH authentication.

Inside the server, wait for initialization:

```bash
cloud-init status --wait
systemctl status nginx
```

Leave the SSH session:

```bash
exit
```

From your computer, request the page:

```bash
curl --fail "http://$DROPLET_IP"
```

You should see the HTML from `cloud-init.yaml`. This tutorial endpoint uses plain HTTP. Add TLS before using it for passwords, sessions, or other sensitive data.

### Run a remote command

You don't need to open an interactive SSH session every time. For example, inspect Nginx's status with:

```bash
doctl compute ssh "$DROPLET_ID" \
  --ssh-key-path "$HOME/.ssh/id_ed25519" \
  --ssh-command 'systemctl is-active nginx'
```

This still runs over SSH. Your computer must be able to reach the server's SSH port, and the selected private key must match an authorized public key.

## Understand asynchronous operations

Cloud operations can take time. Resizing a server or creating a snapshot starts work on DigitalOcean's side.

Some commands support `--wait`. You can also inspect actions directly:

```bash
doctl compute droplet actions "$DROPLET_ID"
doctl compute action list
```

For an action ID returned by one of those commands:

```bash
doctl compute action get 987654321
doctl compute action wait 987654321 --wait-timeout 10m
```

A timeout stops the CLI from waiting. The operation can continue on DigitalOcean. Check the resource and action status before repeating a command that creates something.

```mermaid
flowchart TD
  accTitle: A cloud operation can outlive the CLI
  accDescr: doctl submits an operation and DigitalOcean keeps working. A local timeout requires checking the existing operation before retrying.
  A[Submit create request] --> B[DigitalOcean starts operation]
  B --> C{CLI receives completion?}
  C -->|Yes| D[Check application readiness]
  C -->|Timeout| E[Inspect resource and action]
  E --> F{Resource exists?}
  F -->|Yes| G[Continue waiting or investigate]
  F -->|No| H[Decide whether to retry]
```

## Restart, stop, and resize a server

These operations affect the running machine, so schedule them appropriately for a real application.

To request a reboot:

```bash
doctl compute droplet-action reboot "$DROPLET_ID" --wait
```

To request a graceful shutdown:

```bash
doctl compute droplet-action shutdown "$DROPLET_ID" --wait
```

Check the Droplet's status afterward. A shutdown request can complete without the guest successfully shutting down.

`power-off` is available when you need a forced power-off, but it can interrupt writes. Prefer a clean shutdown from the operating system.

**A powered-off Droplet is still billable.** DigitalOcean continues reserving its resources. Delete the Droplet when you want to stop paying for the server itself.

To resize, shut it down and confirm that its status is `off`. Then select an available size:

```bash
doctl compute droplet-action resize "$DROPLET_ID" \
  --size s-2vcpu-2gb \
  --wait
```

Power it back on:

```bash
doctl compute droplet-action power-on "$DROPLET_ID" --wait
```

The default resize changes CPU and memory while preserving the disk size. `--resize-disk` also grows the disk to the selected plan's size. Disk growth affects your ability to return to a smaller plan, because you cannot shrink the Droplet's disk.

See the [Droplet action reference](https://docs.digitalocean.com/reference/doctl/reference/compute/droplet-action/) for rebuilds, recovery, backup settings, and other operations. A rebuild replaces the server's disk contents, so treat it as a destructive operation.

## Snapshots, backups, and custom images

A snapshot captures a disk image you can use to create another server or recover a previous state.

For a straightforward filesystem snapshot, shut down the server cleanly first. Databases may also need their own backup procedure to produce an application-consistent recovery point.

Create a snapshot:

```bash
doctl compute droplet-action snapshot "$DROPLET_ID" \
  --snapshot-name doctl-web-before-upgrade \
  --wait
```

List Droplet snapshots:

```bash
doctl compute snapshot list --resource droplet
```

If you shut down the tutorial server for the snapshot, start it again before continuing:

```bash
doctl compute droplet-action power-on "$DROPLET_ID" --wait
```

Snapshots have their own storage charges. Deleting the original Droplet doesn't remove all separately stored snapshots.

You can use an appropriate snapshot image ID in `--image` when creating a new Droplet. Snapshot regions matter, so check where the image is available before using it.

For automatic backups, inspect the supported policies and backup options:

```bash
doctl compute droplet-action enable-backups --help
doctl compute droplet backup-policies --help
```

Backups and snapshots serve related purposes, but retention and scheduling differ. Choose them according to how much data you can afford to lose and how you plan to restore it.

The image commands also cover account images and custom image imports:

```bash
doctl compute image list
doctl compute image create --help
doctl compute image-action transfer --help
```

## Marketplace applications and autoscaling pools

You can start with a preconfigured Marketplace application instead of a plain operating system image.

List Droplet applications:

```bash
doctl 1-click list --type droplet
doctl compute image list-application
```

Use the appropriate image slug or ID when creating a Droplet. Read the application's setup instructions afterward: an image can still require initial configuration and application credentials.

Kubernetes Marketplace applications are listed separately:

```bash
doctl 1-click list --type kubernetes
```

The Kubernetes cluster commands can install supported 1-Click applications as part of cluster setup.

For applications that need more servers as traffic grows, doctl also manages **Droplet autoscale pools**:

```bash
doctl compute droplet-autoscale list
doctl compute droplet-autoscale create --help
```

A pool defines the machine image and size, instance limits, and scaling targets. Your application must work when instances appear and disappear. Keep user uploads and durable application state outside replaceable server disks.

An autoscale pool doesn't automatically make a single-server application distributed. Plan health checks, traffic routing, and shared state before enabling it.

## Private networks with VPCs

A **VPC** is a private network for cloud resources. Your application can use it to reach a database without sending that connection through the public internet.

List your VPCs:

```bash
doctl vpcs list
```

To create a separate network for a new environment:

```bash
doctl vpcs create \
  --name tutorial-private \
  --region nyc3 \
  --ip-range 10.50.0.0/24
```

Choose an address range that doesn't overlap your existing networks. Use the returned UUID with `--vpc-uuid` when creating a new Droplet.

Keep database access rules and host security in place for resources inside the VPC too.

doctl also has commands for VPC peerings, NAT gateways, and specialized connectivity:

| Task | Where to look |
| --- | --- |
| Inspect a VPC | `doctl vpcs get --help` |
| Connect VPC networks | `doctl vpcs peerings --help` |
| Manage outbound NAT | `doctl compute vpc-nat-gateway --help` |
| Manage partner network attachments | `doctl network attachment --help` |
| Manage bring-your-own-IP prefixes | `doctl network byoip-prefix --help` |

These features have regional and account requirements. Check their current availability before designing a network around them.

## Reserved IP addresses

A **reserved IP** gives you an address you can reassign to another compatible Droplet in the same region.

For example, you can replace a server and move the reserved address to the replacement, keeping the public address your clients use.

Allocate and assign one to a Droplet:

```bash
doctl compute reserved-ip create --droplet-id "$DROPLET_ID"
```

List reserved addresses:

```bash
doctl compute reserved-ip list
```

To move an existing address, use its actual IP and the replacement Droplet ID:

```bash
doctl compute reserved-ip-action assign 203.0.113.25 123456790
```

Check the current reserved-IP billing rules, especially for unassigned addresses. This operation changes where traffic goes; it doesn't copy your application or data to the replacement server.

IPv6 reservations have their own commands under `compute reserved-ipv6`.

## DNS records

doctl can manage domains hosted in DigitalOcean DNS.

This manages DNS zones and records. Register the domain with a registrar separately, and delegate its nameservers to DigitalOcean if you want these records to be authoritative.

List hosted domains:

```bash
doctl compute domain list
```

For the following examples, imagine you own `tutorial.yourdomain.test`. `.test` is reserved for examples, so replace the domain with one you control before running them.

Create a DNS zone:

```bash
doctl compute domain create tutorial.yourdomain.test
```

Create an A record named `web`:

```bash
doctl compute domain records create tutorial.yourdomain.test \
  --record-type A \
  --record-name web \
  --record-data "$DROPLET_IP" \
  --record-ttl 300
```

That produces `web.tutorial.yourdomain.test`. List its records:

```bash
doctl compute domain records list tutorial.yourdomain.test
```

You can also manage AAAA, CNAME, MX, TXT, CAA, and other supported record types. Read the record-specific flags before adding mail or certificate-validation records.

DNS caching means a change may take time to appear everywhere. If you're new to records and delegation, the [DNS course](https://flaviocopes.com/courses/dns/) explains how they fit together.

## Certificates and load balancers

A **load balancer** accepts incoming traffic and forwards it to healthy servers. It can also terminate HTTPS.

For a domain managed in DigitalOcean DNS, request a managed Let's Encrypt certificate:

```bash
doctl compute certificate create \
  --name tutorial-web-tls \
  --type lets_encrypt \
  --dns-names web.tutorial.yourdomain.test
```

Replace the example domain and make sure its DNS setup is correct. The `compute certificate` group can also import custom certificates.

You can inspect the available certificates with:

```bash
doctl compute certificate list
```

As a separate HTTP-only load-balancer example, create a regional load balancer pointing at our tagged server:

```bash
doctl compute load-balancer create \
  --name tutorial-web-lb \
  --region nyc3 \
  --size lb-small \
  --tag-name doctl-tutorial \
  --forwarding-rules 'entry_protocol:http,entry_port:80,target_protocol:http,target_port:80' \
  --health-check 'protocol:http,port:80,path:/,check_interval_seconds:10,response_timeout_seconds:5,healthy_threshold:3,unhealthy_threshold:3' \
  --wait
```

This creates an additional billable resource. Our tutorial firewall already allows HTTP to the backend, so the health check can reach Nginx after the server is powered on.

For a production service, configure HTTPS forwarding with the certificate ID. You can also restrict backend HTTP access to the load balancer using a firewall rule's `load_balancer_uid` source.

Don't remove your working access path until the replacement rules are in place.

The [load-balancer reference](https://docs.digitalocean.com/reference/doctl/reference/compute/load-balancer/create/) covers forwarding rules, sticky sessions, internal load balancers, and global load balancers. Check the flags for your selected load-balancer type, because they don't all use the same configuration.

## Block storage volumes

A **volume** is a separate block device you attach to a Droplet. It is useful when you need persistent storage beyond the server's root disk.

Create a small formatted volume in the same region as the Droplet:

```bash
VOLUME_ID=$(doctl compute volume create tutorial-data \
  --region nyc3 \
  --size 10GiB \
  --fs-type ext4 \
  --format ID \
  --no-header)
```

Attach it:

```bash
doctl compute volume-action attach "$VOLUME_ID" "$DROPLET_ID" --wait
```

Attaching the device and mounting a filesystem are separate operations, although DigitalOcean can automatically mount preformatted volumes on supported images such as the Ubuntu image used here. Inspect the actual mount before writing to it.

On the Droplet, you can inspect disks with:

```bash
lsblk -f
findmnt
```

Use the stable device identifier from `/dev/disk/by-id/` or the filesystem UUID when configuring persistent mounts. Don't format an existing volume just because it isn't mounted; formatting destroys its filesystem.

doctl also supports volume resizing, snapshots, and detachment:

```bash
doctl compute volume list
doctl compute volume-action resize --help
doctl compute volume snapshot --help
doctl compute volume-action detach --help
```

Growing a volume can also require growing its filesystem inside Linux. Unmount it safely before detaching it.

## Network File Storage

**Network File Storage**, or NFS, provides a shared filesystem. Several machines can mount it, which is different from attaching one block device to a Droplet.

doctl can create shares, attach them to VPCs, resize them, manage snapshots, and configure access points.

For example, list the shares in a region where NFS is available:

```bash
doctl nfs list --region atl1
```

Inspect creation and access-point options:

```bash
doctl nfs create --help
doctl nfs access-point --help
```

Creating a share requires its name, region, size, performance tier, and allowed VPC IDs. Use the [NFS documentation](https://docs.digitalocean.com/products/nfs/) to choose supported values and follow its client mount instructions.

Listing a share through doctl doesn't mount it on your computer. Your client still needs network access and an NFS mount.

## Spaces object storage and CDN

**Spaces** stores objects such as images, downloads, and backups. You access those objects through an S3-compatible API.

There is a boundary here: **doctl manages Spaces access keys, but does not upload or download objects**. Use an S3 client such as AWS CLI, rclone, or s3cmd for the files themselves. DigitalOcean documents this in doctl's [limits](https://docs.digitalocean.com/reference/doctl/#limits).

Inspect key-management commands:

```bash
doctl spaces keys list
doctl spaces keys create --help
```

Spaces access keys are separate from your DigitalOcean account API token. Scope them to the buckets and operations the application needs.

Once you've configured AWS CLI with a dedicated Spaces profile, a file upload looks like this:

```bash
aws --profile spaces \
  --endpoint-url https://nyc3.digitaloceanspaces.com \
  s3 cp ./report.pdf s3://tutorial-assets/reports/report.pdf
```

Here `tutorial-assets` stands for an existing bucket you own, and `report.pdf` must exist locally. Substitute your bucket, file, and region endpoint.

CDN management is available through doctl. List existing CDN endpoints:

```bash
doctl compute cdn list
```

For a public Spaces origin you own, you can create an endpoint:

```bash
doctl compute cdn create https://tutorial-assets.nyc3.digitaloceanspaces.com
```

After replacing an object at the same path, you may need to invalidate its cached copy:

```bash
doctl compute cdn flush --help
```

Uploading an object and purging its CDN cache are different requests. A successful upload doesn't guarantee that every cached response immediately changes.

## Managed PostgreSQL and other databases

A managed database moves much of the database administration to DigitalOcean. You still choose the engine, capacity, access rules, and how your application connects.

We'll use PostgreSQL in this example. First, inspect the currently available engines, regions, versions, and sizes:

```bash
doctl databases options engines
doctl databases options regions --engine pg
doctl databases options versions --engine pg
doctl databases options slugs --engine pg
```

Create a single-node tutorial cluster:

```bash
doctl databases create tutorial-postgres \
  --engine pg \
  --region nyc3 \
  --size db-s-1vcpu-1gb \
  --num-nodes 1 \
  --wait
```

Choose an available plan from the options output. A single-node example keeps the setup small; it doesn't provide the redundancy of a primary with standby nodes.

List clusters and copy this one's ID:

```bash
doctl databases list
```

Set it in your shell, replacing the illustrative UUID:

```bash
DB_ID=0b5b8b35-756c-4e13-9b14-51a4050dbb5a
```

### Restrict database connections

Set trusted sources before putting application data in the cluster. For our tagged web server:

```bash
doctl databases firewalls append "$DB_ID" --rule tag:doctl-tutorial
```

Inspect the complete rule set:

```bash
doctl databases firewalls list "$DB_ID"
```

Appending a rule preserves existing rules. Check for other broad access you didn't intend to keep. `replace` replaces the set, so read its help carefully before using it.

A trusted-source rule doesn't create a private network route. The client still needs connectivity to the selected database endpoint.

### Create databases, users, and connection pools

The managed **cluster** is the service. It can contain individual databases and users.

Create an application database and user:

```bash
doctl databases db create "$DB_ID" tutorial
doctl databases user create "$DB_ID" tutorial_app
```

Review the user's permissions for your application. Don't use the administrator account for routine application queries.

Inspect connection details:

```bash
doctl databases connection "$DB_ID"
```

For a client in the appropriate private network:

```bash
doctl databases connection "$DB_ID" --private
```

Connection output can contain credentials. Current doctl releases mask sensitive values in many interactive outputs, but reveal them in some scripting contexts. Treat connection output and JSON as sensitive, and keep them out of CI logs.

Retrieve the cluster's CA certificate using the command's documented output options:

```bash
doctl databases get-ca --help
```

Configure your PostgreSQL client to verify TLS using that CA. Private networking doesn't replace certificate verification.

A PostgreSQL connection pool lets many application requests share a smaller number of database connections:

```bash
doctl databases pool create "$DB_ID" tutorial-pool \
  --db tutorial \
  --user tutorial_app \
  --mode transaction \
  --size 10
```

Transaction pooling changes what session state your application can rely on. Check your database library's pooling guidance before using it.

### Backups, replicas, maintenance, and engine-specific features

Inspect the backups and maintenance settings:

```bash
doctl databases backups "$DB_ID"
doctl databases maintenance-window get "$DB_ID"
```

The database commands also cover these tasks:

| Task | Command area |
| --- | --- |
| Create a read replica | `databases replica create` |
| Resize capacity | `databases resize` |
| Fork a cluster | `databases fork` |
| Change database configuration | `databases configuration` |
| Change maintenance timing | `databases maintenance-window update` |
| Inspect database events | `databases events list` |
| Manage Kafka topics | `databases topics` |
| Manage search indexes | `databases indexes` |
| Inspect supported migration operations | `databases migrate --help` |

The available features depend on the engine. PostgreSQL connection pools, Kafka topics, and OpenSearch indexes aren't interchangeable operations.

For recovery, test the restore procedure before you need it. Seeing a backup in a list doesn't prove your application can recover from it.

## Deploy with App Platform

**App Platform** builds and runs an application from source code or a container image. It handles more of the runtime environment than a plain Droplet.

doctl manages an app through an **app spec**, a YAML or JSON document describing its components.

For an existing app, list it and export its spec:

```bash
doctl apps list
doctl apps spec get YOUR_APP_ID > app.yaml
```

Replace `YOUR_APP_ID` with your app's actual ID. Review exported configuration before committing it, especially environment settings and references to private services.

### Create an app from a spec

We'll use a small Node.js HTTP service here and reuse it in the container examples. Create a repository named `tutorial-api` in your own GitHub account.

Save this as `package.json`:

```json
{
  "name": "tutorial-api",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node server.js"
  }
}
```

Save the server as `server.js`:

```js
import { createServer } from 'node:http'

const port = Number(process.env.PORT || 8080)

createServer((request, response) => {
  response.writeHead(200, { 'content-type': 'application/json' })
  response.end(JSON.stringify({ message: 'Hello from DigitalOcean' }))
}).listen(port, '0.0.0.0')
```

Generate a lockfile and test it locally:

```bash
npm install --package-lock-only
npm start
```

Open `http://localhost:8080/` to check the response. Commit `package.json`, `package-lock.json`, and `server.js` to your repository's `main` branch, then push them to GitHub.

An `app.yaml` for this service looks like this:

```yaml
name: tutorial-api
region: nyc
services:
  - name: web
    github:
      repo: YOUR_GITHUB_USERNAME/tutorial-api
      branch: main
      deploy_on_push: true
    build_command: npm ci
    run_command: npm start
    http_port: 8080
    instance_count: 1
    instance_size_slug: basic-xxs
    health_check:
      http_path: /
    envs:
      - key: NODE_ENV
        value: production
        scope: RUN_TIME
ingress:
  rules:
    - match:
        path:
          prefix: /
      component:
        name: web
```

Replace the repository with one you control. It needs a lockfile for `npm ci`, a `start` script, and a service listening on `0.0.0.0:8080`. The `/` endpoint must respond successfully for the health check.

Your DigitalOcean account also needs access to the GitHub repository. A doctl token alone doesn't authorize DigitalOcean's GitHub integration.

App Platform uses its own region and instance-size identifiers. Notice `nyc` here, compared with `nyc3` for our Droplet. Check the app-specific options:

```bash
doctl apps list-regions
doctl apps tier instance-size list
```

Validate your spec:

```bash
doctl apps spec validate app.yaml
```

Then create the app:

```bash
doctl apps create --spec app.yaml --wait
```

A valid spec doesn't guarantee that the application will build. Dependencies, repository access, and runtime behavior can still fail.

### Update, deploy, and investigate an app

Save your app ID in a variable:

```bash
APP_ID=YOUR_APP_ID
```

After editing the spec, apply it:

```bash
doctl apps update "$APP_ID" --spec app.yaml --wait
```

By default, this updates configuration without fetching new source code. Add `--update-sources` when you want the update to fetch current source changes too.

To create a deployment using the app's current configuration:

```bash
doctl apps create-deployment "$APP_ID" --wait
```

Inspect deployment history and build logs:

```bash
doctl apps list-deployments "$APP_ID"
doctl apps logs "$APP_ID" web --type build
```

Follow runtime logs:

```bash
doctl apps logs "$APP_ID" web --type run --follow
```

A console session can help inspect the running component:

```bash
doctl apps console "$APP_ID" web
```

Changes made inside a running container aren't a durable deployment. Put application changes in the repository and redeploy them.

App Platform supports more than web services: static sites, workers, jobs, and other components have their own spec settings. doctl also exposes alerts, instances, job invocation history, and local component builds under `apps dev`.

If you want more background on the service itself, see [my App Platform introduction](https://flaviocopes.com/digitalocean-apps/).

## Container registries

A **container registry** stores images that Docker, Kubernetes, and deployment platforms can pull.

Current doctl has both `registry` and `registries` command groups. The plural group works with named registries, so we'll use it here. Older examples often use the singular form.

List your registries and the available plans:

```bash
doctl registries list
doctl registries options subscription-tiers
doctl registries options available-regions
```

Choose a globally available registry name and a plan. For example, after replacing this name with your own:

```bash
doctl registries create YOUR_UNIQUE_REGISTRY_NAME \
  --region nyc3 \
  --subscription-tier basic
```

Set the name for the following commands:

```bash
REGISTRY_NAME=YOUR_UNIQUE_REGISTRY_NAME
```

Log Docker into it with credentials that expire after an hour:

```bash
doctl registries login "$REGISTRY_NAME" --expiry-seconds 3600
```

doctl obtains the credentials and configures Docker. Docker builds and pushes the image.

For the Node.js service from the App Platform section, save this as `Dockerfile` beside `server.js`:

```dockerfile
FROM node:lts-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY server.js ./
USER node
EXPOSE 8080
CMD ["node", "server.js"]
```

For production, choose a supported Node.js release deliberately and plan updates to your base image. This example uses the moving LTS tag to keep the tutorial setup small.

Build and push the image:

```bash
docker build -t "registry.digitalocean.com/$REGISTRY_NAME/tutorial-api:demo" .
docker push "registry.digitalocean.com/$REGISTRY_NAME/tutorial-api:demo"
```

Build for the architecture of your deployment target. An ARM image built on a Mac won't run on an x86-only target unless you build a matching image or a multi-architecture image.

List the repositories and image tags:

```bash
doctl registries repository list-v2 "$REGISTRY_NAME"
doctl registries repository list-tags "$REGISTRY_NAME" tutorial-api
```

For a machine that only pulls images, use `--read-only` when obtaining registry credentials. Generated Kubernetes Secret manifests and Docker authentication configuration contain credentials, so treat their output as a secret.

Deleting a tag doesn't necessarily free all image storage. Inspect manifests and garbage-collection commands when cleaning a registry.

If containers are new to you, the [Docker course](https://flaviocopes.com/courses/docker/) explains images, builds, and registries.

## Kubernetes clusters

DigitalOcean Kubernetes runs a managed Kubernetes control plane with worker nodes in your account.

First inspect the supported regions, versions, and node sizes:

```bash
doctl kubernetes options regions
doctl kubernetes options versions
doctl kubernetes options sizes
```

Create a small cluster only if you want to follow this section. Worker nodes and resources created by Kubernetes can add to your bill.

```bash
doctl kubernetes cluster create tutorial-cluster \
  --region nyc3 \
  --size s-2vcpu-2gb \
  --count 2 \
  --wait
```

Choose a supported node size. Omitting `--version` lets the service choose its default; inspect the available versions if you need to select one explicitly.

### Connect kubectl

Install `kubectl` separately, then save the cluster configuration:

```bash
doctl kubernetes cluster kubeconfig save tutorial-cluster
```

Check which Kubernetes context is active before changing workloads:

```bash
kubectl config current-context
kubectl get nodes
```

doctl manages the DigitalOcean cluster. `kubectl` manages Deployments, Services, Pods, and other Kubernetes objects inside it.

Saving kubeconfig can change the current context. Be especially careful if the same computer also manages production clusters.

### Deploy the container image

If you followed the registry section, generate a registry pull Secret for the `default` namespace and apply it:

```bash
doctl registries kubernetes-manifest "$REGISTRY_NAME" \
  --namespace default |
  kubectl apply -f -
```

The Secret is named `registry-` followed by your registry's name. Workloads need to reference it through `imagePullSecrets`; creating the Secret alone doesn't change existing Pods.

For example, generate a Deployment manifest from the image, add the pull Secret, and apply it:

```bash
kubectl create deployment tutorial-api \
  --image="registry.digitalocean.com/$REGISTRY_NAME/tutorial-api:demo" \
  --dry-run=client \
  -o json |
  jq --arg secret "registry-$REGISTRY_NAME" \
    '.spec.template.spec.imagePullSecrets = [{name: $secret}]' |
  kubectl apply -f -
```

Watch the rollout:

```bash
kubectl rollout status deployment/tutorial-api
kubectl get pods
```

For an application listening on port 8080, test it locally through a port forward:

```bash
kubectl port-forward deployment/tutorial-api 8080:8080
```

In another terminal, request it:

```bash
curl --fail http://localhost:8080/
```

A port forward avoids creating a public load balancer just to test this example. Production exposure needs a Service or ingress setup and appropriate TLS configuration.

### Node pools, autoscaling, and upgrades

Inspect node pools:

```bash
doctl kubernetes cluster node-pool list tutorial-cluster
```

Node-pool creation supports settings such as `--auto-scale`, minimum and maximum node counts, labels, and taints. A cluster can have different pools for different workloads.

Check upgrades available to the cluster:

```bash
doctl kubernetes cluster get-upgrades tutorial-cluster
```

Read the upgrade options before changing it:

```bash
doctl kubernetes cluster upgrade --help
```

Cluster upgrades and node replacement affect workloads. Use readiness checks, multiple replicas where appropriate, and disruption budgets for applications that need to stay available.

Deleting a cluster also needs care. LoadBalancer Services and persistent volumes can create resources outside the cluster's own lifecycle. Inspect the deletion flags and audit remaining resources afterward.

## Serverless Functions

DigitalOcean Functions runs functions without asking you to manage a server.

The `serverless` commands use an additional extension. Install it:

```bash
doctl serverless install
doctl serverless status
```

List existing namespaces:

```bash
doctl serverless namespaces list
```

A **namespace** is the environment your functions deploy into. You can connect to an existing namespace, or create a tutorial namespace in a supported Functions region:

```bash
doctl serverless namespaces create --label doctl-tutorial --region nyc1
```

The create command connects your local serverless tooling to the new namespace by default.

Initialize a JavaScript project:

```bash
doctl serverless init tutorial-functions --language javascript
```

Inspect the generated project. It includes a project configuration and sample function code, organized into packages and functions.

Deploy it:

```bash
doctl serverless deploy tutorial-functions
```

List functions to see their exact package and function names:

```bash
doctl serverless functions list
```

If the generated sample is listed as `sample/hello`, invoke it with:

```bash
doctl serverless functions invoke sample/hello --param name:Flavio
```

Use the name from your output if the generated template differs. Add `--full` to include the activation record, which contains execution information and logs.

Inspect the available activation commands:

```bash
doctl serverless activations --help
```

During development, `serverless watch` can deploy changes as you edit. Check the connected namespace before starting it, because edits can trigger real remote executions.

Functions and AI serverless inference are separate services. Deploying a JavaScript function uses `serverless`; sending a prompt to a hosted model uses `serverless-inference`.

## Secrets Manager

Secrets Manager stores named collections of key-value pairs. doctl can create them, update individual keys, inspect versions, and restore secrets.

Suppose your app needs a database password. Put the value in a local file named `database-password.txt` that is excluded from Git and readable only by your user.

Create a secret from that file:

```bash
doctl secrets create tutorial-api \
  --region nyc3 \
  --value database-password=@./database-password.txt
```

The `@` tells doctl to read the value from the file, keeping the password itself out of the command line.

For this file input, make sure the file contains exactly the intended value. An unwanted trailing newline can become part of a secret.

List secrets and inspect versions:

```bash
doctl secrets list
doctl secrets list-versions tutorial-api --region nyc3
```

To retrieve one value for a process that needs it, `secrets get` supports `--key` and `--raw`:

```bash
doctl secrets get tutorial-api \
  --region nyc3 \
  --key database-password \
  --raw
```

That command prints the secret. Avoid running it in shared terminals or logged CI steps. Feed it directly into the intended consumer when automating.

The broader command set includes `set`, `unset`, `update`, `delete`, and `restore`. Read their help to distinguish changing individual keys from replacing a secret's contents.

Storing a value doesn't automatically inject it into every Droplet or app. Your workload still needs a supported retrieval or integration path and appropriate permissions.

## Monitoring and uptime checks

doctl can create resource alert policies and external uptime checks.

For our tagged Droplet, a memory alert can notify you if utilization stays above 80% for five minutes:

```bash
doctl monitoring alert create \
  --description 'Tutorial server memory above 80 percent' \
  --type v1/insights/droplet/memory_utilization_percent \
  --compare GreaterThan \
  --value 80 \
  --window 5m \
  --tags doctl-tutorial \
  --emails YOUR_EMAIL_ADDRESS
```

Replace `YOUR_EMAIL_ADDRESS` with the address that should receive notifications. The monitoring agent must be working for metrics that depend on it. We requested its installation when creating the Droplet.

List your alert policies:

```bash
doctl monitoring alert list
```

An uptime check tests a service from outside the server. Inspect its creation options:

```bash
doctl monitoring uptime create --help
```

You can configure checks for endpoints and add alerts for availability, latency, or certificate expiry. Creating a check and configuring its alert are separate operations.

Resource alerts tell you about a machine. An uptime check tells you whether a client can reach the service. Both can help diagnose a failure.

### Security posture scans

The `security` group manages cloud security posture management scans, also called CSPM scans:

```bash
doctl security scans --help
```

These commands let you create and inspect scans and investigate resources associated with findings. They don't replace application security testing, dependency review, or operating system patching.

## AI model requests from the terminal

Recent doctl releases can make inference requests directly. The command group is `serverless-inference`, also available as `inference`.

This service has its own authentication requirements. According to the [inference command reference](https://docs.digitalocean.com/reference/doctl/reference/serverless-inference/), it accepts a model access key or a full-access DigitalOcean personal access token. A narrowly scoped token that works for Droplets may fail here.

Use a dedicated inference credential where possible. Keep it in a secret store and expose it only to the commands that need it.

List the models exposed by the inference API:

```bash
doctl serverless-inference models list
```

Pick a supported chat model from that output and set its ID:

```bash
CHAT_MODEL=YOUR_CHAT_MODEL_ID
```

Send a prompt:

```bash
doctl serverless-inference chat-completions create \
  --model "$CHAT_MODEL" \
  --message 'Explain what a DigitalOcean VPC does in two sentences.' \
  --max-tokens 200
```

Add `--stream` to receive the response as it arrives. Inference requests can incur usage charges even though they don't create a server in your Droplet list.

### Send a complete JSON request

For more control, put the request body in `chat-request.json`, using a model ID from your account:

```json
{
  "model": "YOUR_CHAT_MODEL_ID",
  "messages": [
    {
      "role": "system",
      "content": "Explain Linux commands to a beginner."
    },
    {
      "role": "user",
      "content": "What does df -h show?"
    }
  ],
  "max_tokens": 200
}
```

Send that file:

```bash
doctl serverless-inference chat-completions create \
  --request chat-request.json
```

This makes a prompt easy to reproduce without fighting shell quoting.

### Embeddings, images, and other inference APIs

An **embedding** turns text into a vector of numbers that applications can use for similarity search.

Choose a supported embedding model, then send text:

```bash
EMBEDDING_MODEL=YOUR_EMBEDDING_MODEL_ID
doctl serverless-inference embeddings create \
  --model "$EMBEDDING_MODEL" \
  --input 'A guide to deploying a Node.js application'
```

The inference commands also expose image generation, Responses-style requests, Anthropic-style messages, and asynchronous invocations:

```bash
doctl serverless-inference images create --help
doctl serverless-inference responses create --help
doctl serverless-inference messages create --help
doctl serverless-inference async-invoke create --help
```

Choose a model and request format supported by that endpoint. A chat model isn't automatically an image model, and different API styles don't necessarily support identical fields.

## Dedicated inference endpoints

With a dedicated inference endpoint, you manage a deployed model-serving resource rather than only sending requests to a shared inference service.

doctl can inspect, create, update, and delete those endpoints:

```bash
doctl dedicated-inference list
doctl dedicated-inference create --help
```

Creation takes a YAML or JSON spec with `--spec`. Use the [dedicated inference reference](https://docs.digitalocean.com/reference/doctl/reference/dedicated-inference/) to select a model and supported infrastructure.

Gated models may need a Hugging Face credential. Keep that credential out of checked-in specs and shell history.

A dedicated endpoint has a different capacity and billing model from a single chat request. Inspect its running state and delete it when the deployment is no longer needed.

## Vector databases and knowledge bases

These services help build applications that retrieve relevant information before generating an answer.

A **vector database** stores vectors and supports similarity search. A **knowledge base** adds a managed workflow for connecting data sources and indexing their contents.

doctl manages the services and their jobs. Your application still needs retrieval and answer-generation logic appropriate to the product you choose.

### Manage a vector database

List existing vector databases:

```bash
doctl vector-databases list
```

To create one, after checking the available plans and regions:

```bash
doctl vector-databases create tutorial-search \
  --region nyc3 \
  --size small \
  --wait
```

The group includes credentials, resizing, backups, restore operations, and restore status. Credential output is sensitive.

Provisioning the database doesn't populate it with your documents. Follow the database's client API to create the collections or indexes your application needs and ingest vectors.

### Manage a knowledge base

List knowledge bases and inspect creation requirements:

```bash
doctl knowledge-base list
doctl knowledge-base create --help
```

Creation requires a name, region, project ID, embedding-model UUID, and data sources. The embedding-model UUID is a platform resource identifier; don't substitute the chat model ID from the previous section.

The commands cover data-source changes, indexing jobs, and attaching or detaching knowledge bases from agents:

```bash
doctl knowledge-base list-indexing-jobs --help
doctl knowledge-base add-datasource --help
doctl knowledge-base attach --help
```

Wait for indexing to finish before expecting new source material to appear in answers. An accepted indexing request isn't the same as a finished index.

Older tutorials may show `doctl gradient`. DigitalOcean has deprecated that command family. Use `knowledge-base` and `serverless-inference` for the corresponding current operations. The old Gradient agent-management commands have no direct CLI replacement; use the documented control panel or API for those agents.

Hosted coding-agent sessions, covered below, are a separate feature.

## AI evaluations and Signals

The `evaluation` commands manage datasets, metrics, model evaluation runs, and agent simulations.

You can inspect the main areas with:

```bash
doctl evaluation dataset --help
doctl evaluation model-run --help
doctl evaluation simulation-run --help
```

A model evaluation scores a candidate against a dataset. A simulation exercises an agent through multi-turn scenarios. Custom metrics and presets help define what a successful result means for your application.

I'd use these when changing a model or prompt could break behavior that users depend on. Save representative questions and expected behavior, then compare runs before deploying a change.

The `signals` group manages consent, interaction sessions, data exports, and deletion jobs:

```bash
doctl signals consent list
doctl signals session --help
doctl signals export --help
doctl signals deletion --help
```

Review consent and data-handling settings before enabling collection. Exported dialogues can contain private user information, and deletion operations can be permanent.

## Run hosted coding agents

DigitalOcean's **Managed Agents Runtime Services**, or M.A.R.S, can run a coding agent in a hosted sandbox. Its doctl command group is `harness-runtime`.

DigitalOcean labels this service a public preview in the [runtime documentation](https://docs.digitalocean.com/reference/doctl/reference/harness-runtime/). Check its current availability and billing before using it.

The distinction between its main commands is useful:

| Command | What it does |
| --- | --- |
| `launch` | Opens a session, creating one first when given creation options |
| `create` | Creates a session without opening its interactive chat |
| `prompt` | Sends a prompt to an existing session |
| `exec` | Runs a command directly in the sandbox |
| `pause` / `resume` | Changes whether a session is running |
| `remove` | Removes the session |

For example, to create a Claude Code session for a repository you control:

```bash
doctl harness-runtime create \
  --name tutorial-review \
  --harness claude-code \
  --gh-repo YOUR_GITHUB_USERNAME/tutorial-api \
  --permission ask \
  --prompt 'Inspect the project and explain how the HTTP server starts.'
```

Connect the required GitHub access and model-provider credentials first. The runtime's help explains provider connections and secret input. A DigitalOcean login doesn't automatically give the sandbox access to private repositories or a model provider.

`--permission ask` requests approval for tool calls. The documented default is `allow`, so choose this deliberately rather than assuming every action needs your confirmation.

Inspect the session and open it:

```bash
doctl harness-runtime show tutorial-review
doctl harness-runtime launch tutorial-review
```

You can also run a command directly in its sandbox:

```bash
doctl harness-runtime exec tutorial-review -- git status --short
```

The `--` separates the guest command's arguments from doctl's flags. Direct execution doesn't ask the coding agent to decide what to run.

Each `exec` call is independent. A `cd` in one call doesn't affect the next; use `--workdir` when you need a specific directory.

The runtime also supports file upload and download, logs, reusable agent configurations, templates, triggers, and session lifecycle operations:

```bash
doctl harness-runtime list
doctl harness-runtime logs tutorial-review
doctl harness-runtime checkpoint --help
doctl harness-runtime fork --help
doctl harness-runtime triggers --help
```

Checkpoint and rollback coverage depends on the selected harness. In particular, the documented `codex-agentapi` mode keeps agent-loop state at OpenAI, which DigitalOcean sandbox checkpoints don't cover.

When finished, remove the session:

```bash
doctl harness-runtime remove tutorial-review
```

Review the repository changes before pushing them. A hosted session can modify files and interact with services according to the credentials and permissions you give it.

## Projects, tags, and inventory

As an account grows, finding resources becomes as useful as creating them.

List just the tutorial's Droplets:

```bash
doctl compute droplet list --tag-name doctl-tutorial
```

List resources assigned to the project:

```bash
doctl projects resources list "$PROJECT_ID"
```

To assign an existing Droplet, use its resource URN:

```bash
doctl projects resources assign "$PROJECT_ID" \
  --resource "do:droplet:$DROPLET_ID"
```

A **URN** identifies both the resource type and its ID. A Droplet ID alone isn't the same as `do:droplet:123456789`.

Don't treat a project as a dependency manager. Deleting or changing a project isn't a reliable way to clean up every service an application created.

For larger inventories, export individual resource families as JSON and combine them in a report. Keep sensitive connection details out of the inventory unless they're specifically needed.

## Billing, invoices, and limits

You can check account balance and billing history from the terminal:

```bash
doctl balance get
doctl billing-history list
doctl invoice list
```

Inspect invoice retrieval options:

```bash
doctl invoice get --help
```

The `prepayment` commands expose prepayment configuration and status, which are relevant to services that use prepaid balances:

```bash
doctl prepayment config
doctl prepayment status
```

For API request limits:

```bash
doctl account ratelimit
```

An API rate limit is different from your Droplet quota, an account spending setting, or a service-specific limit. If a create request fails, read which limit it actually reached.

Billing output isn't a prediction of every future charge. Keep an inventory of persistent resources such as volumes, snapshots, databases, and load balancers when estimating costs.

## Write a useful automation script

Let's make a read-only report of tutorial servers. This is a good first automation because rerunning it doesn't create another server.

Save this as `tutorial-inventory.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail

context=${1:-tutorial}

doctl --context "$context" \
  compute droplet list \
  --tag-name doctl-tutorial \
  --output json |
  jq -r '.[] | [.id, .name, .status, .region.slug] | @tsv'
```

Run it with:

```bash
bash tutorial-inventory.sh tutorial
```

`set -euo pipefail` helps the script stop on failed commands, unset variables, and failures inside pipelines. It doesn't make a provisioning script automatically safe or reversible.

### Make create scripts safe to rerun

If you put `droplet create` in a script and run it twice, you can create two servers. Resource names aren't a reliable uniqueness constraint across DigitalOcean services.

Before creating, look up the resource and decide what to do when zero, one, or several matches exist.

A sequential script can check for an existing tagged server:

```bash
doctl compute droplet list \
  --tag-name doctl-tutorial \
  --output json |
  jq '[.[] | select(.name == "doctl-web")] | length'
```

If it returns `1`, inspect that server instead of creating another. If it returns more than `1`, stop and resolve the ambiguity.

For concurrent deployment jobs, a lookup followed by a create can still race. Serialize provisioning or use a tool with appropriate state management.

### Use exit status and timeouts

Check command exit status before consuming a captured ID. A nonzero result means the command failed locally, but the cloud operation may still exist if the failure was a timeout or lost response.

For a known action, wait with a finite timeout and investigate afterward:

```bash
doctl compute action wait 987654321 --wait-timeout 10m
```

doctl retries some rate-limit and server errors. `--http-retry-max` controls the retry count. Increasing it won't fix an invalid token, missing permission, or unsupported resource size.

### Be deliberate about destructive commands

Many deletion commands ask for confirmation and support `--force` for unattended use. `--force` skips the confirmation prompt; it doesn't validate that you've selected the right resource.

Resolve and print the exact resource IDs before the destructive step. Don't pipe every ID in the account into a delete command.

Also inspect the current release's output behavior before logging JSON. Masked terminal output doesn't guarantee secrets stay masked in automation.

## doctl, Terraform, and configuration management

I would use doctl for a one-off operation, a small script, or a quick inspection during an incident.

For infrastructure that needs to be recreated and reviewed over time, [Terraform](https://flaviocopes.com/terraform-for-developers/) can be a better fit. It records desired resources and compares them with stored state before applying changes.

| Tool | What I would use it for |
| --- | --- |
| doctl | List servers, inspect failures, start a deployment, automate a small operation |
| Terraform | Review and manage a persistent set of cloud resources |
| cloud-init | Set up a machine's first boot |
| Ansible | Configure machines repeatedly as their desired setup changes |
| SSH | Inspect or administer the operating system |
| kubectl | Manage Kubernetes workloads |
| S3 client | Upload, download, and synchronize Spaces objects |

You can use these together. For example, Terraform creates a server, cloud-init initializes it, and doctl helps inspect its current state.

Avoid making unrecorded changes with doctl to resources Terraform owns. Otherwise a later Terraform run may undo the change or report drift.

## Troubleshoot common problems

### A command doesn't exist

Check the installed version and help:

```bash
doctl version
doctl --help
```

Update an old installation before assuming a feature is unavailable. This is especially relevant to browser login, Secrets Manager, newer AI commands, and hosted agent sessions.

Check for renamed commands too. Older Gradient examples use a command family that has been deprecated.

### Authentication works, but an operation is forbidden

Confirm the context and account:

```bash
doctl auth list
doctl account get
```

Then check the credential's permissions and the account's role. A token can be valid and still lack access to a particular operation.

In CI, also check whether `DIGITALOCEAN_ACCESS_TOKEN` is selecting credentials different from your local configuration. Inspect whether it's set without printing its contents.

### SSH times out

Check the public address, firewall, SSH port, and whether your public IP changed. The tutorial firewall only allows SSH from the address in `ADMIN_CIDR`.

If you use a private IP, your computer needs a network path into the VPC. Knowing the private address doesn't create that route.

For `Permission denied (publickey)`, check the SSH user and private key. That error is different from an API authentication failure.

### The Droplet is active, but the website is unavailable

Check cloud-init and Nginx over SSH:

```bash
cloud-init status --long
systemctl status nginx
journalctl -u nginx --no-pager -n 50
```

Then check the cloud firewall and whether the service listens on the expected interface and port. A process bound only to `127.0.0.1` won't accept a public connection directly.

### A region or size is rejected

Use the options for that service. Droplets, databases, App Platform, and Kubernetes don't share one universal list of sizes and regions.

Check image availability and account quotas as well. A valid slug can still be unavailable for the requested combination.

### A command timed out

Inspect the existing resource and operation before retrying. The server may already have been created, or a resize may still be running.

### You need more diagnostic detail

Start with verbose output:

```bash
doctl compute droplet list --verbose
```

`--trace` adds network details. Treat traces as sensitive and review them before sharing, because requests and responses can contain private account information.

## Clean up the tutorial

Review the resources before deleting anything. These commands remove data, and the independent sections may have created resources outside the tutorial project.

First inspect the server and project:

```bash
doctl compute droplet get "$DROPLET_ID"
doctl projects resources list "$PROJECT_ID"
```

When you're ready, delete the tutorial Droplet:

```bash
doctl compute droplet delete "$DROPLET_ID"
```

Delete the firewall after no remaining tutorial server needs it:

```bash
doctl compute firewall delete "$FIREWALL_ID"
```

If you followed the volume example, verify that the volume is detached and no longer needed, then delete it:

```bash
doctl compute volume delete "$VOLUME_ID"
```

Use the appropriate group for each additional resource:

| Resource you created | Cleanup command |
| --- | --- |
| App Platform app | `doctl apps delete "$APP_ID"` |
| Managed database cluster | `doctl databases delete "$DB_ID"` |
| Kubernetes cluster | `doctl kubernetes cluster delete tutorial-cluster` |
| Vector database | `doctl vector-databases delete --help` |
| NFS share | `doctl nfs delete --help` |
| Hosted agent session | `doctl harness-runtime remove tutorial-review` |
| Functions namespace | `doctl serverless namespaces delete --help` |
| Container registry | `doctl registries delete "$REGISTRY_NAME"` |
| Reserved IP | `doctl compute reserved-ip delete --help` |
| Load balancer | `doctl compute load-balancer delete --help` |
| Snapshot | `doctl compute snapshot delete --help` |
| Monitoring policy | `doctl monitoring alert delete --help` |
| Uptime check | `doctl monitoring uptime delete --help` |
| CDN endpoint | `doctl compute cdn delete --help` |
| Knowledge base | `doctl knowledge-base delete --help` |
| Dedicated inference endpoint | `doctl dedicated-inference delete --help` |
| Stored secret | `doctl secrets delete --help` |

The help entries show which IDs or region flags each service requires. Use the IDs from your own inventory and keep confirmation prompts enabled while learning.

Check separately for Spaces objects and buckets with your S3 client, retained Kubernetes volumes or load balancers, registry storage, DNS records, certificates, monitoring resources, and snapshots.

Once the tutorial resources are gone, remove the now-unused project and tag:

```bash
doctl projects delete "$PROJECT_ID"
doctl compute tag delete doctl-tutorial
```

If you created a separate VPC, inspect its members before deleting it. Revoke temporary API tokens and Spaces keys when you no longer need them.

## How I would use doctl

I'd start with the read-only commands on an existing account. List servers, inspect their tags and projects, check deployment logs, and understand the current billable resources.

Then I'd keep a small set of scripts for operations I repeat: finding a server's address, checking an App Platform deployment, and exporting an infrastructure inventory.

For a temporary server, the Droplet walkthrough is enough to make creation repeatable. For a larger environment with several services, I'd put the infrastructure definition in Terraform and use doctl to inspect it and handle individual operations.

I wouldn't turn a growing collection of shell commands into an improvised infrastructure manager. Once a script needs its own dependency graph, locking, rollback rules, and persistent resource state, I'd use a tool that already manages those concerns.

You can keep the [doctl reference](https://docs.digitalocean.com/reference/doctl/reference/) open as you work. Start with one service, read its `--help`, and practice the full lifecycle, including deletion, before automating it.
