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_KEYfirst (needed to start the container in step 1):bashopenssl rand -hex 32 # outputs 64 hex characters; store it in a password manager — don't lose itUpstream 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:
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.mdFor the agent
If you are that agent, first fetch the raw Markdown of this page, then execute step by step:
$ curl -fsSL https://docs.gatellm.io/en/quickstart/docker-single-node.mdThis 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 byopenssl rand -hex 32in the prerequisites
$ 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-loginis a public string in this document; copying it verbatim sets a well-known weak Console password. LeavingREPLACE_WITH_YOUR_ENCRYPTION_KEYas-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_PASSWORDonly takes effect on first startup. The gateway reads it only when the Console user table is empty (i.e., first startup), to create the adminprotoflux. 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):
$ 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:latestGrab the password:
$ 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 tostorage_mode = "postgresql"+ emptypostgres_url(see theDockerfile). WhenLICENSE_KEYis 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 ifPOSTGRES_URLis still empty the startup is rejected withserver.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
curl http://localhost:7890/health
# 200 means aliveThe 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: setRESET_ADMINto 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_PASSWORDno longer takes effect after first startup; the Console is the only entry point for changing the password (the fallback for a forgotten password isRESET_ADMIN, see above).
The image defaults to
CONSOLE_ALLOW_REMOTE=true, so the Console is accessible from the host. If set tofalse, accessing from the host under Docker Desktop / bridged networking returns 403 — remote access is only allowed whenCONSOLE_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_KEYalong in step 1 per the prerequisites, just save directly here. Only if you skippedENCRYPTION_KEYwill saving at this step reportencryption 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 → 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
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:
$ 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:latestPOSTGRES_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):
| 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"+ emptypostgres_url(see theDockerfile). WhenLICENSE_KEYis 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 ifSTORAGE_MODEis stillpostgresqlwhilePOSTGRES_URLis empty, startup is rejected by config validation withserver.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 inPOSTGRES_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 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 |
# docker-compose.yml snippet
services:
protoflux:
image: protoflux:latest
ulimits:
nofile:
soft: 1048576
hard: 1048576Two 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:
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, onlyrmthe container and keep the data volume. After deleting only the container and runningdocker runagain 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.
