> 原始 Markdown 孪生体（构建期从源 Markdown 生成）。渲染页：https://docs.gatellm.io/zh-CN/quickstart/docker-single-node · 文档索引：https://docs.gatellm.io/zh-CN/llms.txt


# Docker 单机跑通

本章带你从零跑通 GateLLM：装好网关 → 登录控制台 → 配一个上游 → 签发访问密钥 → 发出第一个模型请求。全程约 5 分钟。

## 你将完成什么

- 一个运行中的网关（`http://localhost:7890`）
- 一个已配置的上游（以 OpenAI 为例）和一个模型
- 一把访问密钥，能从任意 OpenAI 兼容客户端调用网关

## 前置 {#prereq}

- 已有至少一个上游的 API key（如 OpenAI 的 `sk-...`）
- 已装 Docker
- **先生成一把加密密钥 `ENCRYPTION_KEY`**（第 1 步启动容器就要用）：

  ```bash
  openssl rand -hex 32     # 输出 64 位十六进制；存进密码管理器，务必别丢
  ```

  上游 API key 与访问密钥都在落库前加密，**没有这把密钥，第 4 步保存上游、第 7 步签发密钥都会失败**。在第 1 步就带上它能避免装到一半再重启容器。

## 支持的架构

官方镜像是 multi-arch（`linux/amd64` + `linux/arm64`），`docker pull` **自动按宿主架构解析**，无需任何额外参数——ARM 服务器（鲲鹏 / 飞腾 / AWS Graviton / Ampere、Apple Silicon）和 x86 服务器开箱即用。

未发布 armv7 / ppc64le / s390x / loongarch64 等架构。要显式钉架构、或遇到 `exec format error` / `no matching manifest`，见 [镜像 tag 与架构](/zh-CN/reference/image-tags-and-arch.md)。

## Agent 辅助安装（可选） {#agent-assisted-install}

下面 8 步是给人看的。如果你手边有 coding agent（Claude Code、Cursor、Codex 等），**建议直接让它替你装** —— 配上游、签发密钥这些步骤涉及若干控制台字段和先后顺序（密钥组必须先于访问密钥），人手动填容易错序；agent 读完整篇再逐步执行更稳。

### 给人类

把下面这段提示词粘给你的 agent：

