Skip to content

Running Potato Locally or in the Cloud

Run Potato on your laptop with pip or Docker, share it for an afternoon, or put a study on AWS, Jetstream2, Hetzner, Fly or Railway with one command.

Potato runs on your own machine with two commands, and potato deploy puts the same config on a cloud host with one more. This page matches each common situation to a host and gives the command for it. The cloud targets below need Potato 2.10.0 or later, which added AWS, Heroku, Fly, Railway, Hetzner, Vultr, Linode and OpenStack to the DigitalOcean, Render and HuggingFace targets that 2.9 already had.

Choosing where to run a task

The right host depends on how long the task has to stay up and who pays for it. Costs in the table are the estimates Potato 2.10.0 prints for each target's default size, and --dry-run prints the estimate for the size you choose.

SituationCommandMonthly cost
Building or trying a taskpotato start config.yamlfree
A pilot with a few people for an afternoonpotato share config.yamlfree
A study, with an AWS accountpotato deploy up config.yaml --provider aws$12
A study at a US institution, with no budgetpotato deploy up config.yaml --provider openstack --cloud jetstream2free with an ACCESS allocation
A study, at the lowest pricepotato deploy up config.yaml --provider hetznerabout €6
A study, with no server to maintainpotato deploy up config.yaml --provider flyabout $6
Other people run copies in their own accountspotato deploy button config.yaml --target herokuset by the host
A server your institution already runsthe Docker image behind a reverse proxynone from Potato

On AWS, use Lightsail, which is what --provider aws creates. Its flat price includes the IPv4 address and a 60 GB disk, and it needs only lightsail:* permissions, so it works under a restricted IAM role. The EC2 target (aws-ec2) is for accounts where Lightsail is turned off.

Jetstream2 is an NSF-funded research cloud. With an ACCESS allocation it costs nothing, and each instance gets a DNS name, so annotators see an ordinary hostname with an ordinary certificate. The default instance uses 2 service units an hour, about 17,500 over a year, so destroy it when the study ends.

Running Potato on your own machine

Potato needs Python 3.9 or newer. Install it and start a task:

bash
pip install potato-annotation
potato start myproject/config.yaml -p 8000

The task is at http://localhost:8000, and nobody else can reach it. potato start reads the config and data files where they sit, so a restart picks up an edit, which makes it the command to use while you write a task. The Quick Start builds a first config, and Installation covers the optional extras and virtual environments.

The published Docker image needs no Python on the host. It holds Potato and its dependencies, and your project folder, with config.yaml and its data, mounts at /app:

bash
docker run -p 8000:7860 -v "$PWD/myproject:/app" ghcr.io/davidjurgens/potato:latest

The container serves on port 7860, which the command maps to 8000 on your machine. Production Setup covers the image's tags, its environment variables, and the file-ownership error that Linux hosts report.

potato share runs the task and puts it on a public HTTPS link for as long as the command runs. It needs a tunnel client, and uses cloudflared, Tailscale or ngrok, in that order, whichever is installed:

bash
brew install cloudflared
potato share myproject/config.yaml

The link stops working when you press Ctrl-C, when the laptop sleeps, or when the network changes, and the annotations stay on your own disk. Before it opens the tunnel, potato share prints who will be able to sign in and asks you to confirm, because the config's sign-in rules then apply to anyone with the link. Some university networks block trycloudflare.com links, and --backend tailscale avoids the block. Use potato share for a pilot or a lab meeting, and a cloud host for any task a participant might come back to the next day.

Deploying to the cloud with one command

potato deploy up takes the config you already have, creates the server, gets an HTTPS certificate, uploads the project, starts the task and prints the URL. Install the extra for your target, then look at the plan before running it:

bash
pip install 'potato-annotation[deploy]'            # most targets
pip install 'potato-annotation[deploy-aws]'        # the three AWS targets
pip install 'potato-annotation[deploy-openstack]'  # Jetstream2 and other OpenStack clouds
 
potato deploy up myproject/config.yaml --provider aws --dry-run
potato deploy up myproject/config.yaml --provider aws

--dry-run needs no account. It prints every resource the deploy would create, the monthly cost, and any setting in your config that is risky on a public server. Without --dry-run, Potato shows the same plan and waits for you to confirm before it creates anything. Running up again on the same config updates the existing deployment, and after the first up, status, logs, pull and destroy need no --provider.

The thirteen cloud targets

Potato 2.10.0 deploys to thirteen cloud targets. They differ most in whether the disk survives a restart, which decides whether a deploy needs the backup described in the next section.

