Deploying to a DigitalOcean Droplet¶
One command takes a task from a config file to an HTTPS URL you can send to annotators.
pip install 'potato-annotation[deploy]'
export DIGITALOCEAN_TOKEN=dop_v1_...
potato deploy up myproject/config.yaml --provider digitalocean
It prints what it will create, what it will cost and what the URL will expose,
then asks before spending anything. Several minutes later — most of it spent
installing Docker and pulling the image on a fresh machine — you have a running
server at https://<droplet-ip>.
Getting a token¶
You need a DigitalOcean account and a personal access token with write scope.
1. Make an account. https://cloud.digitalocean.com/registrations/new. DigitalOcean asks for a payment method during signup, before anything can be created.
2. Open the token page. https://cloud.digitalocean.com/account/api/tokens, reachable from API in the sidebar.
3. Generate a new token. You will be asked for a name, an expiry and a set of scopes.
- Name. Anything;
potato-deploymakes it obvious what to revoke later. - Expiry. The shortest one that covers your study. A token is a password for your whole account, so one that lapses on its own is a much smaller problem than a permanent one sitting in a shell history.
- Scopes. Full access. Potato creates and deletes droplets, volumes,
firewalls and SSH keys, and reads your account to check the token before
doing any of it. A narrower set that misses one of those fails partway
through
up, which is the failure that can leave a droplet billing with nothing recorded locally.
4. Copy it immediately. DigitalOcean shows the token once. If you lose it, delete it and make another.
5. Put it in your shell.
export DIGITALOCEAN_TOKEN=dop_v1_...
Add it to ~/.zshrc or ~/.bashrc if you will use it again, or keep it in the
one terminal if you would rather it not persist.
6. Check that it works — before it can cost anything:
potato deploy providers --verify
Verifying:
digitalocean OK — researcher@example.edu, droplet limit 10
A token that is expired, read-only, or pasted with a stray newline looks
exactly like a working one until up is several resources deep, so it is worth
the two seconds. A bad one says so:
digitalocean REJECTED — DigitalOcean rejected the token (401). Check that it
has not expired and that it carries write scope.
When the study is over, delete the token on the same page. Nothing Potato created depends on it continuing to exist.
Where Potato looks¶
--token, then DIGITALOCEAN_TOKEN, DIGITALOCEAN_ACCESS_TOKEN or
DO_TOKEN, then the token doctl stores in ~/.config/doctl/config.yaml. It
is never written to disk by Potato and never enters the bundle.
potato deploy providers prints which of those it found, with the token
masked, which settles "did the export take" without echoing a credential.
Install the extra¶
The DigitalOcean provider talks SSH, which needs paramiko:
pip install 'potato-annotation[deploy]'
potato deploy providers tells you if it is missing, before you have chosen a
target and typed a token.
What gets created¶
| Resource | Purpose | Cost |
|---|---|---|
Droplet, s-2vcpu-2gb |
Runs the server | $18/month |
| Cloud firewall | Inbound 22, 80, 443 only | free |
| SSH key | Generated for this deployment alone | free |
Block volume (with --volume-gb) |
Annotations that survive the droplet | $0.10/GB/month |
Everything is tagged potato and potato-<name>, so you can find and remove it
from the console even if the local state file is lost.
s-1vcpu-1gb is offered by DigitalOcean and Potato will warn you off it. The
image is about 840 MB and the working set is numpy, pandas and scipy; 1 GB
swaps. 2 GB is the smallest size worth using.
How TLS works without a domain¶
With no --domain, Caddy obtains a Let's Encrypt certificate for the droplet's
IP address. Browsers accept it with no warning and no DNS to configure.
IP certificates are valid for about six days, not ninety. Renewal happens
automatically and has roughly two days of slack rather than thirty. If a study
runs unattended for weeks, check potato deploy status occasionally — a failed
renewal shows up there as a TLS error before an annotator meets a browser
warning.
The certificate store belongs on a volume. With --volume-gb it is placed
there automatically, so a restart does not mean re-issuing.
For a real name, point an A record at the droplet and pass --domain:
potato deploy up myproject/config.yaml --provider digitalocean \
--domain annotate.example.edu
That path uses ordinary ninety-day certificates. Create the DNS record first — Caddy validates over HTTP-01 and needs the name to resolve.
Potato does not use sslip.io or similar wildcard DNS services. They are
deliberately absent from the Public Suffix List, which means a browser treats
every host under one as the same site for cookie purposes: anyone else's machine
on that domain could set a cookie your Flask session would accept.
Keeping the annotations¶
potato deploy pull myproject/config.yaml
This downloads the annotation output directory, plus project.sqlite (memos,
codebook, cases, search index, review workflow) and datasets.sqlite. Neither
database can be regenerated.
Both are snapshotted with sqlite3 .backup on the host before being copied.
That is not a detail you can skip if you fetch them yourself: they run in WAL
mode with a live writer, so copying the file gives you a database that is
corrupt or missing recent work, with nothing to tell you so.
potato deploy destroy refuses to run on a deployment that has never been
pulled. --force overrides it.
Use --volume-gb for anything longer than a pilot:
potato deploy up myproject/config.yaml --provider digitalocean --volume-gb 25
The task directory, with its annotations and databases, then lives on the
volume (/opt/potato/data/app) rather than the droplet's own disk, so
resizing or rebuilding the droplet does not lose them. The volume is formatted
and mounted over SSH after it is attached, not at first boot, and a volume that
already holds data is never reformatted.
Deployments created before this change kept the task on the droplet's disk
even with a volume, and continue to use that layout on later ups. To move
one onto its volume, pull, destroy and deploy again.
Access control¶
Deploy does not change how your task authenticates. Whatever user_config and
authentication say is what ships.
Preflight tells you what the URL exposes before you confirm. Open registration on a public host is reported as a warning, not an error, because for a crowdsourced study it is often the point. To restrict it:
user_config:
allow_all_users: false
users:
- alice
- bob
The admin API key is generated per deployment, injected as an environment
variable, and stored locally in .potato/secrets.json at mode 0600. It never
enters the bundle. Keep .potato/ out of version control — Potato's own
.gitignore already does.
Managing a deployment¶
potato deploy status myproject/config.yaml # is it up and is TLS healthy
potato deploy logs myproject/config.yaml -f # follow the server logs
potato deploy list myproject/config.yaml # every deployment of this config
potato deploy up myproject/config.yaml --provider digitalocean # push changes
potato deploy destroy myproject/config.yaml # remove everything
Running up again on the same name updates the existing droplet rather than
creating a second machine: it uploads the new bundle, pulls the image (--image,
or the current latest) and restarts the service. The size, region, volume and
domain are fixed when the droplet is created; an up that asks to change one is
refused, so pull, destroy and deploy again to change them. The same applies to
every VM target (Lightsail, EC2, Hetzner, Vultr, Linode, OpenStack). Use
--name to run several deployments from one config:
potato deploy up config.yaml --provider digitalocean --name pilot
potato deploy up config.yaml --provider digitalocean --name main
The deploy sequence¶
The waits are long, so it helps to know which one you are sitting in.
- Verify the token, then generate an ed25519 key for this deployment only, so that your own SSH keys are never uploaded and revoking one deployment cannot affect another.
- Create the volume, if you asked for one.
- Launch the droplet from DigitalOcean's
docker-20-04image with a cloud-init payload, and record its id locally before anything else runs. A machine whose id was never written down is a machine that bills forever. - Attach the firewall.
- Wait for an IPv4 address, then for SSH, then for cloud-init. These are three separate moments minutes apart.
- Upload the bundle over SFTP as a single archive.
- Write the environment file at mode 0600, holding the session key and admin key.
- Start the server and Caddy, then poll
/healthuntil it answers.
The app listens on 127.0.0.1:8000 inside the droplet and is reachable only
through Caddy. Port 8000 is never opened, so there is no plaintext route to the
task even if the firewall is removed.
Run potato deploy up --dry-run to see all of it, including the exact firewall
rules and cloud-init, without creating anything or needing a token.
Troubleshooting¶
Refusing to deploy — preflight found an error. Each finding says how to
satisfy it. --force is not honoured for a public host.
It stops at "Waiting for first-boot provisioning" — Docker is installing and
the image is pulling, which takes several minutes on a small droplet. If it
fails, the last 40 lines of /var/log/cloud-init-output.log are printed.
"never became healthy" — the droplet was created and is still running. It is
not cleaned up automatically, because the logs on it are how you find out what
happened. Use potato deploy logs, then destroy --force when you are done.
A browser certificate warning — most often DNS. With --domain, the A
record must resolve before Caddy can validate. Without one, check
potato deploy status: a failed IP-certificate renewal reports as a TLS error.
No deploy key for '<name>' — .potato/secrets.json was deleted. The key
exists nowhere else. Add your own key to the droplet from the DigitalOcean
console to reach it.
Related¶
- Deploying a task — choosing a target
- Your own machine — the same bundle, no account, no cost
- Docker — the image this deploys, and running it yourself
- Reverse proxy — if you terminate TLS yourself
- Scaling — how many annotators one droplet serves