Docker deployment
Run SnowLuma on Linux with the official Docker image
On Linux, SnowLuma runs only via Docker (host-manual is advanced/unofficial). The image bundles Linux QQ + Xvfb + VNC + noVNC + supervisord + the SnowLuma lite distribution, so the host needs nothing beyond Docker.
This page is the recommended Linux path. Image build scripts and Compose templates live in SnowLuma/SnowLuma.Docker.Framework. There is also an experimental one-click installer in Alpha — not the default.
Image
| Item | Value |
|---|---|
| Image | motricseven7/snowluma:latest (or a specific tag, e.g. :v1.10.0) |
| Arch | linux/amd64, linux/arm64 |
| Base | node:22-bookworm-slim |
| Bundled | Linux QQ, Xvfb, VNC, noVNC, supervisord, SnowLuma lite |
The image does not rebuild SnowLuma — it consumes the prebuilt lite tarball from each SnowLuma GitHub Release, so the release and the image build are decoupled.
Ports
| Port | Purpose |
|---|---|
6081 | noVNC (browser desktop for QR login) |
5099 | SnowLuma WebUI |
3000 | OneBot HTTP (default) |
3001 | OneBot WebSocket (default) |
Official templates publish 6081 only. They do not map raw VNC 5900. Add -p 127.0.0.1:5900:5900 yourself if you need a native VNC client.
Remote-desktop password
The official image has no public fixed password. Do not copy sample values from old docs or random posts.
- Leave
VNC_PASSWDunset: the first start generates a random password and prints it once in the container logs (one Chinese line, one English line). It is kept on the data volume. Recreating the container or upgrading the image does not change it, as long as the volume is still there, and it will not be printed again. - Set your own password: pass
VNC_PASSWDat start. The password is rotated only when you change this value, and the new one is printed once. - If the variable still holds the old public default from earlier docs, the next start treats it as unset and replaces it with a new random password.
Do not publish 6081 on an untrusted network either.
Read the current password (it only appears when generated or when you rotate it):
docker logs snowluma 2>&1 | grep -E "远程桌面密码:|remote desktop password:" | tail -n 1If a later restart no longer shows it, that is expected — use the value you saved the first time.
ptrace permissions
Warning
--cap-add=SYS_PTRACE and --security-opt seccomp=unconfined are still required.
SnowLuma's native addon uses ptrace to inject the hook into the in-container QQ process. The official image grants cap_sys_ptrace to /usr/local/bin/node, so SnowLuma can run as the unprivileged snowluma user while still injecting within the container's capability boundary.
With current official images, you no longer need to change the host's kernel.yama.ptrace_scope. Keep these container options instead:
--cap-add=SYS_PTRACE
--security-opt seccomp=unconfinedTo verify the file capability inside the container:
docker exec snowluma getcap /usr/local/bin/node
# expected to include: cap_sys_ptrace=epIf an old image still asks you to relax the host sysctl, upgrade the image first. Changing kernel.yama.ptrace_scope should only be a temporary troubleshooting fallback when upgrading is not possible.
One-line docker run
docker run -d \
--name snowluma \
--restart unless-stopped \
--shm-size=1g \
--ulimit nofile=65536:1048576 \
--cap-add=SYS_PTRACE \
--security-opt seccomp=unconfined \
-e SNOWLUMA_WEBUI_HOST=0.0.0.0 \
-e SNOWLUMA_WEBUI_PORT=5099 \
-e SNOWLUMA_QQ_FLAGS="--disable-gpu --disable-software-rasterizer --disable-gpu-compositing" \
-p 6081:6081 \
-p 5099:5099 \
-p 3000:3000 \
-p 3001:3001 \
-v qq-gateway-data:/app/data \
-v qq-client-config:/app/.config \
-v qq-client-data:/app/.local/share \
motricseven7/snowluma:latestWarning
--cap-add=SYS_PTRACE and --security-opt seccomp=unconfined are mandatory. SnowLuma's native addon uses ptrace to inject the hook into the QQ process; the default seccomp profile blocks that.
--shm-size=1g is needed for QQ's Chromium runtime — the default container /dev/shm is too small and QQ will crash.
docker-compose
Recommended over docker run. The repo's docker-compose.yml:
services:
snowluma:
image: ${SNOWLUMA_IMAGE:-motricseven7/snowluma:latest}
container_name: ${SNOWLUMA_CONTAINER:-snowluma}
restart: unless-stopped
shm_size: 1gb
ulimits:
nofile:
soft: 65536
hard: 1048576
cap_add:
- SYS_PTRACE
security_opt:
- seccomp=unconfined
environment:
VNC_PASSWD: ${VNC_PASSWD:-}
SNOWLUMA_UID: ${SNOWLUMA_UID:-1000}
SNOWLUMA_GID: ${SNOWLUMA_GID:-1000}
SNOWLUMA_WEBUI_HOST: ${SNOWLUMA_WEBUI_HOST:-0.0.0.0}
SNOWLUMA_WEBUI_PORT: ${SNOWLUMA_WEBUI_PORT:-5099}
SNOWLUMA_LOG_LEVEL: ${SNOWLUMA_LOG_LEVEL:-info}
SNOWLUMA_SCREEN: ${SNOWLUMA_SCREEN:-1920x1080x24}
SNOWLUMA_HOOK_AUTOLOAD: ${SNOWLUMA_HOOK_AUTOLOAD:-1}
SNOWLUMA_EXTRA_QQ_HOMES: "${SNOWLUMA_EXTRA_QQ_HOMES:-}"
SNOWLUMA_QQ_FLAGS: "${SNOWLUMA_QQ_FLAGS:---disable-gpu --disable-software-rasterizer --disable-gpu-compositing}"
ports:
- "${NOVNC_PORT:-6081}:6081"
- "${SNOWLUMA_WEBUI_HOST_PORT:-5099}:${SNOWLUMA_WEBUI_PORT:-5099}"
- "${ONEBOT_HTTP_PORT:-3000}:3000"
- "${ONEBOT_WS_PORT:-3001}:3001"
volumes:
- qq-gateway-data:/app/data
- qq-client-config:/app/.config
- qq-client-data:/app/.local/share
volumes:
qq-gateway-data:
name: qq-gateway-data
qq-client-config:
name: qq-client-config
qq-client-data:
name: qq-client-dataStart:
docker compose up -dUpgrade:
docker compose pull
docker compose up -dEnvironment variables
| Var | Default | Notes |
|---|---|---|
VNC_PASSWD | (empty) | Remote-desktop password. Leave empty to generate a random one on first start (printed once in the logs). Set it only if you want your own password. |
TZ | Asia/Shanghai | Container timezone. Set in Dockerfile, overridable via env. |
SNOWLUMA_UID | 1000 | uid of the in-container snowluma user. Match it to the host owner of your mounted volumes. |
SNOWLUMA_GID | 1000 | gid of the in-container snowluma user. |
SNOWLUMA_WEBUI_HOST | 0.0.0.0 | WebUI bind address. Docker examples listen on all container interfaces so published ports work. |
SNOWLUMA_WEBUI_PORT | 5099 | WebUI listen port (inside the container). |
SNOWLUMA_WEBUI_HOST_PORT | 5099 | WebUI host mapping port (defaults to same as container port). |
SNOWLUMA_LOG_LEVEL | info | error / warn / info / debug. |
SNOWLUMA_SCREEN | 1920x1080x24 | Xvfb resolution + color depth. |
SNOWLUMA_HOOK_AUTOLOAD | 1 | Image opts into auto-injection by default. Set 0 to fall back to the manual Load workflow. |
SNOWLUMA_EXTRA_QQ_HOMES | empty | Comma- or space-separated extra QQ /app/... HOME paths for auto-starting multiple accounts. |
SNOWLUMA_QQ_FLAGS | --disable-gpu --disable-software-rasterizer --disable-gpu-compositing | Flags passed to Linux QQ. The default disables GPU / SwiftShader software rendering to prevent memory leaks. |
NOVNC_PORT | 6081 | noVNC host mapping port. Official templates publish only this remote-desktop port. |
ONEBOT_HTTP_PORT | 3000 | OneBot HTTP host mapping port. |
ONEBOT_WS_PORT | 3001 | OneBot WebSocket host mapping port. |
Environment variables take precedence over runtime.json. To restrict the WebUI to the container itself, set SNOWLUMA_WEBUI_HOST to 127.0.0.1 and recreate the container.
Volumes
| Volume | Container path | Contents |
|---|---|---|
qq-gateway-data | /app/data | SnowLuma config, cache, SQLite. Includes config/onebot.json and config/runtime.json. |
qq-client-config | /app/.config | QQ client config. |
qq-client-data | /app/.local/share | QQ user data (login state, cache). |
Don't delete these on upgrade — you'll lose login state and configuration.
First-time login
- Open
http://<host>:6081/(noVNC) and enter the remote-desktop password from the logs (see Remote-desktop password). - Inside the remote desktop you'll see QQ already running. Scan the QR to log in.
- Once login completes the hook auto-switches from passive observation to working mode — no need to click Load in the WebUI.
- Open
http://<host>:5099/for the SnowLuma WebUI.
Finding the WebUI bootstrap password
On a fresh data volume the first launch logs a one-time password:
docker logs snowluma 2>&1 | grep -E "临时密码|initial credentials" | tail -n 1Just the password string:
docker logs snowluma 2>&1 | \
sed -nE 's/.*(临时密码: |initial credentials: user=admin password=)([^[:space:]]+).*/\2/p' | tail -n 1Info
The bootstrap password is logged only on a brand-new data volume's first boot. Reusing the volume or restarting won't regenerate it.
Running multiple QQ accounts
Linux QQ ships a single-instance lock — clicking the QQ icon again on a desktop that already has one running just focuses the existing window, it doesn't start a second process. The lock file lives at $HOME/.config/QQ/SingletonLock, so the solution is to give each instance its own HOME.
SnowLuma needs no extra configuration: HookManager walks every QQ main process it can find, injects the hook into each, and spins up a separate OneBotInstance per UIN with its own config/onebot_<uin>.json.
Recommended: auto-start through an environment variable
Mount one dedicated volume per extra account, then list the corresponding container paths in SNOWLUMA_EXTRA_QQ_HOMES. The list can be comma- or space-separated:
services:
snowluma:
# ... existing config ...
environment:
SNOWLUMA_EXTRA_QQ_HOMES: /app/qq-acct2,/app/qq-acct3
volumes:
- qq-gateway-data:/app/data
- qq-client-config:/app/.config # primary account
- qq-client-data:/app/.local/share # primary account
- qq-client-account-2:/app/qq-acct2 # second account's full HOME
- qq-client-account-3:/app/qq-acct3 # third account...
volumes:
qq-gateway-data:
name: qq-gateway-data
qq-client-config:
name: qq-client-config
qq-client-data:
name: qq-client-data
qq-client-account-2:
name: qq-client-account-2
qq-client-account-3:
name: qq-client-account-3On boot, the container generates one supervisor program per extra HOME. Each extra QQ runs as the snowluma user with the same DISPLAY=:1 and the same SNOWLUMA_QQ_FLAGS, so injection won't fail because a manual docker exec accidentally launched QQ as root.
Check process status:
docker exec snowluma supervisorctl status
# qq, qq-extra-1, qq-extra-2, and snowluma should be RUNNING or startingTemporarily launch one extra account by hand
For a one-off launch, keep the user and environment explicit. The important bit is: do not run the extra QQ as root.
docker exec -u snowluma \
-e DISPLAY=:1 \
-e HOME=/app/qq-acct2 \
-d snowluma \
sh -lc 'qq --no-sandbox ${SNOWLUMA_QQ_FLAGS}'If /app/qq-acct2 is not mounted to a volume, recreating the container loses that account's login state. For long-term use, prefer the Compose setup above.
Caveats
- One HOME per QQ instance — never share a
HOMEbetween two QQs; they'll fight over the lock file. - Resources: each QQ is a full Chromium process, ~300-500 MB RAM. The default
--shm-size=1gis fine for one; bump to2gb+ for multi-account. - All windows share the same Xvfb desktop: noVNC shows every QQ window; minimize the ones you've already logged into. Hook keeps working regardless of window state.
- WebUI lists every UIN automatically: no manual configuration — SnowLuma surfaces each new account in the WebUI and
get_login_inforeturns the new UINs once login completes. - Do not rely on desktop autostart folders: the image uses
fluxbox, not an XFCE/GNOME session.~/.config/autostart/*.desktopmay not be read. Let supervisor handle multi-account boot instead.
Auto-injection
The image defaults to SNOWLUMA_HOOK_AUTOLOAD=1: when the container starts, the hook is injected into the QQ process in passive-observe mode. Once you scan-login, the hook auto-promotes to working mode. supervisor also restarts QQ on crash through the same flow.
Turning it off
If you prefer the legacy "manual Load via WebUI" workflow:
docker run -e SNOWLUMA_HOOK_AUTOLOAD=0 ... motricseven7/snowluma:latestOr set SNOWLUMA_HOOK_AUTOLOAD: 0 in docker-compose.yml, or edit /app/data/config/runtime.json inside the volume to "hookAutoLoad": false. Environment variables win.
Common ops
Shell into the container:
docker exec -it snowluma bashFollow logs:
docker logs -f snowlumaJust SnowLuma's own logs (filter out QQ's noisy output):
docker logs -f snowluma 2>&1 | grep -v "QtNetwork\|GLib\|libQt6"Browse the data directory:
docker exec -it snowluma bash -c "cd /app/data && ls -la"Edit OneBot config and restart:
# edit /app/data/config/onebot_<uin>.json
docker restart snowlumaPort remapping
If a host port is taken, just shift the host side; container ports stay the same. Example moving OneBot HTTP to host 8080:
ports:
- "8080:3000" # host 8080 → container 3000Or via env (the Compose template already supports it):
ONEBOT_HTTP_PORT=8080 docker compose up -dBuilding the image yourself
Useful when you need an unreleased SnowLuma build:
# Build the lite tarball in the SnowLuma main repo
pnpm build:all
# Drop it into the Docker repo root with the expected name
cp dist/SnowLuma-*-linux-x64-lite.tar.gz \
../SnowLuma.Docker.Framework/SnowLuma.Framework.tar.gz
cd ../SnowLuma.Docker.Framework
./scripts/build-image.shOr let the script pull the latest release:
SNOWLUMA_TAG=v1.10.0 ./scripts/build-image.shMulti-arch manifests are merged by CI (.github/workflows/docker-image.yml); the local script only builds a single platform.
Security notes
- SnowLuma's native addon injects code into the QQ process; the container needs
SYS_PTRACEandseccomp=unconfined. Only run on trusted hosts. - Do not expose noVNC / WebUI / OneBot ports raw on the public internet. There is no shared default remote-desktop password, but an open port is still enough for someone else to connect.
- Comply with NTQQ / Tencent's terms of service plus SnowLuma's and its dependencies' open-source licenses.
Was this page helpful?
Last updated on