Deploying a Task¶
Putting a task somewhere annotators can reach it. It ships with the potato-skill skill:
/plugin marketplace add davidjurgens/potato-skill
/plugin install potato-skill@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 … |
| Other labs running their own copy | potato deploy button config.yaml --target … |
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.
potato deploy up --provider tunnel exits 2 with a REFUSED: line telling you
to run potato share instead. The tunnel target runs only under share.
Hosted deployment¶
potato deploy providers # what targets exist, which have credentials
potato deploy check config.yaml --provider aws # preflight, changes nothing
potato deploy build config.yaml --out ./bundle # assemble, stop
potato deploy up config.yaml --provider aws --dry-run # the plan and the cost
potato deploy up config.yaml --provider aws # 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 lifecycle is the same on every target, with two gaps: potato deploy logs
does not work on fly (use fly logs -a <app>), and aws-ecs has logs but no
SSH, because there is no server to log in to.
The host runs the image for the Potato version you have installed: on 2.11.1,
up --dry-run plans docker.pull ghcr.io/davidjurgens/potato:2.11.1. Up to
2.10.3 the default was :latest, so two ups of one study could run different
releases; on those versions pass --image ghcr.io/davidjurgens/potato:2.10.3,
or whichever version you tested the study on. --image picks any other tag.
The fifteen targets¶
potato deploy providers on 2.10.3:
aws AWS Lightsail: one VM, flat $12/mo for 2 GB with IPv4 and disk; the recommended AWS target
aws-ec2 AWS EC2: a t4g.small VM with an Elastic IP, about $18/mo with its root disk
aws-ecs AWS ECS Express Mode: managed HTTPS, no server, ~$45-70/mo; ephemeral disk, so it needs --backup
digitalocean supports pull
fly Fly.io: one Machine + volume on <app>.fly.dev, about $6/mo
heroku Heroku container dyno from $7/mo; ephemeral disk, so it needs --backup (sustaining mode since 2026-02)
hetzner Hetzner Cloud: a 4 GB VM for about €6/mo, the cheapest durable target
huggingface ephemeral filesystem, supports pull
linode Akamai/Linode: a 2 GB VM for $12/mo
local local only, supports pull
openstack OpenStack (Jetstream2, campus clouds): free with an allocation; real hostname on Jetstream2
railway Railway: image + volume on *.up.railway.app, usage-billed (~$10-20/mo)
render ephemeral filesystem, supports pull
tunnel temporary public URL; stops when `potato share` does
vultr Vultr: a 2 GB VPS for $10/mo
Quote a researcher the price --dry-run prints. It comes from the planner's
price table and includes the disk and the IPv4 address.
Three extras pull in the provider SDKs:
pip install 'potato-annotation[deploy]' # every target not named below
pip install 'potato-annotation[deploy-aws]' # aws, aws-ec2, aws-ecs (boto3)
pip install 'potato-annotation[deploy-openstack]' # openstack (openstacksdk)
check blocks a target whose extra is missing, and names the one to install:
ERROR [D021] the openstack target needs openstack, which is not installed
-> pip install 'potato-annotation[deploy-openstack]'
Choosing a target¶
The targets differ on who can reach the task, whether the annotations survive a
restart, and what it costs. The rest is provisioning detail. Costs are the
--dry-run figures at the default size.
| Target | What it is | Annotations survive a restart | Cost | Also needs |
|---|---|---|---|---|
local |
Docker on this machine, 127.0.0.1 only |
yes | free | Docker running |
tunnel |
public until the command stops | yes, local disk | free | a tunnel binary |
aws |
Lightsail VM | yes, on its disk; --volume-gb to outlive it |
$12/mo | the deploy-aws extra |
aws-ec2 |
EC2 t4g.small |
yes, as above | $18.31/mo | deploy-aws; --subnet if the region has no default VPC |
aws-ecs |
ECS Express, a managed container | no — only what the backup holds | $47/mo | deploy-aws, and --backup |
hetzner |
VM, 2 vCPU / 4 GB | yes | €5.99/mo, billed in euros | — |
vultr |
VM, 1 vCPU / 2 GB | yes | $10/mo | your IP on the API key's access list |
linode |
VM, 1 vCPU / 2 GB | yes | $12/mo | — |
digitalocean |
VM, 2 vCPU / 2 GB | yes | $18/mo default, $6 smallest | — |
openstack |
VM on Jetstream2 or a campus cloud | yes | your allocation's service units | deploy-openstack, a clouds.yaml |
fly |
one Machine and a 1 GB volume | yes, on the volume | $5.85/mo | --backup if the project is over 512 KB |
railway |
one service and a volume | yes, on the volume | usage-billed, $10–20/mo | --backup, always |
heroku |
one Basic dyno | no; the dyno's disk is wiped at least daily | $7/mo | --backup, or --demo |
render (free) |
web service; stops after 15 minutes idle | no | free | --backup, always |
render --plan starter --volume-gb 1 |
as above, with a disk | yes, on the disk | $7/mo + $0.25/GB | --backup, always |
huggingface |
a Docker Space | no; the backup is always on | free to run | a write token, and a paid HF plan to create the Space |
The lines that catch people out:
render,railwayandaws-ecsrefuseupwithout--backup, and--demodoes not change that. These hosts run the published image and have no way to receive files, so the project is uploaded to the backup's storage and downloaded when the container starts. No backup means nowhere to put the project.flyrefuses the same way once the bundle is over 512 KB. Heroku builds its own image, so--demoalone is accepted there.- A free Render instance takes no disk.
--volume-gbwithout--plan starteror higher is refused. Without a disk the backup is the only copy of the annotations. - 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. - A VM without
--domaingets a certificate for its IP address, and those are valid for about six days rather than 90. They renew on their own;potato deploy statusreports a renewal that fails.openstack --cloud jetstream2is the exception, because Jetstream2 gives every instance a DNS name. flyandrailwaydelete the volume ondestroy.--keep-datacannot keep a Fly volume. Pull first.
Where each one fits, from Potato's own install guide: an AWS account →
aws; a US academic with an ACCESS allocation → openstack --cloud jetstream2,
which costs no money; the cheapest durable VM → hetzner; nobody to look after
a server → fly or railway. Heroku has been in sustaining-engineering mode
since February 2026. It works, but use it only if the lab already has an
account.
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, and openstack draws down an allocation. 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. |
Backups and restore¶
Any target can mirror what Potato collects to storage the researcher owns. On
aws-ecs, heroku, render and huggingface that copy is the only one that
outlives a restart. On a VM it is a second copy for no extra cost.
potato deploy up config.yaml --provider heroku --backup hf --hf-token $HF_TOKEN
potato deploy up config.yaml --provider heroku --backup s3 --s3-bucket my-bucket
potato deploy up config.yaml --provider fly --backup s3 --s3-bucket my-bucket \
--s3-endpoint https://<account>.r2.cloudflarestorage.com # R2, B2, MinIO
potato deploy up config.yaml --provider railway --backup hf,s3 --hf-token $HF_TOKEN \
--s3-bucket my-bucket --backup-minutes 10
| Flag | Default |
|---|---|
--backup hf\|s3\|hf,s3 |
none. Without it, --hf-token alone means --backup hf and --s3-bucket alone means --backup s3 |
--hf-token |
HF_TOKEN. Needs write access |
--hf-backup-repo |
<account>/<name>-annotations, created private |
--s3-bucket |
none; the bucket must already exist |
--s3-prefix |
potato/<name> |
--s3-region, --s3-endpoint |
boto3's defaults; the endpoint is for S3-compatible stores |
--backup-minutes |
5 |
S3 credentials come from POTATO_S3_ACCESS_KEY_ID and
POTATO_S3_SECRET_ACCESS_KEY and end up in the server's environment, where
anyone with a shell on the host can read them. Use a key that reaches only this
bucket.
What it copies: the annotation output directory file by file (every
user_state.json, plus user_config.json, which holds the accounts), and
snapshots of project.sqlite and datasets.sqlite taken with SQLite's backup
API rather than copied as live WAL files. Only changed files are uploaded each
cycle.
Restore is automatic. When the server starts and its output directory holds
no user_state.json, it downloads the latest backup before it loads anything.
It never overwrites: a disk that already holds annotations is treated as the
authoritative copy. The log says which happened:
Restored project.sqlite from backup
RESTORED 5 file(s) from the s3://probe-bucket/potato/probe backup into an empty task. This is expected after a restart on a host without a persistent disk.
Loaded user data for 1 users
That is from a local run against an S3-compatible server: three items annotated, the task directory deleted, the server started again. The annotator logged in with the original password and landed on the fourth item.
potato deploy up writes the backup: block into the bundled config, never
into yours. On a server you run yourself, add it by hand:
backup:
schedule_minutes: 5
restore_on_boot: true
sinks:
- type: huggingface
repo_id: lab/pilot-annotations
- type: s3
bucket: my-bucket
prefix: potato/pilot
region: us-east-1
The older huggingface_backup: block still works and is read as one
HuggingFace sink, with restore_on_boot off, so a config that relied on it
backs up but does not restore. Move it to backup: if the host's disk does not
last.
On an S3-only backup, the project link the container downloads at start is a
presigned URL valid for at most seven days. A host with a disk downloads it once
per deploy. A host without one (render free, aws-ecs) downloads it on every
restart, so a week later a restart fails until you run up again. The plan
warns about this; use --backup hf on those hosts.
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, a single-context ~/.config/doctl/config.yaml, the
key heroku login writes to ~/.netrc, an AWS profile or SSO session, and a
clouds.yaml are all 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 | Credential | Notes |
|---|---|---|
aws, aws-ec2, aws-ecs |
the AWS chain: AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, AWS_PROFILE, ~/.aws/, SSO |
--aws-profile lab picks a profile and is remembered for status, pull and destroy; --region defaults to us-east-1 |
digitalocean |
DIGITALOCEAN_TOKEN |
read and write scope |
fly |
FLY_API_TOKEN |
fly tokens create org, or a personal token |
heroku |
HEROKU_API_KEY, or heroku login |
|
hetzner |
HCLOUD_TOKEN |
project token with read and write |
huggingface |
HF_TOKEN |
write access |
linode |
LINODE_TOKEN |
read/write on Linodes, Firewalls and Volumes |
openstack |
clouds.yaml plus --cloud <entry>, or OS_CLOUD |
Jetstream2: an application credential from Horizon, saved to ~/.config/openstack/clouds.yaml |
railway |
RAILWAY_API_TOKEN or RAILWAY_TOKEN |
|
render |
RENDER_API_KEY |
|
tunnel |
NGROK_AUTHTOKEN |
ngrok only; cloudflared quick tunnels need no account |
vultr |
VULTR_API_KEY |
every request returns 401 until your IP is on the key's access list |
local |
— |
For the token-based providers, POTATO_DEPLOY_TOKEN_<PROVIDER> overrides the
variables above, and --token overrides that. potato
deploy up without a token prints where to get one for the 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 aws
potato deploy up config.yaml --provider aws --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.
14 step(s):
1. aws.identity confirm the credentials with sts:GetCallerIdentity
2. ssh.keygen generate an ed25519 deploy key, delivered by cloud-init
3. lightsail.bundles check that bundle small_3_0 is still sold
4. lightsail.instance create potato-deploy-probe (small_3_0, ubuntu_24_04) in us-east-1a
5. state.persist record the instance name before anything else can fail
6. wait.active poll until the instance is running
7. lightsail.static_ip allocate potato-deploy-probe-ip and attach it, so the address and its certificate survive a stop/start
8. lightsail.ports open 22/80/443 only; never 8000
9. wait.ssh poll until the host accepts an SSH connection
10. wait.cloud_init wait for docker install and image pull to finish
11. ssh.upload upload the bundle to /opt/potato/app
12. ssh.env write /opt/potato/potato.env at mode 0600
13. ssh.start systemctl start potato and caddy
14. wait.http poll https://<instance-ipv4>/health
Result URL: https://<instance-ipv4>
Estimated cost: $12.00/month
WARNING: No --domain, so TLS uses a Let's Encrypt certificate for the instance's IP address. These are valid ~6 days rather than 90, so check `potato deploy status` if the study runs unattended.
WARNING: No --volume-gb. Annotations live on the instance's own disk, which is destroyed with the instance. Pull before you destroy.
When up would refuse, the plan is followed by a REFUSED: line naming the
reason and the flags that fix it, and the command exits 2 instead of 0. The dry run and the real up run the same check, before
any provider is called:
2. bundle.publish upload the project tarball to (nowhere; see REFUSED below); the container fetches it at start
...
REFUSED: Render runs the published image and fetches your project into it at start, so the project has to be uploaded somewhere first. It goes to the backup's storage, so --demo is not an option here: pass --backup hf --hf-token <token> (recommended: the link never expires) or --backup s3 --s3-bucket <bucket>. On the free plan the backup is also the only copy of the annotations, since the instance stops after 15 minutes idle and loses its filesystem.
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 aws --volume-gb 10
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 comes
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.
Flags by target¶
potato deploy up --help lists them all on one screen without saying which
target reads which. Grouped:
| Flag | Read by |
|---|---|
--size |
every target with a machine size: a Lightsail bundle (small_3_0), an EC2 type (t4g.small; t3.small for x86), a Heroku dyno (basic, standard-2x), a Fly memory size in MB, a server type, or an OpenStack flavor |
--volume-gb |
the VM targets, fly (default 1) and render --plan starter. Puts the task on a disk that outlives the instance |
--acme-email |
the VM targets: the Let's Encrypt contact. Defaults to git config user.email |
--domain |
the VM targets: point an A record at the IP first, and you get a 90-day certificate |
--aws-profile, --region |
the three AWS targets (--region also picks a Fly or VM region) |
--subnet |
aws-ec2, when the region has no default VPC |
--cloud, --network, --os-image |
openstack |
--heroku-registry |
heroku: build locally with Docker and push to Heroku's registry, for when the build on Heroku fails |
--plan |
render (free, starter, standard) |
--owner |
huggingface and fly (the organization) |
The VM targets are aws, aws-ec2, digitalocean, hetzner, vultr,
linode and openstack. All seven provision the same way: a deploy key made
for that deployment alone, a firewall open on 22/80/443 and never 8000, Docker,
Caddy in front, and Potato as a systemd service. logs, pull over SSH and
--volume-gb work on all of them.
Per-target traps:
hetzner: the defaultcxtypes are sold only in the European locations. Inashorhil, pass acpxtype with--size, or Potato stops before creating anything and lists where your type is sold.openstack:--cloud jetstream2is a preset for the flavor (m3.small), the image (Featured-Ubuntu24) and a real hostname,potato-<name>.<allocation>.projects.jetstream-cloud.org. Any otherclouds.yamlentry getsm1.small,Ubuntu 24.04and an IP certificate. Anm3.smallcosts 2 service units an hour, about 17,500 a year if nobody destroys it. A project without an auto-allocated network needs--network.aws-ecs: Potato switches the service to stop the old task before starting the new one, so two tasks never restore and back up the same study at once. A redeploy takes the task offline for a minute or two. If AWS changes the setting back, the nextuprefuses;destroyandupagain, pulling first. The two IAM roles it creates are shared and left in place bydestroy.heroku:--size ecosleeps after 30 minutes idle and restores from the backup on every wake.R14 (Memory quota exceeded)in the logs means--size standard-2x.
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 with at least one entry under authentication.providers.
WARNING [D011] The render filesystem is ephemeral: annotations are lost when the host restarts or redeploys.
-> Pass `potato deploy up` --backup hf (with --hf-token) or --backup s3 --s3-bucket <name> so the data is mirrored off the host and restored after a restart, or add a backup: block to the config.
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 (injected as POTATO_ADMIN_API_KEY, not written into the bundle)
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. With debug_phase it signs every visitor in as
one shared debug_user and skips them to that phase, and it opens the
admin pages without a key to requests from the server's own machine.
It is for a server on a developer's machine.
-> 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 can sign every visitor in as the same user, and it lets the
host's own machine into the admin pages without a key.
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 size matters on fly: a bundle up to 512 KB travels inside the Machine's
configuration and needs no storage, and a larger one needs --backup.
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.
Deploy buttons¶
potato deploy button writes the files a host reads to offer a one-click deploy
of the task from its git repository, and prints the README badge. It is for
other people running their own copy in their own accounts, not for hosting the
study you run.
potato deploy button config.yaml --target heroku --backup hf \
--hf-backup-repo lab/pilot-annotations --dry-run # print the files, write nothing
--target |
Writes | Needs |
|---|---|---|
heroku |
app.json, heroku.yml, Dockerfile.potato |
--backup |
render |
render.yaml, Dockerfile.potato |
--backup |
aws |
potato-lightsail.cfn.yaml, a CloudFormation stack for Lightsail |
--backup, a public repository, and the template uploaded to S3 and passed back as --template-url |
railway |
nothing; it prints the steps for publishing a template from the Railway dashboard | — |
Without --backup, the heroku, render and aws targets exit 1 before writing
anything. All of them also write potato.deploy.yaml beside your config, a copy
with debug: false, persist_sessions: true and the backup block, and
.dockerignore if the repository has none. Your config.yaml is not changed.
No secrets are written.
Read the warnings it prints before you commit. It lists anything that should
not reach a public repository and is not gitignored (.potato, which holds the
admin key, plus annotation_output and project.sqlite). With no origin
remote, the badge carries a <repo-url> placeholder for you to fill in.
Existing files are kept unless you pass --force.
Behind a reverse proxy¶
"We'll put it at example.edu/annotate" is a request for a path prefix, and Flask
has to be told about it or every asset URL comes out rooted at /. Two
switches, both environment variables rather than config keys:
# the proxy sets headers (nginx: proxy_set_header X-Forwarded-Prefix /annotate)
POTATO_PROXY_FIX=1 potato start config.yaml
# the proxy cannot; name the prefix yourself
POTATO_URL_PREFIX=/annotate potato start config.yaml
Both converge on the WSGI SCRIPT_NAME. POTATO_PROXY_FIX also reads
POTATO_PROXY_FIX_X_FOR, _X_PROTO, _X_HOST and _X_PREFIX, for saying how
many proxies deep the server sits. Potato's own
docs/deployment/reverse-proxy.md is the upstream page.
Driven on 2.8.2-26 behind a proxy mounting a task at /annotate: under either
switch the annotation page renders styled and url_prefix reaches the page as
/annotate. Walk the whole sign-in on the proxied URL before you hand it
out, and do it in a browser: a prefix that reaches the stylesheet has not
necessarily reached every form on the page.
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 can sign everyone in as one user and opens admin to the host without a key |
| Do sessions survive a restart? | secret_key, or let deploy generate one |
| Where do annotations live if the host restarts? | A volume (--volume-gb), or --backup with restore on boot |
| 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 where the provider allows it; on fly it cannot.
destroy never touches the backup dataset or bucket.
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(fly logs -a <app>on Fly) — the startup log, with the same lines you grep locally:Loaded N training instances,Loaded N attention check items, any phase errors, and on a host with a backup,Backup: … every N minute(s). 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. With a backup, open the dataset or bucket too and check auser_state.jsonhas arrived.
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¶
Run on 2.11.1 (cf0873a0; the retagged 68880e8c changes only wording), from a checkout, with Docker stopped: up
--dry-run on local, render and railway plans the 2.11.1 image, which is
published on ghcr.io; check with debug: true blocks on D002 with exit 2,
quoted above; check on openstack without its SDK still blocks on D021.
Run on 2.11.0 (eb284819), from a checkout, with Docker stopped:
up --dry-runon the fourteen targets other thantunnel. The three whose plans name the image,local,renderandrailway, plan the 2.11.0 image and none planslatest. The image tag2.11.0is published on ghcr.io;checkon the eleven VM and container targets.openstack, whose SDK was not installed, blocked on D021 with exit 2 andok: falsein--json; the others passed.
Run on 2.10.3, installed from PyPI, with --image
ghcr.io/davidjurgens/potato:2.10.3:
- the whole lifecycle on
local:up,status(healthy True),logs, a three-item annotator walk,pull(one annotator,project.sqlite, and the three answers inuser_state.json) anddestroy, which removed the container; - the provider list and credential detection, from
potato deploy providers.
Run on the re-cut 2.10.2 tag (1a9235b1). The only deploy change between it
and 2.10.3 is the button command's HuggingFace account lookup, which was run
again on the 2.10.3 release candidate:
--dry-runfor all fifteen targets, which reaches no provider. That covers the plans, the costs above, the jetstream2 preset's flavor and hostname, and theREFUSED:exit 2 for heroku, aws-ecs, render and railway without a backup, for fly with a 1 MB bundle, for render with--volume-gbon the free plan, and for tunnel;checkoutput and exit codes, thedebugblock, and whatbuildincludes and strips;- the button command for all four targets with
--dry-run, including its exit 1 without--backup.
Run on 2.10.1 (6b281231) and not repeated:
- the subcommands and flags, from
--help.tests/test_deploy_flags.pystill checks every flag this page names against the parser; - backup and restore on boot, end to end, with an S3 sink pointed at a local S3-compatible server. That run is quoted in Backups and restore.
Read from the code but not run: that a real up reaches the same refusal check
as the dry run, before calling the provider (cmd_up checks it ahead of the
--dry-run branch, after the credential check); that the legacy
huggingface_backup: block maps to a sink with restore_on_boot off; and that
every other target takes the same default image as the three dry runs that
print it (they all read DEFAULT_IMAGE in potato/deploy/image.py). The prices are the planner's own tables, not a quote from anyone's
billing page.
Not verified here: an actual up against any cloud provider, and therefore the
provisioning, DNS, TLS, the HuggingFace sink and pull behaviour of a live host.
Provisioning costs money and creates real resources, so do it deliberately and
with the researcher's knowledge. In a handover, say which of these steps you
ran and which you only prepared.