> ## Documentation Index
> Fetch the complete documentation index at: https://docs.freesolo.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-hosting

> Run your own Flash control plane against your own GPU accounts.

Flash can run as your own control plane against your own GPU accounts, with no
Freesolo backend involved. You supply the GPU credentials, you hold the auth key,
and you choose which providers to use. This is an operator deployment, not a
one-command install: it points the whole training path - SFT, GRPO, and OPD - at
hardware you pay for directly.

<Note>
  Self-hosting is for teams who want to run on their own GPU accounts. The
  managed platform at [platform.freesolo.co](https://platform.freesolo.co) needs
  none of this: [`flash login`](/reference/cli#auth-identity) and [`flash
      train`](/guides/training) are the whole setup.
</Note>

## What you need

Three settings and at least one GPU account. `flash-server` refuses to start
without all four.

| Variable                | Purpose                                                                                                                                                                |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| One GPU provider key    | `RUNPOD_API_KEY`, `LAMBDA_API_KEY`, **or** `VAST_API_KEY`. One is enough.                                                                                              |
| `HF_TOKEN`              | A Hugging Face token with **write** access. Flash streams code, checkpoints, and adapters through Hugging Face dataset repos, so every run needs it on every provider. |
| `FLASH_HF_NAMESPACE`    | The Hugging Face user or org those repos are created under. Required with `FLASH_STANDALONE=1`: it defaults to Freesolo's namespace, which your token cannot write to. |
| `FREESOLO_INTERNAL_KEY` | The key your clients present to your plane. Generate it yourself.                                                                                                      |

Those four bring up a plane that runs SFT and GRPO. **OPD needs two more**, and
without them an OPD submission fails before GPU allocation: `PARASAIL_API_KEY`
for the managed teacher, and a worker-reachable HTTPS `FLASH_PUBLIC_URL`. See
[Optional pieces](#optional-pieces).

`FLASH_HF_NAMESPACE` is **one** id segment - `your-username`, not
`your-username/some-repo`. Flash appends the repo name itself, so the
`owner/repo` spelling would build a three-segment id that Hugging Face rejects.
Startup validates the id it will actually create and refuses the `owner/repo`
form by name, rather than letting it pass and fail at your first submit.

Everything else is optional.

## Quickstart

On the plane host, write the operator config to a file so both the server and
your shell read the same values:

```bash theme={null}
pip install 'freesolo-flash[server]'   # the base install is client-only

cat > flash-plane.env <<'EOF'
FLASH_STANDALONE=1
HF_TOKEN=hf_...
FLASH_HF_NAMESPACE=your-hf-username   # where run artifacts are created
RUNPOD_API_KEY=...                    # or LAMBDA_API_KEY, or VAST_API_KEY
EOF

echo "FREESOLO_INTERNAL_KEY=$(openssl rand -hex 32)" >> flash-plane.env
chmod 600 flash-plane.env             # it holds your plane's root credential

set -a && . ./flash-plane.env && set +a
flash-server --host 127.0.0.1 --port 8080
```

<Warning>
  The `server` extra pulls in `runpod-flash`, which installs its own `flash`
  command. Whichever package was installed last owns the name, so on a plane host
  `flash` can be RunPod's CLI - and `flash runs cancel <id>` would then exit 0
  without cancelling anything. Flash also installs **`flash-cli`**, a name nothing
  else claims. Use it on any host with the `server` or `dev` extra, or run
  `python -m flash.cli`. This page writes `flash` for client commands; substitute
  `flash-cli` whenever you run one on the plane host itself.
</Warning>

`flash-server` runs in the foreground, so leave it running and open a second
terminal for the client. Read the key back from the same file rather than
retyping it:

```bash theme={null}
FREESOLO_INTERNAL_KEY=$(grep '^FREESOLO_INTERNAL_KEY=' flash-plane.env | cut -d= -f2-)

flash-cli login --api-url http://127.0.0.1:8080 --api-key "$FREESOLO_INTERNAL_KEY"
flash-cli train run.toml
```

<Warning>
  `FREESOLO_INTERNAL_KEY` is the plane's root credential, and the quickstart
  above binds to `127.0.0.1` so it never leaves the host. Before you move the
  client to another machine, put the connection behind **TLS or an SSH/VPN
  tunnel** - `--host 0.0.0.0` with a plain `http://` URL sends that key in
  cleartext to anyone who can observe the network, and it grants full control of
  your runs, logs, and GPU spending. `flash login` warns when you give it a
  non-loopback `http://` URL - for `--freesolo-url` as well as `--api-url`,
  since the key is sent to both (Flash 1.1.40).
</Warning>

`flash-server` never terminates TLS itself. For anything but loopback, put a
TLS-terminating reverse proxy (nginx, Caddy, a cloud load balancer) in front of
it and point `--api-url` at the proxy's `https://` address rather than the
plane's own port.

Because `--api-url` points at your own plane, `flash login` stores the key and
checks it against that plane. It does **not** send it to `api.freesolo.co`: your
plane authenticates `FREESOLO_INTERNAL_KEY` itself, and that key controls the
plane, so it must not travel to a service you do not run. If you operate your own
Freesolo-compatible auth backend, pass `--freesolo-url` and verification happens
against it.

`flash-server` reads the **process** environment, not `.env`. If you keep
credentials in a file, load it first:

```bash theme={null}
set -a && . ./.env && set +a && flash-server
```

Kubernetes Secrets, systemd `EnvironmentFile`, and any orchestrator's secret
store work as-is.

## Projects on a standalone plane

Every run config still needs a top-level `project` UUID - it groups runs and is
required in standalone mode too. Any well-formed UUID works, so pick one and
reuse it:

```toml theme={null}
project = "11111111-1111-4111-8111-111111111111"
model = "Qwen/Qwen3.5-4B"
algorithm = "sft"

[environment]
id = "github:your-org/your-repo@main:path/to/env/environment.py"

[train]
epochs = 1
max_examples = 1000
```

`flash projects create <name>` mints a UUID locally for you; pass it to
`flash env setup --project <uuid>`. `flash projects list` has no org directory to
enumerate and says so, so keep track of the ids you use. On the plane host, run
these as `flash-cli` (see the warning above).

## Environments

Point `[environment] id` at a GitHub source the standalone plane can read. Every
supported form resolves to an `environment.py` entrypoint.

| Form                                                                | Resolves to                                         |
| ------------------------------------------------------------------- | --------------------------------------------------- |
| `github:owner/repo`                                                 | `environment.py` on the repository's `main` branch. |
| `github:owner/repo@ref`                                             | `environment.py` at the selected ref.               |
| `github:owner/repo@ref:path/to/env`                                 | `path/to/env/environment.py` at the selected ref.   |
| `github:owner/repo@ref:path/to/env/environment.py`                  | That exact entrypoint at the selected ref.          |
| `https://github.com/owner/repo`                                     | `environment.py` on the repository's `main` branch. |
| `https://github.com/owner/repo/tree/ref/path/to/env`                | `path/to/env/environment.py` at the selected ref.   |
| `https://github.com/owner/repo/blob/ref/path/to/env/environment.py` | That exact file at the selected ref.                |

`ref` may be a branch, tag, or commit SHA; pin a SHA for reproducible runs.
Public repos work without credentials, subject to GitHub's unauthenticated rate
limit; set `GITHUB_TOKEN` for private repos.

The standalone and managed forms do not overlap. A standalone plane rejects
`namespace/project/name` and every spelling of Freesolo's managed Hub because
that private repository is not readable from the operator's plane. The managed
service does the inverse: it accepts only a published `namespace/project/name`
slug and rejects direct GitHub references. The check reads `FLASH_STANDALONE` on
the **server**, so the plane you submit to decides, not the CLI you submit from.

`flash env push` publishes to the managed hub and is not part of a self-hosted
deployment. A **local directory is not a supported environment source**: the GPU
worker fetches the environment itself, so it needs a source it can reach, and an
`[environment] path` is rejected at submit.

## Choosing providers

Flash allocates across RunPod, Lambda, and Vast. **Configure the ones you have;
the rest are never considered.** A class is eligible only on a configured
provider that can actually provision it. Run `flash gpus` for the active class
names, VRAM, and estimated hourly rates.

| Provider | Variable         | Notes                                                           |
| -------- | ---------------- | --------------------------------------------------------------- |
| RunPod   | `RUNPOD_API_KEY` | One key, or several comma-separated for multi-account failover. |
| Lambda   | `LAMBDA_API_KEY` |                                                                 |
| Vast     | `VAST_API_KEY`   |                                                                 |

Startup fails only when **all three** are missing. By default Flash picks the
cheapest validated fitting class across the configured providers. In a run
config, scalar `[gpu] type` or `[gpu] provider` is a hard pin. A `type` list names
acceptable classes that are cost-ranked together. Ordered `[gpu] providers` is a
soft preference: named providers rank first, but other configured providers stay
eligible as fallbacks. Do not combine `provider` and `providers`.

A single RunPod key works, and warns at startup: a one-account pool cannot ride
out that account's quota or credit exhaustion by moving to another. That is an
availability property, not a correctness one.

<Warning>
  Flash does not cap RunPod endpoint concurrency, on a self-hosted plane or a
  managed one. If you expect many concurrent runs, cap them upstream of Flash or
  raise the worker quota on your RunPod account - a large enough burst hits
  RunPod's account limit and the excess deploys fail there.
</Warning>

## What `FLASH_STANDALONE=1` does

The managed deployment keeps organizations, projects, and billing in a Freesolo
backend and validates against it on every run. A self-hoster has no such backend:
without this flag the plane tries to reach `api.freesolo.co`, the validation call
fails, and **every run is rejected with a 503**.

With it set:

* **`FREESOLO_INTERNAL_KEY` is the only credential the plane accepts.** External
  bearer tokens are rejected rather than accepted unverified, which would turn a
  self-hosted plane into an open one.
* **Project ids are taken as given** - still required and shape-validated, but
  not checked against an org directory that does not exist here.
* **Backend reporting is off** - billing, cost reconciliation, checkpoint
  registration, and the hosted artifact sweep. Otherwise these would send your
  operator key to Freesolo.
* **Hosted-only CLI commands say so.** `flash projects create` mints a UUID
  locally; `flash projects list` and `flash traces export` have no local store to
  read and refuse with the reason.

Self-hosting relaxes the billing boundaries, not the catalog: trainable models
are the same [curated set](/reference/models) on both deployments, and an
uncatalogued id is rejected at config parse time before a GPU is rented.

## The state directory

Everything the plane persists locally - the SQLite database of keys and run
ownership, run records, results, and the CLI's saved login - lives under one
root, `~/.flash` by default. Set `FLASH_DATA_DIR` to move it somewhere a rootless
container, a mounted PVC, or a `ProtectHome` systemd unit can write:

```bash theme={null}
export FLASH_DATA_DIR=/var/lib/flash
```

It must be **exported**, not just assigned: `flash-server` reads it from its own
environment, so a bare shell assignment leaves state under `~/.flash` while you
believe it moved.

Everything moves together, so back up or mount that one directory. In Docker,
setting `FLASH_DATA_DIR` also means mounting your own volume at the new path -
the image's `VOLUME` names the default location, and state written anywhere else
lands on the container's writable layer and is lost when the container is
replaced.

Run exactly **one** instance per state directory. State is local files plus
SQLite; there is no horizontal scaling. On networked storage, raise
`FLASH_SQLITE_BUSY_TIMEOUT_SECONDS` (default 30).

## Logs

`flash-server` logs at INFO. Provider resolution, capability revocation, reaper
startup, and degraded-configuration warnings are reported there and nowhere else,
so that is the first place to look when the plane misbehaves.
`FLASH_LOG_LEVEL` turns it up (`DEBUG`) or down (`WARNING`), and
`FLASH_LOG_FORMAT=json` emits one JSON object per line for a structured sink.

## Serving

`flash serve deploy` provisions serving in **your own Modal or RunPod account**,
running the published serving image against one base model and one run's adapter.
That is the supported serving path when you are not on managed serving, and it
works the same whether your training plane is managed or standalone. See
[Serve in your own account](/guides/deploy-and-chat#serve-in-your-own-account)
for the full walkthrough and
[the CLI reference](/reference/cli#serving-in-your-own-account) for every flag.

```bash theme={null}
pip install 'freesolo-flash[server]'   # serve needs the server extra
export HF_TOKEN=hf_...
export FLASH_SERVING_KEY=$(python -c 'import secrets; print(secrets.token_urlsafe(32))')
export MODAL_TOKEN_ID=... MODAL_TOKEN_SECRET=...

flash serve deploy \
  --provider modal \
  --model Qwen/Qwen3.5-4B \
  --run <run-id> \
  --deployment-id my-4b-serving \
  --image ghcr.io/freesolo-co/freesolo-flash-serve@sha256:<digest> \
  --artifact-repo <hub-repo> \
  --artifact-subfolder <path-within-repo> \
  --lora-rank 32 \
  --modal-workspace <your-workspace> \
  --modal-environment main \
  --modal-region us-east
```

The command prints an endpoint you call directly, authenticated with the
`FLASH_SERVING_KEY` you generated. Provider credentials are read for one call and
never stored. `flash serve status` and `flash serve undeploy` inspect and remove
that exact deployment.

<Warning>
  Do **not** point `FREESOLO_SERVING_URL` at an endpoint from `flash serve
      deploy`. `flash models deploy`, `undeploy`, and `chat` authenticate with
  `X-Freesolo-Internal-Key` and carry `FREESOLO_INTERNAL_KEY` - the key that
  controls your whole plane. A customer-owned deployment does not read that
  header, so the request fails `401` after sending a plane-wide credential to a
  provider endpoint.
</Warning>

`flash models deploy`, `undeploy`, and `chat` drive a multi-LoRA backend with a
dynamic adapter-registration surface. On a standalone plane they **error out**
until you point `FREESOLO_SERVING_URL` at such a backend that you operate;
pointing it at a Freesolo-hosted URL is refused, for the credential reason above.
A `flash serve deploy` endpoint is not one of these: it receives its adapters in
an immutable manifest at boot and serves only `/healthz`, `/v1/models`, and
`/v1/chat/completions`.

Training, checkpoint streaming, and adapter export never depend on any of this:
your adapters land in your own Hugging Face repos, and `flash models export`
copies one to a repo you name, ready for vLLM or any LoRA-capable server.

## Optional pieces

| Variable               | Effect if unset                                                                                                                  |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `GITHUB_TOKEN`         | Environments in **private** GitHub repos cannot be fetched. Public repos still work. Warns at startup.                           |
| `PARASAIL_API_KEY`     | OPD submissions fail before GPU allocation. Set it on the control plane; workers never receive it. SFT and GRPO do not use it.   |
| `FLASH_PUBLIC_URL`     | OPD submissions fail before GPU allocation. See [the plane's public origin](#the-planes-public-origin) below.                    |
| `FREESOLO_SERVING_URL` | `flash models deploy`, `undeploy`, and `chat` refuse to run. Training and `flash serve` are unaffected. See [Serving](#serving). |

### The plane's public origin

`FLASH_PUBLIC_URL` is **this plane's own public HTTPS origin** - the address a
rented GPU worker dials back to reach the teacher broker. It is not the same
setting as the client's `FLASH_API_URL` / `--api-url`, which may point at a
private address (`http://your-plane:8080`, a tunnel, a VPN) that no rented worker
can resolve.

<Warning>
  An origin workers cannot reach only surfaces **after** a GPU is allocated, so
  the run has already started costing money by the time it fails. Only OPD uses
  it; SFT and GRPO never do.
</Warning>

<Note>
  Named `FLASH_CONTROL_PANEL_URL` before Flash 1.1.38. The old name is not read.
</Note>

## The security model

**A standalone plane is single-tenant.** It cannot distinguish organizations, so
anyone holding `FREESOLO_INTERNAL_KEY` can submit runs, read any run's status and
logs, and spend your GPU budget. Treat that key like a root password: generate it
with `openssl rand -hex 32`, never commit it, and rotate it if it leaks.

Rotating is safe. A standalone plane records run ownership against a fixed
single-tenant owner rather than the key's value, so runs started under the old
key stay listed, inspectable, and cancellable under the new one, and the old key
stops working immediately.

<Warning>
  Do not point a standalone plane at the state directory of a **multi-tenant**
  deployment. It has no way to tell whose runs those were and will treat all of
  them as the operator's.
</Warning>

Do not expose a standalone plane to untrusted callers. Put it on a private
network, behind a VPN, or behind an authenticating reverse proxy. If you need
real multi-tenancy - separate organizations, per-user keys, project ownership
enforcement - run without `FLASH_STANDALONE` and set `FREESOLO_BASE_URL` to an
identity backend serving the auth-verify contract.

## Verifying your setup

`flash-server` preflights at startup: it fails on anything required to run a job,
and only warns when a setting degrades an optional capability.

It **fails** on no GPU provider configured (all three keys missing), a missing
`HF_TOKEN`, a missing `FREESOLO_INTERNAL_KEY`, or - in standalone mode - a
missing or unusable `FLASH_HF_NAMESPACE`. It **warns** on a single-account RunPod
pool and a missing `GITHUB_TOKEN`, then logs the providers it resolved:

```text theme={null}
GPU provider(s) configured: vast
```

That line is the quickest confirmation your credentials were picked up. Then
submit a small run and watch it allocate.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Every run is rejected 503 with 'project validation is unavailable'">
    `FLASH_STANDALONE` is not set, so the plane is trying to reach a Freesolo
    backend. Set it to `1`.
  </Accordion>

  <Accordion title="401 on every request">
    In standalone mode `FREESOLO_INTERNAL_KEY` is the only accepted credential.
    Confirm the client is sending that exact value (`flash login --api-key ...`,
    or `flash-cli login` on the plane host) and that the plane was started with it
    set.
  </Accordion>

  <Accordion title="Startup rejects FLASH_HF_NAMESPACE">
    It must be a single Hugging Face id segment. `owner/repo` is rejected because
    Flash appends the repo name itself. Use just the user or org name.
  </Accordion>

  <Accordion title="Startup says no GPU provider is configured, but you set a key">
    The value must be non-empty and non-whitespace. For RunPod specifically, a
    value of `","` parses to zero usable accounts.
  </Accordion>

  <Accordion title="Runs fail to allocate on a class you expected">
    The allocator only proposes classes on configured providers. Check the
    `GPU provider(s) configured:` line at startup.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Configuration reference" icon="gear" href="/reference/configuration">
    Every field in a training config.
  </Card>

  <Card title="Supported models" icon="layer-group" href="/reference/models">
    The curated catalog a plane can train.
  </Card>
</CardGroup>
