Skip to content
This page is a translation of the authoritative Chinese source and may lag behind.View the original

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

  • 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.

Agent-assisted install (optional)

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.

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

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.

4. Configure the 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 for details.

Console left sidebar → UpstreamsNew, fill in:

FieldValue (example)Description
NameopenaiCustom, just needs to be unique
ProtocolopenaiSelect the upstream's protocol
Base URLhttps://api.openai.com/v1The upstream's real address, no trailing /
API Keysk-...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:

FieldValue (example)Description
Namegpt-4oThe model name clients use when calling; customizable
UpstreamopenaiSelect the upstream you just created
Upstream model IDgpt-4oThe 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:

FieldValueDescription
NamedefaultGroup name
ModelsSelect 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:

FieldValueDescription
Namemy-app-keyThe key's identifier, used for management and auditing
API Keyclick "Generate"Auto-generates a string; this is the credential the client carries
GroupSelect 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)

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):

VariablePurpose
CONSOLE_PASSWORDInitial 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_KEYEncryption 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_MODEsqlite (single node) / postgresql (multi-instance); multi-instance needs POSTGRES_URL + REDIS_URL
LICENSE_KEYActivate a license, unlocking multi-instance and a higher memory limit
RESET_ADMINOne-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 for the license requirement).

Production deployment: raise the fd limit (strongly recommended)

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 formHow to set
docker runadd --ulimit nofile=1048576:1048576 to the command
docker-composeadd ulimits: { nofile: { soft: 1048576, hard: 1048576 } } under the service
systemd (bare metal)add LimitNOFILE=1048576 to the unit file
bare shell, run manuallyrun ulimit -n 1048576 before starting
Kubernetesthe 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.

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 and Troubleshoot from symptoms.

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).

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.

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.

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 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 for details.

Next: Integrators should read Endpoints · auth · protocol interop to learn all endpoints and authentication methods; administrators should read Console login and roles to start managing. You can also continue with Connect an OpenAI upstream for a more complete OpenAI integration example.