Deploying a Task¶
Putting a task somewhere annotators can reach it. It ships with the potato-tasks skill:
/plugin marketplace add davidjurgens/potato-skill
/plugin install potato-tasks@potato
potato start serves on one machine. Three ways past that, in increasing order
of commitment:
| Want | Use |
|---|---|
| A colleague on the same network | potato start --host 0.0.0.0 -p 8000, give them the LAN address |
| Someone to look at it for an hour | potato share config.yaml — temporary public HTTPS |
| Annotators working over days or weeks | potato deploy up config.yaml --provider … |
potato deploy and potato share are listed under other commands in potato --help:
potato deploy --help, potato share --help.
A temporary public URL¶
potato share config.yaml -p 8000 --backend cloudflared # or tailscale, ngrok
Serves the task and opens a tunnel to it. The URL dies when the command does, so
it is for a demo or a pilot with three people, not for a study. It runs a
preflight first and asks you to confirm the exposure; --yes skips the
confirmation and --skip-preflight skips the assessment, which you should not do
on anything with real annotators behind it.
Same rule as potato start: it does not return. Background it.
Hosted deployment¶
potato deploy providers # what targets exist, which have credentials
potato deploy check config.yaml --provider render # preflight, changes nothing
potato deploy build config.yaml --out ./bundle # assemble, stop
potato deploy up config.yaml --provider render --dry-run # the plan and the cost
potato deploy up config.yaml --provider render # provision
potato deploy status config.yaml
potato deploy logs config.yaml -f
potato deploy pull config.yaml --dest ./collected # get the annotations back
potato deploy destroy config.yaml
potato deploy list config.yaml # deployments recorded for this config
The five targets¶
digitalocean supports pull
huggingface ephemeral filesystem, supports pull
local local only, supports pull
render ephemeral filesystem, supports pull
tunnel temporary public URL; stops when `potato share` does
potato deploy providers --verify asks each one whether its token actually
works, rather than only whether one is present. Credentials are picked up from
the environment and from provider caches — a HuggingFace token in the
huggingface_hub cache is found without being configured for Potato.
"Ephemeral filesystem" means annotations are lost when the host restarts. On
render and huggingface this is not a warning about an edge case; a redeploy
or an idle timeout takes the data with it. Either give it somewhere to back up
to, or accept that the run is disposable:
potato deploy up config.yaml --provider huggingface \
--hf-token $HF_TOKEN --backup-minutes 30 # mirror annotations to a Dataset
potato deploy up config.yaml --provider render --demo # throwaway; silences the warning
--hf-token works on any provider, not only HuggingFace: it is the generic
"back the annotations up somewhere durable" flag.
Choosing a target¶
The five differ on who can reach the task, whether the annotations survive a restart, and what it costs. The rest is provisioning detail.
| Target | Reachable from | Annotations survive a restart | Cost | Also needs |
|---|---|---|---|---|
local |
127.0.0.1 only |
yes, in the mounted bundle | free | Docker installed and running |
tunnel |
public, until the command stops | yes, local disk | free | a tunnel binary; ngrok also wants an account |
render (free) |
public | no, and it stops after 15 minutes idle | free | a Render API key |
render --plan starter --volume-gb 1 |
public | yes, on the disk | $7/mo + $0.25/GB | a Render API key |
huggingface |
public | no — only what reached the backup Dataset | free to run | a write token, and a paid HF plan to create the Space |
digitalocean |
public | yes on the droplet; --volume-gb to outlive it |
$18/mo default, $6 smallest | a read/write token, pip install 'potato-annotation[deploy]' |
Two of those lines catch people out on their first deploy:
- A Docker Space needs a paid HuggingFace plan (PRO personally, Team or
Enterprise for an org). Restarting an existing Space does not. A free account
also runs at most three Spaces, and one over the limit sits
PAUSEDand never wakes for a visitor. - The $6 DigitalOcean droplet is too small.
s-1vcpu-1gbgets a warning in the plan: the image alone is ~840 MB and Potato's working set is numpy/pandas/scipy.s-2vcpu-2gbat $18 is the default for that reason.
Four questions decide it, and they are the researcher's to answer, not yours:
| Ask | Because |
|---|---|
| How long does the study run? | An afternoon is potato share. Weeks of annotators is deploy up. |
| Whose account and whose money? | Every public target except free Render bills someone. Do not create a billable resource on an assumption. |
| Would losing the annotations end the study, or just cost a morning? | This is the whole ephemeral/durable choice, and it is much cheaper to answer now. |
| Is anything in the data or the survey answers identifiable? | Decides whether a public URL with open sign-up is acceptable at all. |
Walking someone through a first deploy¶
Six steps. The first three cost nothing and touch no provider, so run all three before asking anyone to make an account.
1. Find out what is already configured.
potato deploy providers # targets, and which have a credential
potato deploy providers --verify # ask each one whether the token really works
Credentials are found without being configured for Potato: a HuggingFace token in
the huggingface_hub cache and a single-context ~/.config/doctl/config.yaml
are both picked up. Often the answer to "which provider" is "the one they are
already signed in to".
--verify matters because an expired, read-only or newline-terminated token
looks exactly like a good one until up is several resources deep.
2. Get a token, if the chosen target needs one.
Nothing is ever written to disk — the token is resolved per invocation from the environment, so a leaked project directory is not a leaked cloud account.
| Provider | Environment variable | Where the token comes from |
|---|---|---|
digitalocean |
DIGITALOCEAN_TOKEN |
https://cloud.digitalocean.com/account/api/tokens, read and write scope |
huggingface |
HF_TOKEN |
https://huggingface.co/settings/tokens, write access |
render |
RENDER_API_KEY |
https://dashboard.render.com/u/settings#api-keys |
tunnel |
NGROK_AUTHTOKEN |
only for the ngrok backend; cloudflared quick tunnels need no account |
local |
— | none |
POTATO_DEPLOY_TOKEN_<PROVIDER> overrides all of them, and --token overrides
that. potato deploy up without a token prints exactly this table for the one
provider you asked for, so you can also just run it and read the error.
3. Run the preflight and the dry run.
potato deploy check config.yaml --provider render
potato deploy up config.yaml --provider render --dry-run
check reports what the deployment exposes (next section). --dry-run is the
other half: it builds the bundle, then prints the provisioning steps, the URL the
task will end up at, and the monthly cost, without credentials and without
touching the provider.
5 step(s):
1. render.owners verify the API key with GET /v1/owners
2. render.service create a free web service from ghcr.io/davidjurgens/potato:latest
3. state.persist record the service id before anything else can fail
4. wait.deploy poll the deploy until it reports live
5. wait.http poll the service URL until it answers
Result URL: https://potato-full-study-skeleton.onrender.com
Estimated cost: free
WARNING: A free Render instance has no disk and stops after 15 minutes idle.
Everything written to it is lost when it stops, including annotations.
WARNING: Nothing is configured to carry the data off the instance. Supply
--hf-token for a HuggingFace Dataset backup, choose --plan starter
--volume-gb 1, or pass --demo if the annotations are genuinely disposable.
Show the researcher this output before spending their money. It is the only place the cost, the URL and the durability warnings appear together, and it reaches no provider.
Building the bundle writes it under .potato/bundle/<provider>/<name>/ in the
task directory. That is gitignored in Potato's own tree, but it is a real
directory of copied project files, so a dry run against four providers leaves
four copies behind.
4. Deploy.
potato deploy up config.yaml --provider render --plan starter --volume-gb 1
It prints the plan again and asks for confirmation; --yes skips the prompt and
is for scripts, not for the first time. --force proceeds despite preflight
errors and should never be used against a public host.
If it fails partway, the deployment is still recorded — state.persist is
deliberately step three or five, before anything expensive can fail — so
potato deploy status, logs and destroy all work on a half-built
deployment. Do not retry by hand-deleting resources in the provider's console.
5. Prove it works, with the steps under After it is up below. A bundle that omitted a side file fails on the host having worked locally, so the walk-it-as-an-annotator step is not optional just because it passed at home.
6. Hand it over. The URL, who can sign in, where the annotations live, how to pull them, and how to take it down. Say which of these steps you actually ran and which you only prepared.
The preflight¶
potato deploy check is the part of this worth learning. It never touches a
provider, it takes a second, and it reports what the deployment exposes:
$ potato deploy check config.yaml --provider render
WARNING [D003] Anyone who finds the URL can register and annotate.
-> Set user_config.allow_all_users: false and list your annotators under
user_config.users, or use authentication.method: oauth.
WARNING [D011] The render filesystem is ephemeral: annotations are lost when the
host restarts or redeploys.
-> Provide an HF token so a backup dataset can be configured, or pass
--demo to accept throwaway data.
note [D012] No secret_key is set, so one will be generated and injected as
POTATO_SECRET_KEY. Sessions survive restarts only because of it.
Exposure:
Reachable from the public internet: yes
Sign-in: open — anyone with the URL can create an account
Password required: yes
Admin access: via the generated admin API key
Preflight: 0 error(s), 2 warning(s)
PASS — safe to deploy
Exit codes: 0 for PASS, 2 for BLOCKED. Errors block; warnings do not.
The one that blocks most often:
ERROR [D002] debug is true, which disables admin authentication entirely:
validate_admin_api_key returns True unconditionally in debug mode, so
anyone reaching /admin has full control.
-> Remove `debug: true` from the config before deploying.
If you turned on debug to check the interface, take it out. It is not a
logging flag; it removes login and admin auth.
Run check against the provider you actually intend to use. local reports no
exposure and no durability problem, so a clean check with the default provider
tells you nothing about hosting it.
Add --private when the host will not be public (behind a VPN, on an internal
network) so the assessment is not arguing about exposure you have already
handled.
What ships¶
potato deploy build assembles a bundle and stops, which is the cheap way to see
what would be uploaded:
Bundle: ./bundle
20 files, 152.6KB
sha256 f70b22ad201b4e37
Two things to know about it:
- It copies the whole task directory. Screenshots in
shots/, a strayconfig_unsafe.yaml, notes, scratch data — all of it goes. Clean the directory before building, and look at the file list rather than the count. annotation_output/is stripped to an empty.gitkeep. Annotations you collected while testing do not ship. Verified by putting auser_state.jsonin there and rebuilding.
The bundle also carries the generated layouts/, so the host does not
regenerate them. Delete layouts/ and rebuild if you changed schemes and the
deployed page looks stale.
Secrets are not written into the bundle. The secret key and admin API key are
generated and injected as POTATO_SECRET_KEY and POTATO_ADMIN_API_KEY
environment variables at deploy time. Pass your own with --env KEY=VALUE and
--secret KEY=VALUE.
Before you expose anything¶
The preflight will say all of this, but decide it before you type up:
| Question | Where it is answered |
|---|---|
| Who can sign in? | user_config.allow_all_users: false plus user_config.users, or authentication.method: oauth. The default lets anyone with the URL create an account |
Is debug off? |
It must be. It disables admin auth outright |
| Do sessions survive a restart? | secret_key, or let deploy generate one |
| Where do annotations live if the host restarts? | Durable disk (digitalocean --volume-gb) or a backup Dataset (--hf-token) |
| Who holds the admin key? | Generated per deployment; potato deploy status and the provider's env |
| Is anything identifiable in the data or the survey answers? | export_include_phase_data defaults to false for a reason |
Getting the data back¶
potato deploy pull config.yaml --dest ./collected
Every provider supports pull. Do this before destroy — destroy refuses to
run without a prior successful pull unless you pass --force, which is a
deliberate guard rather than an annoyance. --keep-data on destroy keeps the
provider-side volume.
pull --allow-empty records a pull that returned nothing, for when you know the
task collected nothing and want destroy to proceed.
After it is up¶
Deploying is not verifying. The same standard as running-a-task.md applies, and
the failure modes are worse remotely because you cannot read the log by looking
sideways:
potato deploy status config.yaml. It should say provisioned and running.potato deploy logs config.yaml --lines 200— the startup log, with the same lines you grep locally:Loaded N training instances,Loaded N attention check items, any phase errors. A feature that loaded locally can fail on the host if a side file was excluded from the bundle.- Open the URL and walk the study as an annotator: register, consent, one item, navigate away and back.
- Run
potato deploy pullonce, early, and check the files are what you expect. Finding out that pull does not work after four weeks of annotation is the expensive version of this mistake.
Then tell the researcher the URL, who can sign in, where the annotations live, how to pull them, and how to take it down. A deployment nobody can pull from or destroy is worse than a local server.
What I have and have not verified¶
Verified against potato-annotation 2.7.0: the subcommands and their flags, the
provider list and credential detection, check output and exit codes for all
five providers, the debug block, and what build includes and strips. The
--dry-run plans, cost figures and provider warnings above are the real output
of potato deploy up --dry-run for each target, which reaches no provider; the
prices are the tables the planner prices from, not a quote from anyone's billing
page.
Not verified here: an actual up against a real provider, and therefore the
provisioning, DNS, TLS and pull behaviour of a live host. Provisioning costs
money and creates real resources, so do it deliberately and with the
researcher's knowledge — and say plainly in a handover which of these steps you
ran and which you only prepared.