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

# Install with Docker Compose

> Postgres, Redis and the console on one machine — the fastest real deployment.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
git clone https://github.com/tuneplane/tuneplane && cd tuneplane
./deploy/up-prod.sh
```

That script copies `.env.example` to `.env`, generates the passwords and the JWT secret that are
still template values, and brings the stack up. It is idempotent — run it again after editing `.env`.

## What comes up

| Service | Image | Why |
| - | - | - |
| `postgres` | `timescale/timescaledb:latest-pg18` | The ledger. TimescaleDB because metric and hardware points are hypertables |
| `redis` | `redis:latest` | Shared cache, distributed locks, rate limiting, immediate token revocation |
| `worker` | `tuneplane-server:latest` | Scheduling, reconciliation and background work |
| `app` | `tuneplane-server:latest` | FastAPI plus the built console, served from one image |

> [!WARNING]
>
> Postgres must start with `shared_preload_libraries=timescaledb`, which the compose file sets. A data
> volume first initialised by plain `postgres` will not have it in `postgresql.conf`, and the
> hypertable migrations then fail. Start from a fresh volume, or add the setting by hand.

## Before you start

**1. Configure shared storage**

Set `TUNEPLANE_STORAGE_ROOT=/srv/tuneplane`. Mount the shared working storage at the same path on the control plane and every Node. Jobs receive scoped mounts rather than the entire storage root.

**2. Start the control plane**

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
docker compose up -d --build
docker compose logs -f app worker
```

The API and workers need no GPU, NVIDIA runtime, Docker CLI, or Docker socket. They schedule work onto registered Fleets.

**3. Connect execution capacity**