```text
安装并配置 GateLLM 网关。请先用 curl 取回这份安装指南的原始 Markdown，从头到尾读完后逐步执行（步骤间有先后顺序依赖，例如密钥组必须先于访问密钥建立，不要跳读）：

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

### 给 Agent

如果你就是那个 agent，先取回本文的原始 Markdown，然后逐步执行：

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

这份文档覆盖：启动容器（含为什么必须显式 `-e STORAGE_MODE=sqlite`）、健康检查、控制台首次登录与密码找回、配上游 → 配模型 → 建密钥组 → 签发访问密钥的**固定顺序**、发出第一个请求、常用环境变量、以及 401 / 403 / 404 / 502 的对症排查。

**不要摘要，从头读到尾**：步骤之间有顺序依赖，跳读会漏掉先建密钥组这类前置条件。本站每个页面都有对应的原始 Markdown —— 把 clean URL 加上 `.md` 后缀即可。完整文档索引见 `/zh-CN/llms.txt`。

## 1. 启动网关

镜像默认开控制台、开远程访问，首次启动且控制台用户表为空时自动创建管理员账号 **`protoflux`**。密码有两种来法，任选一条。

### 路径 A：启动时自己指定密码（推荐）

启动命令里有两个占位值，**复制后先替换再运行**：

- `CONSOLE_PASSWORD=change-me-on-first-login` → 换成你要用的控制台密码（**≥8 字符**）
- `ENCRYPTION_KEY=REPLACE_WITH_YOUR_ENCRYPTION_KEY` → 换成前置里 `openssl rand -hex 32` 生成的那串

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

> ⚠️ **别用占位值原样启动**。`change-me-on-first-login` 是文档里的公开字符串，照抄等于给控制台设了一个人尽可知的弱密码；`REPLACE_WITH_YOUR_ENCRYPTION_KEY` 原样留下则会让加密数据用公开密钥加密、形同未加密。两个都必须换成你自己的值。

启动后日志里只有一行 `bootstrap: created default admin user 'protoflux'`——**不含明文密码**（你已经知道它）。不用翻日志，直接进第 2 步。

> ⚠️ **`CONSOLE_PASSWORD` 只在第一次启动生效**。网关只在控制台用户表为空（也就是首次启动）时读它，用来创建管理员 `protoflux`。此后它被**完全忽略**：改这个变量再重启既不会改密、也不会报错。
>
> - 改密码 → 控制台 →「控制台用户」页（唯一入口）
> - 忘了密码 → `RESET_ADMIN`，见[第 3 步](#login-console)
> - 值必须 **≥8 字符**，否则启动被配置校验拒掉，报 `web_console init_login_password is too short (< 8 chars)`

### 路径 B：不指定密码，让网关生成随机密码

不带 `CONSOLE_PASSWORD` 启动，网关在首次启动生成一把 24 位随机密码并把明文**打印一次**到日志（`ENCRYPTION_KEY` 仍要替换成你自己的值）：

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

抓密码：

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

这行日志只在首启打印一次，重启不会重打；日志被轮转或丢弃后就只能靠 `RESET_ADMIN` 重置。所以生产环境更推荐路径 A。

> 两条命令都显式带 `-e STORAGE_MODE=sqlite`（命令里没写也会由模板自动补上）：镜像默认 `storage_mode = "postgresql"` + 空 `postgres_url`（见 `Dockerfile`）。**未注入 `LICENSE_KEY` 时**，license 层会把这个惰性默认自动降级为 sqlite 让网关能启动，所以不加这条也能起；但显式带上能让行为**不依赖授权状态**——一旦注入有效授权，license 层不再降级，此时若 `POSTGRES_URL` 仍为空就会拒启，报 `server.postgres_url is required for postgresql storage mode`。完整原因见下方「常用环境变量（可选）」一节的「存储后端与授权」。

## 2. 健康检查

```bash
curl http://localhost:7890/health
# 200 即存活
```

就绪探针 `/ready` 在数据库不可达、网关排空（drain 模式）或入口落盘卷挂死时返回 503，可用于负载均衡器判断是否分发流量。

## 3. 登录控制台 {#login-console}

浏览器打开 `http://localhost:7890/console`。

- 用户名 **`protoflux`**，密码取决于第 1 步走的哪条路径：走路径 A 就是你在 `-e CONSOLE_PASSWORD=` 里填的值；走路径 B 就是首启日志里那把随机密码。
- 忘记密码时，设环境变量 `RESET_ADMIN=<新密码>` 后重启即重置（**仅生效一次**）。需再次重置：重新设 `RESET_ADMIN` 为新值再重启。重置成功后去掉该环境变量再重启，可避免每次重启告警。
- 登录后立即在「控制台用户」页改密（≥8 字符）。`CONSOLE_PASSWORD` 首启之后不再生效，控制台是唯一的改密入口（忘密码的兜底是 `RESET_ADMIN`，见上）。

> 镜像默认 `CONSOLE_ALLOW_REMOTE=true`，控制台可从宿主机访问。若设为 `false`，Docker Desktop / 桥接网络下从宿主机访问会 403——只在 `CONSOLE_ALLOW_REMOTE=true`、或经反向代理与访问控制时才放行远程。

> ℹ️ 登录后你可能在控制台顶部看到一条琥珀色「**免费版 — 内存上限为 512MB…**」横幅。这是**未授权状态**的正常提示，**不影响本教程跑通**（单机模式下功能完整）。关于这条横幅、512MB 上限与如何激活授权，见 [许可与授权](/zh-CN/reference/licensing.md)。

## 4. 配第一个上游 {#configure-first-upstream}

