The complete guide to doctl, the DigitalOcean CLI

By

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

~~~

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, 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 manageCommand groupExamples
Virtual serverscompute droplet, compute droplet-actionCreate, resize, restart, snapshot, delete
Server accesscompute ssh-key, compute sshUpload public keys, connect over SSH
Networkingvpcs, compute firewall, compute load-balancerPrivate networks, traffic rules, load balancing
DNS and certificatescompute domain, compute certificateDNS records, managed TLS certificates
Storagecompute volume, nfs, spaces keysBlock volumes, shared filesystems, object-storage keys
ApplicationsappsDeploy from a spec, inspect deployments, stream logs
Managed databasesdatabasesPostgreSQL and other engines, users, pools, replicas
Containersregistries, registryRegistry credentials, repositories, image tags
KuberneteskubernetesClusters, node pools, kubeconfig, upgrades
FunctionsserverlessDeploy functions, invoke them, inspect executions
SecretssecretsStore key-value pairs, retrieve values, manage versions
Monitoring and securitymonitoring, securityResource alerts, uptime checks, posture scans
AI infrastructureserverless-inference, dedicated-inferenceModel requests and dedicated endpoints
AI data and evaluationknowledge-base, vector-databases, evaluation, signalsRetrieval data, evaluations, collected interaction data
Hosted coding agentsharness-runtimeCreate sessions, send prompts, inspect sandboxes
Organization and billingprojects, account, balance, invoiceGroup 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.

Where doctl fits in a deploymentUse 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.Your terminaldoctlDigitalOcean APIsCreate or inspectresourcesUse the appropriateclientSSH, kubectl, or anS3 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:

brew install doctl

If it’s already installed, update it:

brew upgrade doctl

On Ubuntu with Snap:

sudo snap install doctl

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

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. 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:

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. 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:

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 for scope selection and browser options.

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

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:

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:

doctl auth list
doctl auth switch --context tutorial

You can also choose the context for one command:

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:

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:

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:

doctl service resource operation [arguments] [flags]

For example:

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:

doctl databases list

You can ask for help at any level:

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:

source <(doctl completion bash)

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

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:

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

Remove column headings when you want a raw value:

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:

doctl compute droplet list --output json

With jq, you can turn that response into a small inventory:

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:

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:

doctl compute region list

List operating system images:

doctl compute image list-distribution

List Droplet sizes:

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.

List the keys already in your account:

doctl compute ssh-key list

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

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:

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:

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

You can list projects at any time:

doctl projects list

Also create a tag for our server:

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 configuration to a Droplet. cloud-init handles initialization when the machine first boots.

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

#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:

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:

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:

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 covers that work.

Create the Droplet

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

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:

doctl compute droplet get "$DROPLET_ID"

Extract its public address:

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

Then connect over SSH:

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:

cloud-init status --wait
systemctl status nginx

Leave the SSH session:

exit

From your computer, request the page:

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:

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:

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

For an action ID returned by one of those commands:

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.

A cloud operation can outlive the CLIdoctl submits an operation and DigitalOcean keeps working. A local timeout requires checking the existing operation before retrying.YesTimeoutYesNoSubmit createrequestDigitalOcean startsoperationCLI receivescompletion?Check applicationreadinessInspect resource andactionResource exists?Continue waiting orinvestigateDecide whether toretry

Restart, stop, and resize a server

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

To request a reboot:

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

To request a graceful shutdown:

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:

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

Power it back on:

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 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:

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

List Droplet snapshots:

doctl compute snapshot list --resource droplet

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

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:

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:

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:

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:

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:

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:

doctl vpcs list

To create a separate network for a new environment:

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:

TaskWhere to look
Inspect a VPCdoctl vpcs get --help
Connect VPC networksdoctl vpcs peerings --help
Manage outbound NATdoctl compute vpc-nat-gateway --help
Manage partner network attachmentsdoctl network attachment --help
Manage bring-your-own-IP prefixesdoctl 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:

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

List reserved addresses:

doctl compute reserved-ip list

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

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:

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:

doctl compute domain create tutorial.yourdomain.test

Create an A record named web:

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:

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 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:

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:

doctl compute certificate list

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

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 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:

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

Attach it:

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:

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:

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:

doctl nfs list --region atl1

Inspect creation and access-point options:

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 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.

Inspect key-management commands:

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:

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:

doctl compute cdn list

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

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:

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:

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:

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:

doctl databases list

Set it in your shell, replacing the illustrative UUID:

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:

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

Inspect the complete rule set:

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:

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:

doctl databases connection "$DB_ID"

For a client in the appropriate private network:

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:

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:

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:

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