Register a Node Fleet and enroll each compute machine using the [Node installation guide](https://docs.tuneplane.ai/en/ops/executor-node.md). A single compute machine uses the same flow. Kubernetes and Slurm clusters each register as their own Fleet.

## Recommended configuration

Everything else has a working default. These do not.

```bash .env theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
TUNEPLANE_STORAGE_ROOT=/srv/tuneplane
TUNEPLANE_PUBLIC_URL=https://tuneplane.your-company.com
TUNEPLANE_WEB_JWT_SECRET=<openssl rand -hex 32>
TUNEPLANE_DB_URL=postgresql+psycopg://tuneplane:<password>@postgres:5432/tuneplane
TUNEPLANE_REDIS_URL=redis://:<password>@redis:6379/0
TUNEPLANE_INGEST_URL=http://<host reachable from a training container>:8080
```

> [!NOTE]
>
> Use a control-plane address reachable from compute nodes for `TUNEPLANE_INGEST_URL`. Loopback points to the compute machine itself.

The database and Redis hostnames are compose service names, resolvable only inside the
`tuneplane-internal` network. Bring the whole stack up with compose; a bare `docker run` of the app
image fails with `failed to resolve host 'postgres'`.

## Check it worked

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -s http://localhost:8080/api/version
docker compose ps          # API, worker, PostgreSQL and Redis are healthy
```

Then open the console. The first visit runs the onboarding that creates the first administrator.

## Moving off one machine

The control plane can stay on this CPU-only machine as capacity grows. Register more Nodes, a Kubernetes cluster, or a Slurm cluster as Fleets. Moving the control plane into Kubernetes is a separate deployment choice.

## Native training telemetry

The `gateway` Compose profile starts the gateway: one process that admits
collector telemetry on port 8444 and serves Node control on port 8443, each
with its own limits. Training jobs use the pinned Rust collector; no client
certificate issuer is required. Startup automatically persists the gateway TLS
identity and supplies its public CA. Set `TUNEPLANE_NODE_GATEWAY_TARGET` to the
public `host:8443` for automatic Node enrollment. The private key stays on the
control plane. All replicas must share PostgreSQL, `TUNEPLANE_SECRET_ENC_KEY`
and `TUNEPLANE_WEB_JWT_SECRET`.

Build a collector for the execution hosts with `scripts/build-rust-collector.py`
and place its complete output directory on platform-managed working storage,
outside job packages and run outputs. Set these values in `.env`:

```dotenv theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
TUNEPLANE_TELEMETRY_GATEWAY_TARGET=telemetry.example.internal:8444
TUNEPLANE_TELEMETRY_COLLECTOR_ARTIFACT=/srv/tuneplane/platform/collector-linux-amd64
TUNEPLANE_TELEMETRY_SPOOL_ROOT=/var/lib/tuneplane/collector-spool
```

With automatic gateway TLS enabled and neither gateway trust-bundle override set, startup supplies the public CA and each launch distributes it to the collector. Set `TUNEPLANE_TELEMETRY_TRUST_BUNDLE` only for an externally managed certificate. `TUNEPLANE_TELEMETRY_SPOOL_BYTES` defaults to 256 MiB and `TUNEPLANE_TELEMETRY_DRAIN_SECONDS` to 30 seconds; set them only to override these limits. Node enrollment configures the Node control connection; it does not replace the three telemetry settings above.

The target is `host:port`, without a URL scheme. Its hostname must match the
server certificate and be reachable from execution hosts. The trust bundle is
the public issuing CA chain. All gateway replicas use the same database and
fixed `TUNEPLANE_WEB_JWT_SECRET`. Leave the target unset only when this deployment
does not enable native collection; required sources then refuse to start.

Provision the spool root on persistent local disk on each execution host,
mode `0700`, owned by the execution service account. Do not use NFS, a container
writable layer, or temporary storage. The platform creates isolated launch and
member subdirectories, issues scoped renewal credentials, and mounts private
grant files read-only. Shared-storage file ownership must let the workload read
its `0600` grant files; align service UIDs instead of making credentials public.

For a Node or Slurm Fleet:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
docker compose --profile gateway up -d
```

The spool survives process/container restart on its original host. Permanent
host-disk loss is outside that guarantee. Shutdown drains for the configured
budget; incomplete delivery retains the spool and fails required collection.
Do not remove retained directories with pending records. Grant files and local
spools are not part of training artifacts. Renewals expire with their execution
lease, superseded attempt, revocation, or the five-minute terminal drain window.

Use a collector artifact matching the execution host architecture. A mixed
architecture installation needs separate compatible deployment profiles; an
incorrect binary is rejected rather than installed into the training environment.

## Separate storage disks

Use the optional overlay for separate physical locations. All sources and targets
are identical absolute paths. The gateway mounts them read-only; training jobs
receive only the slices granted by the executor.

| Host and API/worker path | Purpose | Training access |
| - | - | - |
| `/srv/tuneplane` | Default platform files | No whole-root mount |
| `/srv/tuneplane-runs` | Work, checkpoints, exports and logs | Own history read-only; current output writable |
| `/srv/tuneplane-cache` | Download and dataset caches | Framework cache subdirectories writable |
| `/srv/tuneplane-volumes` | Governed models, datasets and shared material | Authorized Volume subdirectories read-only |
| PostgreSQL/Redis named volumes | Database and queue state | Never mounted |

Add these values to `.env`. API, workers and Nodes must use the same values.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
TUNEPLANE_STORAGE_ROOT=/srv/tuneplane
TUNEPLANE_RUNS_ROOT=/srv/tuneplane-runs
TUNEPLANE_CACHE_ROOT=/srv/tuneplane-cache
TUNEPLANE_VOLUME_ROOT=/srv/tuneplane-volumes
```

Provision actual disks/NAS mounts first, then create their subdirectories. For a
new installation using the image's UID 10001:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
sudo install -d -o 10001 -g 10001 /srv/tuneplane /srv/tuneplane-runs /srv/tuneplane-cache
sudo install -d -o 10001 -g 10001 /srv/tuneplane/{packages,reflow,state}
sudo install -d -o 10001 -g 10001 /srv/tuneplane-cache/{hf/hub,hf/datasets,modelscope,hubs,datasets,volumes}
sudo install -d -o 10001 -g 10001 /srv/tuneplane-volumes
docker compose -f docker-compose.yml -f deploy/docker/compose.storage.yml config --quiet
docker compose -f docker-compose.yml -f deploy/docker/compose.storage.yml up -d
```

Keep both `-f` options on subsequent Compose commands. The overlay refuses to
create absent bind sources. It also supports finer cache and state overrides
listed in [storage layout](https://docs.tuneplane.ai/en/ops/storage.md); provision each explicitly configured
source before startup. Existing external mounts can keep their own paths and
ownership: register them through Storage Connections and mount them read-only
into API/worker at the same host paths. Do not change ownership recursively on
an existing external dataset or model library.

This is a new-install example. Do not switch an existing deployment's roots until
its files and stored references have been migrated. A directory existing on the
host does not prove the NAS mount succeeded; use host mount dependencies and
preflight checks. The overlay does not deploy S3 storage or migrate database data.

Code sandboxes are optional. The default Compose deployment has no host container runtime;
configure an external `TUNEPLANE_SANDBOX_ENDPOINT` if training needs a code execution service.
An explicitly enabled first-party sandbox requires its own Docker or Podman access.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.