# Running Potato Locally or in the Cloud

Source: https://www.potatoannotator.com/docs/deployment/quick-deploy

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.

| Situation | Command | Monthly cost |
|---|---|---|
| Building or trying a task | `potato start config.yaml` | free |
| A pilot with a few people for an afternoon | `potato share config.yaml` | free |
| A study, with an AWS account | `potato deploy up config.yaml --provider aws` | $12 |
| A study at a US institution, with no budget | `potato deploy up config.yaml --provider openstack --cloud jetstream2` | free with an ACCESS allocation |
| A study, at the lowest price | `potato deploy up config.yaml --provider hetzner` | about €6 |
| A study, with no server to maintain | `potato deploy up config.yaml --provider fly` | about $6 |
| Other people run copies in their own accounts | `potato deploy button config.yaml --target heroku` | set by the host |
| A server your institution already runs | the Docker image behind a reverse proxy | none 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](https://jetstream-cloud.org) is an NSF-funded research cloud. With an [ACCESS](https://access-ci.org) 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](/docs/getting-started/quick-start) builds a first config, and [Installation](/docs/getting-started/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](/docs/deployment/production-setup#docker-deployment) covers the image's tags, its environment variables, and the file-ownership error that Linux hosts report.

### A temporary public link with `potato share`

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

| `--provider` | What it creates | Monthly cost | Disk survives a restart |
|---|---|---|---|
| `aws` | an AWS Lightsail VM, 2 GB | $12 | yes |
| `aws-ec2` | an EC2 `t4g.small` VM with an Elastic IP | about $18 | yes |
| `aws-ecs` | a container on ECS Express Mode | about $45-70 | no, needs `--backup` |
| `openstack` | a VM on Jetstream2 or another OpenStack cloud | free with an allocation | yes |
| `hetzner` | a Hetzner Cloud VM, 2 vCPU and 4 GB | about €6 | yes |
| `vultr` | a Vultr VM, 1 vCPU and 2 GB | $10 | yes |
| `linode` | an Akamai Linode VM, 1 vCPU and 2 GB | $12 | yes |
| `digitalocean` | a DigitalOcean Droplet, 2 vCPU and 2 GB | $18 | yes |
| `fly` | a Fly.io Machine with a 1 GB volume | about $6 | yes |
| `railway` | a Railway service with a volume | billed by use, usually $10-20 | yes |
| `render` | a Render web service | free, or $7 plus disk on `starter` | with a paid disk only |
| `heroku` | one Heroku Basic dyno | $7 | no, needs `--backup` |
| `huggingface` | a Docker Space and a private dataset for the annotations | a PRO plan ($9) or a Team plan | no, 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:

- [Installing and running Potato](https://github.com/davidjurgens/potato/blob/master/docs/deployment/installation.md) and [Deploying a task](https://github.com/davidjurgens/potato/blob/master/docs/deployment/one-command-deploy.md), the full `potato deploy` lifecycle
- [AWS](https://github.com/davidjurgens/potato/blob/master/docs/deployment/deploy-aws.md): Lightsail, EC2 and ECS Express, with the IAM permissions each needs
- [Jetstream2 and OpenStack](https://github.com/davidjurgens/potato/blob/master/docs/deployment/deploy-openstack.md), including how to request an allocation
- [Hetzner, Vultr and Linode](https://github.com/davidjurgens/potato/blob/master/docs/deployment/deploy-vps.md) and [DigitalOcean](https://github.com/davidjurgens/potato/blob/master/docs/deployment/deploy-digitalocean.md)
- [Fly.io](https://github.com/davidjurgens/potato/blob/master/docs/deployment/deploy-fly.md), [Railway](https://github.com/davidjurgens/potato/blob/master/docs/deployment/deploy-railway.md), [Render](https://github.com/davidjurgens/potato/blob/master/docs/deployment/deploy-render.md), [Heroku](https://github.com/davidjurgens/potato/blob/master/docs/deployment/deploy-heroku.md) and [HuggingFace Spaces](https://github.com/davidjurgens/potato/blob/master/docs/deployment/deploy-huggingface.md)
- [Backups](https://github.com/davidjurgens/potato/blob/master/docs/deployment/deploy-backups.md), [Getting annotations back](https://github.com/davidjurgens/potato/blob/master/docs/deployment/deploy-pull.md) and [Deploy buttons](https://github.com/davidjurgens/potato/blob/master/docs/deployment/deploy-buttons.md)
- [Sharing a task on a temporary URL](https://github.com/davidjurgens/potato/blob/master/docs/deployment/deploy-share.md)

On this site, [Production Setup](/docs/deployment/production-setup) covers running Potato under gunicorn and Docker on a server you manage, and [Reverse Proxy](/docs/deployment/reverse-proxy) covers serving it under a URL path prefix.