The database commands also cover these tasks:

TaskCommand area
Create a read replicadatabases replica create
Resize capacitydatabases resize
Fork a clusterdatabases fork
Change database configurationdatabases configuration
Change maintenance timingdatabases maintenance-window update
Inspect database eventsdatabases events list
Manage Kafka topicsdatabases topics
Manage search indexesdatabases indexes
Inspect supported migration operationsdatabases 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:

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:

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

Save the server as server.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:

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:

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:

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

Validate your spec:

doctl apps spec validate app.yaml

Then create the app:

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:

APP_ID=YOUR_APP_ID

After editing the spec, apply it:

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:

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

Inspect deployment history and build logs:

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

Follow runtime logs:

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

A console session can help inspect the running component:

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.

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:

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:

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

Set the name for the following commands:

REGISTRY_NAME=YOUR_UNIQUE_REGISTRY_NAME

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

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:

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:

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:

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 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:

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.

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:

doctl kubernetes cluster kubeconfig save tutorial-cluster

Check which Kubernetes context is active before changing workloads:

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:

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:

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:

kubectl rollout status deployment/tutorial-api
kubectl get pods

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

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

In another terminal, request it:

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:

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:

doctl kubernetes cluster get-upgrades tutorial-cluster

Read the upgrade options before changing it:

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:

doctl serverless install
doctl serverless status

List existing namespaces:

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:

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:

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:

doctl serverless deploy tutorial-functions

List functions to see their exact package and function names:

doctl serverless functions list

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

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:

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:

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:

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:

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:

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:

doctl monitoring alert list

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

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:

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, 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:

doctl serverless-inference models list

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

CHAT_MODEL=YOUR_CHAT_MODEL_ID

Send a prompt:

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:

{
  "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:

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:

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:

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:

doctl dedicated-inference list
doctl dedicated-inference create --help

Creation takes a YAML or JSON spec with --spec. Use the dedicated inference reference 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:

doctl vector-databases list

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

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:

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:

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:

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:

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. Check its current availability and billing before using it.

The distinction between its main commands is useful:

CommandWhat it does
launchOpens a session, creating one first when given creation options
createCreates a session without opening its interactive chat
promptSends a prompt to an existing session
execRuns a command directly in the sandbox
pause / resumeChanges whether a session is running
removeRemoves the session

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

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:

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

You can also run a command directly in its sandbox:

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:

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:

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:

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

List resources assigned to the project:

doctl projects resources list "$PROJECT_ID"

To assign an existing Droplet, use its resource URN:

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:

doctl balance get
doctl billing-history list
doctl invoice list

Inspect invoice retrieval options:

doctl invoice get --help

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

doctl prepayment config
doctl prepayment status

For API request limits:

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:

#!/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 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:

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:

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 can be a better fit. It records desired resources and compares them with stored state before applying changes.

ToolWhat I would use it for
doctlList servers, inspect failures, start a deployment, automate a small operation
TerraformReview and manage a persistent set of cloud resources
cloud-initSet up a machine’s first boot
AnsibleConfigure machines repeatedly as their desired setup changes
SSHInspect or administer the operating system
kubectlManage Kubernetes workloads
S3 clientUpload, 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:

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:

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:

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:

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:

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

When you’re ready, delete the tutorial Droplet:

doctl compute droplet delete "$DROPLET_ID"

Delete the firewall after no remaining tutorial server needs it:

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:

doctl compute volume delete "$VOLUME_ID"

Use the appropriate group for each additional resource:

Resource you createdCleanup command
App Platform appdoctl apps delete "$APP_ID"
Managed database clusterdoctl databases delete "$DB_ID"
Kubernetes clusterdoctl kubernetes cluster delete tutorial-cluster
Vector databasedoctl vector-databases delete --help
NFS sharedoctl nfs delete --help
Hosted agent sessiondoctl harness-runtime remove tutorial-review
Functions namespacedoctl serverless namespaces delete --help
Container registrydoctl registries delete "$REGISTRY_NAME"
Reserved IPdoctl compute reserved-ip delete --help
Load balancerdoctl compute load-balancer delete --help
Snapshotdoctl compute snapshot delete --help
Monitoring policydoctl monitoring alert delete --help
Uptime checkdoctl monitoring uptime delete --help
CDN endpointdoctl compute cdn delete --help
Knowledge basedoctl knowledge-base delete --help
Dedicated inference endpointdoctl dedicated-inference delete --help
Stored secretdoctl 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:

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 open as you work. Start with one service, read its --help, and practice the full lifecycle, including deletion, before automating it.

Tagged: CLI · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about cli: