> ## Documentation Index
> Fetch the complete documentation index at: https://starforge.mintlify.site/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/wccdev/starforge && cd starforge
./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 |
| `app`      | `starforge-console: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.
</Warning>

## Before you start

<Steps>
  <Step title="Set the storage root">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    FORGE_STORAGE_ROOT=/srv/starforge
    ```

    Compose bind-mounts it at **the same path** inside the container. That is not cosmetic: with the
    `local` executor the console drives the host Docker daemon, so the left side of a `-v` flag is a
    host path. If the two differ, the console cannot see what jobs write.
  </Step>

  <Step title="Give the container the docker group">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    getent group docker | cut -d: -f3     # put the number in DOCKER_GID
    ```

    The console runs as uid/gid 10001 and needs to write `/var/run/docker.sock`.
  </Step>

  <Step title="Decide about GPUs">
    The compose file requests `gpus: all` so the NVIDIA toolkit injects `nvidia-smi` and the driver
    libraries — the console can *see* the cards without holding them.

    On a machine with no NVIDIA runtime, delete `gpus: all` and the `NVIDIA_DRIVER_CAPABILITIES`
    line, then set `FORGE_LOCAL_GPU_COUNT` explicitly.
  </Step>

  <Step title="Start it">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    docker compose up -d --build
    docker compose logs -f app
    ```
  </Step>
</Steps>

## Internal DNS names

A container does not read the host's `/etc/hosts`. If your OIDC provider or image mirror resolves
only through it, add the mappings:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
cp docker-compose.override.example.yml docker-compose.override.yml
# edit extra_hosts, taking IPs from `getent hosts <name>` on the host
docker compose up -d --force-recreate app
```

## Recommended configuration

Everything else has a working default. These do not.

```bash .env theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
FORGE_DEFAULT_FLEET_KIND=local
FORGE_STORAGE_ROOT=/srv/starforge
FORGE_PUBLIC_URL=https://starforge.your-company.com
FORGE_WEB_JWT_SECRET=<openssl rand -hex 32>
FORGE_DB_URL=postgresql+psycopg://forge:<password>@postgres:5432/forge
FORGE_REDIS_URL=redis://:<password>@redis:6379/0
FORGE_INGEST_URL=http://<host reachable from a training container>:8080
```

<Info>
  `FORGE_INGEST_URL` is the one people get wrong. Training containers POST their metrics back to it,
  and on the `local` executor they share the host network — so `127.0.0.1` works there but stops
  working the moment you add a second machine. Use the host's real address from the start.
</Info>

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

## Confirm it worked

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -s http://localhost:8080/api/version
docker compose ps          # all three healthy
```

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

## Moving off one machine

Compose with the `local` backend tops out at one host. When you outgrow it, you register a
second Fleet rather than migrating: a [node](/en/ops/executor-node) Fleet for several
bare-metal boxes, or move the console itself into
[Kubernetes](/en/ops/install-kubernetes). The jobs already on the first Fleet stay where they
are.