> 若你在第 1 步已按前置带上 `ENCRYPTION_KEY`，这里直接保存即可。只有**跳过**了 `ENCRYPTION_KEY` 时，这一步保存才会报 `encryption key: encryption_key not set in config`——补法是生成一把（`openssl rand -hex 32`）并在第 1 步命令上加 `-e ENCRYPTION_KEY=<刚生成的那串>` 重启（复用同一数据卷，已有配置不丢）。**务必备份这把密钥：丢了已加密的数据永久不可读。** 详见[环境变量配置参考 → ENCRYPTION_KEY](/zh-CN/reference/configuration.md#storage-multi-instance)。

控制台左侧 → **上游服务** → **新建**，填：

| 字段 | 值（示例） | 说明 |
|------|-----------|------|
| 名称 | `openai` | 自定义，唯一即可 |
| 协议 | `openai` | 选对应上游的协议 |
| 基础 URL | `https://api.openai.com/v1` | 上游真实地址，结尾不带 `/` |
| API Key | `sk-...` | 上游给你的密钥，点「添加」可加多把，按权重分布 |
| 启用 | ✓ | |

保存。上游列表出现 `openai`，状态为活跃。

## 5. 配第一个模型

在上游 `openai` 这一行点 **展开** → 模型子表 → **新建模型**，填：

| 字段 | 值（示例） | 说明 |
|------|-----------|------|
| 名称 | `gpt-4o` | 客户端调用时填的模型名，可自定义 |
| 上游 | `openai` | 选刚建的上游 |
| 上游模型 ID | `gpt-4o` | 上游真实模型名 |
| 启用 | ✓ | |

保存。客户端用 `gpt-4o` 这个名字调用时，网关会路由到上游 `openai` 的 `gpt-4o`。

> 「名称」是你对外暴露给客户端的名字，「上游模型 ID」是发往上游的真实名字。两者可以不同——这是网关做模型别名/映射的基础。

## 6. 建密钥组

访问密钥的权限由其所属密钥组决定，所以**先建密钥组、再签发访问密钥**（顺序不能反，否则建密钥时「分组」字段无项可选）。

控制台 → **访问密钥** → 切到「**密钥组**」页签 → **新建**，填：

| 字段 | 值 | 说明 |
|------|-----|------|
| 名称 | `default` | 分组名 |
| 模型 | 选 `gpt-4o`，或 `*`（全部） | 决定该组密钥能调哪些模型 |
| 启用 | ✓ | |

保存。这个密钥组决定了挂在它下面的访问密钥能调哪些模型。

> **关键**：访问密钥的权限由其所属密钥组的「模型」列表决定。模型列表为空 = 无权访问任何模型；填 `["*"]` = 可访问所有模型；填具体模型名 = 只能访问列出的。

## 7. 签发访问密钥

控制台 → **访问密钥** → 切回「**访问密钥**」页签 → **新建**，填：

| 字段 | 值 | 说明 |
|------|-----|------|
| 名称 | `my-app-key` | 密钥的标识，用于管理与审计 |
| API 密钥 | 点「生成」 | 自动生成一串，这是客户端要带的凭证 |
| 分组 | 选 `default`（上一步建） | 决定这把密钥能访问哪些模型 |
| 启用 | ✓ | |

保存。客户端调用时带这把访问密钥（**不是**上游的 `sk-...`）。

## 8. 发第一个请求

```bash
curl http://localhost:7890/v1/chat/completions \
  -H "Authorization: Bearer <你的访问密钥>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o",
    "messages": [{"role":"user","content":"你好"}]
  }'
```

收到模型回复即跑通。

## 常用环境变量（可选） {#common-env-vars}

第 1 步只带了跑通教程所必需的变量。其余项按需加 `-e` 即可，**不需要挂任何配置文件**。例如切到 PostgreSQL + Redis 多实例：

```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 用 `sslmode` 查询参数控制（`disable` / `require` / `verify-full`）。多实例部署时**所有实例必须用同一把 `ENCRYPTION_KEY`**——上面写成字面占位值而不是 `$(openssl rand -hex 32)`，正是因为后者在每台机器上会生成不同的密钥。

常用项一览（完整清单见 [环境变量配置参考](/zh-CN/reference/configuration.md)）：

| 变量 | 作用 |
|------|------|
| `CONSOLE_PASSWORD` | 控制台管理员 `protoflux` 的初始密码，**仅首次启动生效**（≥8 字符）；不设则首启自动生成随机密码并打印一次到日志 |
| `ENCRYPTION_KEY` | 敏感数据加密密钥；**保存上游 API key 或签发访问密钥前必设**，否则保存报 `encryption_key not set in config`。`openssl rand -hex 32` 生成，务必备份 |
| `STORAGE_MODE` | `sqlite`（单机）/ `postgresql`（多实例）；多实例需配 `POSTGRES_URL` + `REDIS_URL` |
| `LICENSE_KEY` | 激活授权，解锁多实例与更高内存上限 |
| `RESET_ADMIN` | 忘记控制台密码时的一次性重置 |

> ⚠️ **存储后端与授权**：镜像默认 `storage_mode = "postgresql"` + 空 `postgres_url`（见 `Dockerfile`）。**未注入 `LICENSE_KEY` 时**，license 层会把这个惰性默认自动降级为 sqlite 让网关能启动；但**注入有效授权后** license 层不再降级，此时若 `STORAGE_MODE` 仍是 `postgresql` 而 `POSTGRES_URL` 为空，启动会被配置校验拒掉，报 `server.postgres_url is required for postgresql storage mode`。
>
> 因此第 1 步显式带 `-e STORAGE_MODE=sqlite`，让单机命令在是否授权两种状态下都能跑通。要多实例用 PostgreSQL：填 `POSTGRES_URL` + `REDIS_URL`（多实例属分布式能力，授权要求见 [许可与授权](/zh-CN/reference/licensing.md)）。

## 生产部署：抬高 fd 上限（强烈建议） {#raise-fd-limit}

fd（file descriptor，文件描述符）是操作系统给每个"打开的东西"（网络连接、文件）分配的编号，总数有上限（`ulimit -n`）。网关每个**排队中**的请求会一直握着约 3 个 fd（客户端连接 + 断开检测句柄 + 落盘临时文件）；一旦上游故障导致大量请求排队，fd 会线性增长。若部署环境的 fd 上限偏低（如 1024），排队请求可把 fd 耗尽，之后网关**连新连接都接不进来**（`accept` 报 `Too many open files`），控制台也随之不可达。

fd 上限在内核眼里只是编号空间，**调大几乎零成本，没有理由不调**。网关推荐软、硬上限都设 **1048576**。各部署形态的设置方法：

| 部署形态 | 设置方法 |
|----------|----------|
| `docker run` | 命令加 `--ulimit nofile=1048576:1048576` |
| docker-compose | service 下加 `ulimits: { nofile: { soft: 1048576, hard: 1048576 } }` |
| systemd（裸机） | unit 文件加 `LimitNOFILE=1048576` |
| 裸 shell 手工运行 | 先 `ulimit -n 1048576` 再启动 |
| Kubernetes | Pod spec 无原生 ulimit 字段，依赖宿主机运行时默认值（Docker ≥ 20.10 默认即 1048576，各 K8s 运行时不一）；以启动日志打印的实际生效值验收 |

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

> **注意两点**：
> - **Dockerfile 里设不了**——ulimit 是容器运行期属性，Dockerfile 规范没有该指令，只能靠上面的运行时参数。
> - **K8s 的 initContainer 改不了**——rlimit 是进程属性，init 容器退出后不留痕迹，应用容器的限制来自运行时配置。
>
> 代码侧有兜底：网关启动时会尽力把软上限抬到硬上限（setrlimit），并在启动日志打印**实际生效的 fd 上限与据此算出的连接预算**（运维可据此验收）；抬高后仍低于 65536 会记 WARN 提醒调整。同时数据面有连接预算（`DATA_PLANE_MAX_CONNECTIONS`，默认从 fd 上限自动推导），无论上限多小都保证 fd 用不到顶、超限快速 429 而非停车攒连接——见 [环境变量配置参考 → 监听与请求限制](/zh-CN/reference/configuration.md#listen-and-request-limits)。

## 如何清理（可选）

教程跑完想彻底移除这套环境时，删掉容器和它的数据卷：

```bash
docker rm -f gatellm            # 容器，即第 1 步 --name 的值
docker volume rm gatellm-data   # 数据卷（上游、模型、密钥、日志都在这里）
```

> 删数据卷会**永久清除**网关的全部配置与请求日志，不可恢复。只是想重启用 `docker restart gatellm`；想重建容器但保留数据，只 `rm` 容器、保留数据卷即可。仅删除容器后再次 `docker run` 挂回同名数据卷，配置会自动恢复。

## 常见问题

> 更多错误码与症状排查见 [错误码速查](/zh-CN/reference/error-codes.md) 与 [从症状出发排障](/zh-CN/usecases/troubleshooting.md)。

**Q：登录控制台提示密码错误 / 忘记密码？**
用 `RESET_ADMIN=<新密码>` 环境变量重置（仅生效一次，完整步骤见[第 3 步](#login-console)）。

**Q：改了 `CONSOLE_PASSWORD` 重启，密码没变？**
预期行为。`CONSOLE_PASSWORD` 只在控制台用户表为空（首次启动）时被读取；表里已有用户后它被完全忽略，且不会报错。改密走控制台 →「控制台用户」；忘了密码走 `RESET_ADMIN`（见上）。

**Q：启动失败，报 `web_console init_login_password is too short (< 8 chars)`？**
`CONSOLE_PASSWORD` 少于 8 字符会被配置校验拒启。换一个 ≥8 字符的值，或干脆去掉这个变量走路径 B。

**Q：控制台打不开 / 返回 403？**
零配置主路径不会遇到（镜像默认 `CONSOLE_ALLOW_REMOTE=true`）。若你把它设成了 `false`，Docker Desktop / 桥接网络下从宿主机访问会 403——见[第 3 步](#login-console)的 `CONSOLE_ALLOW_REMOTE` 说明。

**Q：日志里没看到 "auto-generated" 密码行？**
最常见原因：你走的是第 1 步的路径 A，带了 `-e CONSOLE_PASSWORD=` 启动。这时网关用你设的密码建管理员，并**刻意不打印明文**，日志里只有 `bootstrap: created default admin user 'protoflux'` 这一行——直接用你设的值登录即可。
其次：该行只在首次启动且控制台用户表为空时打印一次，重启不会重打。若已登录过又忘了密码，用 `RESET_ADMIN` 重置（见上）。也可能是控制台被 `CONSOLE_ENABLED=false` 关掉，此时根本不会建管理员。

**Q：请求返回 401？**
访问密钥没带对。检查 `Authorization: Bearer` 后面跟的是你签发的访问密钥（不是上游的 `sk-...`）。

**Q：请求返回 403 model_access_denied？**
密钥所在的密钥组「模型」列表没包括你要调的模型。回到密钥组把该模型加进去（或用 `*`）。

**Q：请求返回 404 model_not_found？**
模型名拼错，或该模型被设了 `hide_name`（只能用别名访问）。

**Q：请求返回 502 bad_gateway？**
上游不通。检查上游的 `base_url` 和 API key 是否正确、上游是否可访问。

**Q：在控制台保存上游 / 签发访问密钥时报 `encryption key: encryption_key not set in config`？**
`ENCRYPTION_KEY` 没设。见[第 4 步](#configure-first-upstream)开头的警示。

**Q：容器启动就退出，日志报 `exec format error`？**
镜像架构与宿主不符。通常因为显式带了 `--platform` 钉到了错误架构（如 ARM 宿主上钉了 `linux/amd64`）。去掉 `--platform` 即可——镜像本就按宿主架构自动解析。详见[镜像 tag 与架构](/zh-CN/reference/image-tags-and-arch.md)。

**Q：`docker pull` 报 `no matching manifest for linux/xxx`？**
该架构未发布，官方只发 `linux/amd64` 与 `linux/arm64`。详见[镜像 tag 与架构](/zh-CN/reference/image-tags-and-arch.md)。

**下一步**：接入方看 [端点 · 认证 · 协议互通](/zh-CN/reference/endpoints.md) 了解全部端点和认证方式；管理员看 [控制台登录与角色](/zh-CN/console/login-and-roles.md) 开始管理。也可以继续 [接入 OpenAI 上游](/zh-CN/quickstart/openai-compatible.md) 看更完整的 OpenAI 接入示例。