--providerWhat it createsMonthly costDisk survives a restart
awsan AWS Lightsail VM, 2 GB$12yes
aws-ec2an EC2 t4g.small VM with an Elastic IPabout $18yes
aws-ecsa container on ECS Express Modeabout $45-70no, needs --backup
openstacka VM on Jetstream2 or another OpenStack cloudfree with an allocationyes
hetznera Hetzner Cloud VM, 2 vCPU and 4 GBabout €6yes
vultra Vultr VM, 1 vCPU and 2 GB$10yes
linodean Akamai Linode VM, 1 vCPU and 2 GB$12yes
digitaloceana DigitalOcean Droplet, 2 vCPU and 2 GB$18yes
flya Fly.io Machine with a 1 GB volumeabout $6yes
railwaya Railway service with a volumebilled by use, usually $10-20yes
rendera Render web servicefree, or $7 plus disk on starterwith a paid disk only
herokuone Heroku Basic dyno$7no, needs --backup
huggingfacea Docker Space and a private dataset for the annotationsa PRO plan ($9) or a Team planno, backed up to the dataset

The seven VM targets (aws, aws-ec2, openstack, hetzner, vultr, linode and digitalocean) are set up the same way. Each gets a deploy key generated for that deployment, a firewall that opens only ports 22, 80 and 443, Caddy with a Let's Encrypt certificate, and Potato as a systemd service, and potato deploy logs and pull work on all of them. Fly, Railway, Render and ECS Express run the published image and download your project when the container starts. Railway, Render and ECS Express fetch it from the backup storage, so all three need --backup, and a Railway or Render service with a disk needs it too. On Fly, a project under 512 KB travels inside the Machine's configuration and needs no storage.

Google Cloud Run, Azure Container Apps and AWS App Runner are not targets. The storage that Cloud Run and Azure Container Apps offer cannot hold a SQLite database safely, and App Runner closed to new customers on 30 April 2026. On Azure, a VM running the Docker image works.

Backups for hosts that wipe their disk

On Heroku, ECS Express, Render's free tier and HuggingFace Spaces, the disk is wiped when the server restarts. --backup copies the annotation output and snapshots of the project databases to a HuggingFace dataset or an S3 bucket every five minutes, and restores them when a server starts with an empty disk:

bash
potato deploy up myproject/config.yaml --provider heroku --backup hf --hf-token hf_...
potato deploy up myproject/config.yaml --provider heroku --backup s3 --s3-bucket my-bucket

Heroku and ECS Express refuse to deploy without --backup, unless --demo declares the annotations disposable. The restore brings back the account list along with the annotations, so annotators sign in with the same passwords and continue where they stopped, and it never overwrites annotations already on the disk. --s3-endpoint sends the S3 backup to Cloudflare R2, Backblaze B2, MinIO or a university object store. The backup works on the VM targets too, where it keeps a second copy of the data off the server.

Outside potato deploy, a backup block in the config does the same job on a server you run yourself. Install the hosting extra, which provides the HuggingFace and S3 clients, set HF_TOKEN in the server's environment, and add this block to an existing config:

yaml
backup:
  schedule_minutes: 5
  restore_on_boot: true
  sinks:
    - type: huggingface
      repo_id: lab/pilot-annotations

Pulling annotations before destroy

potato deploy pull downloads everything a deployment has collected into a timestamped directory and checks what arrived, and potato deploy destroy removes the server. Run them in that order:

bash
potato deploy pull myproject/config.yaml
potato deploy destroy myproject/config.yaml

destroy refuses to remove a deployment that has never been pulled, and a pull that returns zero files does not count. The pull copies project.sqlite through SQLite's backup command rather than as a file, because a file copy of a database in WAL mode can be missing recent work. On Fly and Railway, destroying the app deletes its volume, so the pull is the only copy unless you also ran a backup.

Deploy buttons for other people's accounts

potato deploy button writes the files a hosting platform reads to offer a one-click deploy from your git repository, and prints the README badge. Use it when collaborators, students or another lab should run your task in their own accounts:

bash
potato deploy button studies/pilot/config.yaml --target heroku \
    --backup hf --hf-backup-repo lab/pilot-annotations

The targets are heroku, render, aws and railway. The aws target writes a CloudFormation "Launch Stack" template that creates a Lightsail instance, and for railway the command prints the steps, because Railway publishes templates from its dashboard. Your config.yaml is left unchanged, and the command writes a copy beside it, potato.deploy.yaml, with deployment settings applied. No secrets go into the repository. Heroku, Render and the AWS template generate the session and admin keys at deploy time. The Heroku, Render and AWS buttons require --backup, and the person deploying supplies its credentials.

Further reading

Each target has its own page in the Potato documentation:

On this site, Production Setup covers running Potato under gunicorn and Docker on a server you manage, and Reverse Proxy covers serving it under a URL path prefix.