> Raw Markdown twin (generated at build time from the source Markdown). Rendered page: https://docs.gatellm.io/en/quickstart/docker-single-node · Doc index: https://docs.gatellm.io/en/llms.txt


# Docker single node

This chapter walks you through GateLLM from scratch: install the gateway → log in to the Console → configure an upstream → issue an access key → send the first model request. The whole flow takes about 5 minutes.

## What you'll accomplish

- A running gateway (`http://localhost:7890`)
- A configured upstream (using OpenAI as an example) and a model
- An access key that can call the gateway from any OpenAI-compatible client

## Prerequisites {#prereq}

- Already have an API key for at least one upstream (such as OpenAI's `sk-...`)
- Docker installed
- **Generate an encryption key `ENCRYPTION_KEY` first** (needed to start the container in step 1):

  ```bash
  openssl rand -hex 32     # outputs 64 hex characters; store it in a password manager — don't lose it
  ```

  Upstream API keys and access keys are encrypted before being stored. **Without this key, saving an upstream in step 4 and issuing an access key in step 7 will both fail.** Bringing it along in step 1 avoids restarting the container halfway through the setup.

## Supported architectures

The official image is multi-arch (`linux/amd64` + `linux/arm64`), so `docker pull` **auto-resolves by host architecture** with no extra flags — ARM servers (Kunpeng / Phytium / AWS Graviton / Ampere, Apple Silicon) and x86 servers work out of the box.

armv7 / ppc64le / s390x / loongarch64 and other architectures are not published. To explicitly pin an architecture, or if you hit `exec format error` / `no matching manifest`, see [image tags and architectures](/en/reference/image-tags-and-arch.md).

## Agent-assisted install (optional) {#agent-assisted-install}

The 8 steps below are written for humans. If you have a coding agent at hand (Claude Code, Cursor, Codex, etc.), **it's recommended to let it install for you** — configuring upstreams and issuing access keys involves several Console fields and a strict ordering (a key group must exist before an access key), and filling them in manually is error-prone; an agent that reads the whole page before executing step by step is more reliable.

### For humans

Paste the following prompt into your agent:

```text
Install and configure the GateLLM gateway. First curl this install guide's raw Markdown, read it end to end, then follow each step in order (steps have dependencies — e.g. a key group must exist before an access key; don't skip ahead):

curl -fsSL https://docs.gatellm.io/en/quickstart/docker-single-node.md
```

### For the agent

If you are that agent, first fetch the raw Markdown of this page, then execute step by step:

```bash
curl -fsSL https://docs.gatellm.io/en/quickstart/docker-single-node.md
```

This document covers: starting the container (including why you must explicitly pass `-e STORAGE_MODE=sqlite`), health checks, first Console login and password recovery, the **fixed order** of configure upstream → configure model → create key group → issue access key, sending the first request, common environment variables, and symptom-based troubleshooting for 401 / 403 / 404 / 502.

**Do not summarize; read from start to finish**: there are ordering dependencies between steps, and skimming will miss prerequisites like creating the key group first. Every page on this site has a corresponding raw Markdown file — append `.md` to the clean URL. For the full documentation index, see `/en/llms.txt`.

## 1. Start the gateway

The image enables the Console and remote access by default. On first startup, when the Console user table is empty, it automatically creates the admin account **`protoflux`**. The password comes from one of two paths — pick either.

### Path A: specify the password at startup (recommended)

The startup command has two placeholder values; **replace them before running**:

- `CONSOLE_PASSWORD=change-me-on-first-login` → change to the Console password you want (**≥8 characters**)
- `ENCRYPTION_KEY=REPLACE_WITH_YOUR_ENCRYPTION_KEY` → change to the string generated by `openssl rand -hex 32` in the prerequisites

```bash
docker run -d \
  --name gatellm \
  -p 7890:7890 \
  -e CONSOLE_PASSWORD=change-me-on-first-login \
  -e ENCRYPTION_KEY=REPLACE_WITH_YOUR_ENCRYPTION_KEY \
  -e STORAGE_MODE=sqlite \
  -v gatellm-data:/var/lib/protoflux \
  ghcr.io/gatellm-io/gatellm:latest
```

> ⚠️ **Do not start with the placeholder values as-is.** `change-me-on-first-login` is a public string in this document; copying it verbatim sets a well-known weak Console password. Leaving `REPLACE_WITH_YOUR_ENCRYPTION_KEY` as-is would encrypt data with a public key, making it effectively unencrypted. Replace both with your own values.

After startup, the log contains only one line `bootstrap: created default admin user 'protoflux'` — **it does not contain the plaintext password** (you already know it). No need to dig through the log; go straight to step 2.

> ⚠️ **`CONSOLE_PASSWORD` only takes effect on first startup.** The gateway reads it only when the Console user table is empty (i.e., first startup), to create the admin `protoflux`. After that it is **completely ignored**: changing this variable and restarting neither changes the password nor reports an error.
>
> - Change the password → Console → the **Console users** page (the only entry point)
> - Forgot the password → `RESET_ADMIN`, see [step 3](#login-console)
> - The value must be **≥8 characters**, otherwise startup is rejected by config validation with `web_console init_login_password is too short (< 8 chars)`

### Path B: don't specify a password, let the gateway generate a random one

Start without `CONSOLE_PASSWORD`; on first startup the gateway generates a 24-character random password and **prints the plaintext once** to the log (`ENCRYPTION_KEY` still needs to be replaced with your own value):

```bash
docker run -d \
  --name gatellm \
  -p 7890:7890 \
  -e ENCRYPTION_KEY=REPLACE_WITH_YOUR_ENCRYPTION_KEY \
  -e STORAGE_MODE=sqlite \
  -v gatellm-data:/var/lib/protoflux \
  ghcr.io/gatellm-io/gatellm:latest
```

Grab the password:

```bash
docker logs gatellm 2>&1 | grep "auto-generated"
```

This log line is printed only once on first startup and is not reprinted on restart; once the log is rotated or discarded, only `RESET_ADMIN` can reset it. So in production, path A is recommended.

> Both commands explicitly pass `-e STORAGE_MODE=sqlite` (if omitted from the command, the template fills it in automatically): the image defaults to `storage_mode = "postgresql"` + empty `postgres_url` (see the `Dockerfile`). **When `LICENSE_KEY` is not injected**, the license layer automatically downgrades this lazy default to sqlite so the gateway can start, so omitting it still works; but passing it explicitly makes the behavior **independent of license status** — once a valid license is injected, the license layer stops downgrading, and if `POSTGRES_URL` is still empty the startup is rejected with `server.postgres_url is required for postgresql storage mode`. For the full reasoning, see "Storage backend and licensing" in the "Common environment variables (optional)" section below.

## 2. Health check

```bash
curl http://localhost:7890/health
# 200 means alive
```

The readiness probe `/ready` returns 503 when the database is unreachable, the gateway is draining (drain mode), or the ingress spill volume is stuck, and can be used by a load balancer to decide whether to route traffic.

## 3. Log in to the Console {#login-console}

Open `http://localhost:7890/console` in a browser.

- Username **`protoflux`**; the password depends on which path you took in step 1: path A uses the value you filled in `-e CONSOLE_PASSWORD=`; path B uses the random password from the first-startup log.
- If you forgot the password, set the environment variable `RESET_ADMIN=<new password>` and restart to reset it (**takes effect only once**). To reset again: set `RESET_ADMIN` to a new value and restart again. After a successful reset, remove the variable and restart to avoid a warning on every restart.
- After logging in, change the password on the **Console users** page immediately (≥8 characters). `CONSOLE_PASSWORD` no longer takes effect after first startup; the Console is the only entry point for changing the password (the fallback for a forgotten password is `RESET_ADMIN`, see above).

> The image defaults to `CONSOLE_ALLOW_REMOTE=true`, so the Console is accessible from the host. If set to `false`, accessing from the host under Docker Desktop / bridged networking returns 403 — remote access is only allowed when `CONSOLE_ALLOW_REMOTE=true`, or through a reverse proxy with access control.

> ℹ️ After logging in you may see an amber banner at the top of the Console: "**Free tier — memory limit 512MB…**". This is a normal notice for the **unlicensed state** and does **not affect completing this tutorial** (features are fully functional in single-node mode). For this banner, the 512MB limit, and how to activate a license, see [Licensing](/en/reference/licensing.md).

## 4. Configure the first upstream {#configure-first-upstream}

> If you already brought `ENCRYPTION_KEY` along in step 1 per the prerequisites, just save directly here. Only if you **skipped** `ENCRYPTION_KEY` will saving at this step report `encryption key: encryption_key not set in config` — the fix is to generate one (`openssl rand -hex 32`) and add `-e ENCRYPTION_KEY=<the string you just generated>` to the step 1 command, then restart (reuse the same data volume; existing config is not lost). **Be sure to back up this key: if lost, the encrypted data is permanently unreadable.** See [Environment variable reference → ENCRYPTION_KEY](/en/reference/configuration.md#storage-multi-instance) for details.

Console left sidebar → **Upstreams** → **New**, fill in:

| Field | Value (example) | Description |
|------|-----------|------|
| Name | `openai` | Custom, just needs to be unique |
| Protocol | `openai` | Select the upstream's protocol |
| Base URL | `https://api.openai.com/v1` | The upstream's real address, no trailing `/` |
| API Key | `sk-...` | The key the upstream gave you; click "Add" to add more, distributed by weight |
| Enabled | ✓ | |

Save. The upstream `openai` appears in the list with status active.

## 5. Configure the first model

In the `openai` upstream row, click **Expand** → model sub-table → **New model**, fill in:

| Field | Value (example) | Description |
|------|-----------|------|
| Name | `gpt-4o` | The model name clients use when calling; customizable |
| Upstream | `openai` | Select the upstream you just created |
| Upstream model ID | `gpt-4o` | The upstream's real model name |
| Enabled | ✓ | |

Save. When clients call using the name `gpt-4o`, the gateway routes to `gpt-4o` on the `openai` upstream.

> "Name" is the name you expose to clients; "upstream model ID" is the real name sent to the upstream. They can differ — this is the basis of the gateway's model alias/mapping.

## 6. Create a key group

An access key's permissions are determined by the key group it belongs to, so **create the key group first, then issue the access key** (the order cannot be reversed, otherwise the "group" field has nothing to select when creating the key).

Console → **Access Keys** → switch to the **Key Groups** tab → **New**, fill in:

| Field | Value | Description |
|------|-----|------|
| Name | `default` | Group name |
| Models | Select `gpt-4o`, or `*` (all) | Determines which models this group's keys can call |
| Enabled | ✓ | |

Save. This key group determines which models the access keys under it can call.

> **Key point**: an access key's permissions are determined by its key group's "models" list. An empty model list = no access to any model; `["*"]` = access to all models; listing specific model names = access only to those listed.

## 7. Issue an access key

Console → **Access Keys** → switch back to the **Access Keys** tab → **New**, fill in:

| Field | Value | Description |
|------|-----|------|
| Name | `my-app-key` | The key's identifier, used for management and auditing |
| API Key | click "Generate" | Auto-generates a string; this is the credential the client carries |
| Group | Select `default` (created in the previous step) | Determines which models this key can access |
| Enabled | ✓ | |

Save. Clients carry this access key when calling (**not** the upstream's `sk-...`).

## 8. Send the first request

```bash
curl http://localhost:7890/v1/chat/completions \
  -H "Authorization: Bearer <your access key>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o",
    "messages": [{"role":"user","content":"hello"}]
  }'
```

Receiving a model reply means it works.

## Common environment variables (optional) {#common-env-vars}

Step 1 only brought the variables required to complete the tutorial. The rest can be added with `-e` as needed — **no config file needs to be mounted**. For example, switching to a PostgreSQL + Redis multi-instance deployment:

```bash
docker run -d \
  --name gatellm \
  -p 7890:7890 \
  -e STORAGE_MODE=postgresql \
  -e POSTGRES_URL=postgres://gw:secret@db:5432/protoflux \
  -e REDIS_URL=redis://redis:6379 \
  -e ENCRYPTION_KEY=the-same-64-hex-key-on-every-instance \
  -e LICENSE_KEY=your-license-key \
  -v gatellm-data:/var/lib/protoflux \
  ghcr.io/gatellm-io/gatellm:latest
```

`POSTGRES_URL` TLS is controlled by the `sslmode` query parameter (`disable` / `require` / `verify-full`). In a multi-instance deployment, **all instances must use the same `ENCRYPTION_KEY`** — the value above is written as a literal placeholder rather than `$(openssl rand -hex 32)`, precisely because the latter generates a different key on every machine.

Common items at a glance (full list in [Environment variable reference](/en/reference/configuration.md)):

| Variable | Purpose |
|------|------|
| `CONSOLE_PASSWORD` | Initial password for the Console admin `protoflux`, **takes effect only on first startup** (≥8 characters); if unset, a random password is auto-generated on first startup and printed once to the log |
| `ENCRYPTION_KEY` | Encryption key for sensitive data; **must be set before saving an upstream API key or issuing an access key**, otherwise saving reports `encryption_key not set in config`. Generate with `openssl rand -hex 32`; be sure to back it up |
| `STORAGE_MODE` | `sqlite` (single node) / `postgresql` (multi-instance); multi-instance needs `POSTGRES_URL` + `REDIS_URL` |
| `LICENSE_KEY` | Activate a license, unlocking multi-instance and a higher memory limit |
| `RESET_ADMIN` | One-time reset when you forget the Console password |

> ⚠️ **Storage backend and licensing**: the image defaults to `storage_mode = "postgresql"` + empty `postgres_url` (see the `Dockerfile`). **When `LICENSE_KEY` is not injected**, the license layer automatically downgrades this lazy default to sqlite so the gateway can start; but **once a valid license is injected** the license layer stops downgrading, and if `STORAGE_MODE` is still `postgresql` while `POSTGRES_URL` is empty, startup is rejected by config validation with `server.postgres_url is required for postgresql storage mode`.
>
> That's why step 1 explicitly passes `-e STORAGE_MODE=sqlite`, so the single-node command works in both licensed and unlicensed states. To use PostgreSQL for multi-instance: fill in `POSTGRES_URL` + `REDIS_URL` (multi-instance is a distributed capability; see [Licensing](/en/reference/licensing.md) for the license requirement).

## Production deployment: raise the fd limit (strongly recommended) {#raise-fd-limit}

An fd (file descriptor) is the number the operating system assigns to every "open thing" (network connections, files), and the total has an upper limit (`ulimit -n`). Each **queued** request holds about 3 fds (client connection + disconnect-detection handle + spill temp file); once an upstream failure causes many requests to queue, fds grow linearly. If the deployment's fd limit is low (e.g. 1024), queued requests can exhaust fds, after which the gateway **can't even accept new connections** (`accept` reports `Too many open files`), and the Console becomes unreachable.

From the kernel's perspective, the fd limit is just a number space; **raising it costs almost nothing, so there is no reason not to**. The gateway recommends setting both the soft and hard limits to **1048576**. How to set it in each deployment form:

| Deployment form | How to set |
|----------|----------|
| `docker run` | add `--ulimit nofile=1048576:1048576` to the command |
| docker-compose | add `ulimits: { nofile: { soft: 1048576, hard: 1048576 } }` under the service |
| systemd (bare metal) | add `LimitNOFILE=1048576` to the unit file |
| bare shell, run manually | run `ulimit -n 1048576` before starting |
| Kubernetes | the Pod spec has no native ulimit field; it relies on the host runtime's default (Docker ≥ 20.10 defaults to 1048576; varies across K8s runtimes); verify with the actual effective value printed in the startup log |

```yaml
# docker-compose.yml snippet
services:
  protoflux:
    image: protoflux:latest
    ulimits:
      nofile:
        soft: 1048576
        hard: 1048576
```

> **Two notes**:
> - **It can't be set in the Dockerfile** — ulimit is a container runtime property; the Dockerfile spec has no such instruction, so it can only be set via the runtime parameters above.
> - **K8s initContainer can't change it** — rlimit is a process property; an init container leaves no trace after exiting, and the app container's limit comes from the runtime configuration.
>
> There's a fallback on the code side: on startup the gateway tries to raise the soft limit to the hard limit (setrlimit) and prints, in the startup log, the **actually effective fd limit and the connection budget derived from it** (ops can use this to verify); if after raising it's still below 65536, it logs a WARN as a reminder to adjust. The data plane also has a connection budget (`DATA_PLANE_MAX_CONNECTIONS`, derived automatically from the fd limit by default), which guarantees fds never hit the ceiling no matter how low the limit is — over-limit requests fail fast with 429 instead of piling up connections — see [Environment variable reference → Listening and request limits](/en/reference/configuration.md#listen-and-request-limits).

## How to clean up (optional)

When you've finished the tutorial and want to completely remove the environment, delete the container and its data volume:

```bash
docker rm -f gatellm            # the container, i.e. the --name value from step 1
docker volume rm gatellm-data   # the data volume (upstreams, models, keys, logs all live here)
```

> Deleting the data volume **permanently wipes** all the gateway's configuration and request logs, and is not recoverable. To just restart, use `docker restart gatellm`; to rebuild the container but keep the data, only `rm` the container and keep the data volume. After deleting only the container and running `docker run` again with the same-named data volume, the configuration is restored automatically.

## FAQ

> For more error codes and symptom-based troubleshooting, see [Error code reference](/en/reference/error-codes.md) and [Troubleshoot from symptoms](/en/usecases/troubleshooting.md).

**Q: Console login says wrong password / forgot the password?**
Reset with the `RESET_ADMIN=<new password>` environment variable (takes effect only once; full steps in [step 3](#login-console)).

**Q: Changed `CONSOLE_PASSWORD` and restarted, but the password didn't change?**
Expected behavior. `CONSOLE_PASSWORD` is read only when the Console user table is empty (first startup); once the table has a user, it is completely ignored, and it doesn't error. To change the password, go to Console → **Console users**; if you forgot it, use `RESET_ADMIN` (see above).

**Q: Startup fails with `web_console init_login_password is too short (< 8 chars)`?**
`CONSOLE_PASSWORD` shorter than 8 characters is rejected by config validation. Use a ≥8-character value, or drop the variable entirely and take path B.

**Q: Console won't open / returns 403?**
The zero-config main path won't hit this (the image defaults to `CONSOLE_ALLOW_REMOTE=true`). If you set it to `false`, accessing from the host under Docker Desktop / bridged networking returns 403 — see the `CONSOLE_ALLOW_REMOTE` explanation in [step 3](#login-console).

**Q: No "auto-generated" password line in the log?**
Most common cause: you took path A in step 1 and started with `-e CONSOLE_PASSWORD=`. In that case the gateway creates the admin with your password and **deliberately does not print the plaintext**; the log has only the single line `bootstrap: created default admin user 'protoflux'` — just log in with the value you set.
Second: that line is printed only once, on first startup when the Console user table is empty; restarting doesn't reprint it. If you already logged in and then forgot the password, use `RESET_ADMIN` (see above). It could also be that the Console was disabled with `CONSOLE_ENABLED=false`, in which case no admin is created at all.

**Q: Request returns 401?**
The access key isn't right. Check that `Authorization: Bearer` is followed by the access key you issued (not the upstream's `sk-...`).

**Q: Request returns 403 model_access_denied?**
The key group your key belongs to doesn't include the model you're trying to call in its "models" list. Go back to the key group and add that model (or use `*`).

**Q: Request returns 404 model_not_found?**
The model name is misspelled, or the model has `hide_name` set (only accessible by alias).

**Q: Request returns 502 bad_gateway?**
The upstream is unreachable. Check whether the upstream's `base_url` and API key are correct and whether the upstream is reachable.

**Q: Saving an upstream / issuing an access key in the Console reports `encryption key: encryption_key not set in config`?**
`ENCRYPTION_KEY` isn't set. See the warning at the start of [step 4](#configure-first-upstream).

**Q: The container exits immediately on start and the log reports `exec format error`?**
The image architecture doesn't match the host. Usually because you explicitly passed `--platform` pinning the wrong architecture (e.g. pinning `linux/amd64` on an ARM host). Remove `--platform` — the image already auto-resolves by host architecture. See [image tags and architectures](/en/reference/image-tags-and-arch.md) for details.

**Q: `docker pull` reports `no matching manifest for linux/xxx`?**
That architecture isn't published — only `linux/amd64` and `linux/arm64` are released. See [image tags and architectures](/en/reference/image-tags-and-arch.md) for details.

**Next**: Integrators should read [Endpoints · auth · protocol interop](/en/reference/endpoints.md) to learn all endpoints and authentication methods; administrators should read [Console login and roles](/en/console/login-and-roles.md) to start managing. You can also continue with [Connect an OpenAI upstream](/en/quickstart/openai-compatible.md) for a more complete OpenAI integration example.
