SnowLuma
Deployment

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

ItemValue
Imagemotricseven7/snowluma:latest (or a specific tag, e.g. :v1.10.0)
Archlinux/amd64, linux/arm64
Basenode:22-bookworm-slim
BundledLinux 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

PortPurpose
6081noVNC (browser desktop for QR login)
5099SnowLuma WebUI
3000OneBot HTTP (default)
3001OneBot 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_PASSWD unset: 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_PASSWD at 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 1

If 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=unconfined

To verify the file capability inside the container:

docker exec snowluma getcap /usr/local/bin/node
# expected to include: cap_sys_ptrace=ep

If 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:latest

Warning

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

Start:

docker compose up -d

Upgrade:

docker compose pull
docker compose up -d

Environment variables

VarDefaultNotes
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.
TZAsia/ShanghaiContainer timezone. Set in Dockerfile, overridable via env.
SNOWLUMA_UID1000uid of the in-container snowluma user. Match it to the host owner of your mounted volumes.
SNOWLUMA_GID1000gid of the in-container snowluma user.
SNOWLUMA_WEBUI_HOST0.0.0.0WebUI bind address. Docker examples listen on all container interfaces so published ports work.
SNOWLUMA_WEBUI_PORT5099WebUI listen port (inside the container).
SNOWLUMA_WEBUI_HOST_PORT5099WebUI host mapping port (defaults to same as container port).
SNOWLUMA_LOG_LEVELinfoerror / warn / info / debug.
SNOWLUMA_SCREEN1920x1080x24Xvfb resolution + color depth.
SNOWLUMA_HOOK_AUTOLOAD1Image opts into auto-injection by default. Set 0 to fall back to the manual Load workflow.
SNOWLUMA_EXTRA_QQ_HOMESemptyComma- or space-separated extra QQ /app/... HOME paths for auto-starting multiple accounts.
SNOWLUMA_QQ_FLAGS--disable-gpu --disable-software-rasterizer --disable-gpu-compositingFlags passed to Linux QQ. The default disables GPU / SwiftShader software rendering to prevent memory leaks.
NOVNC_PORT6081noVNC host mapping port. Official templates publish only this remote-desktop port.
ONEBOT_HTTP_PORT3000OneBot HTTP host mapping port.
ONEBOT_WS_PORT3001OneBot 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

VolumeContainer pathContents
qq-gateway-data/app/dataSnowLuma config, cache, SQLite. Includes config/onebot.json and config/runtime.json.
qq-client-config/app/.configQQ client config.
qq-client-data/app/.local/shareQQ user data (login state, cache).

Don't delete these on upgrade — you'll lose login state and configuration.

First-time login

  1. Open http://<host>:6081/ (noVNC) and enter the remote-desktop password from the logs (see Remote-desktop password).
  2. Inside the remote desktop you'll see QQ already running. Scan the QR to log in.
  3. Once login completes the hook auto-switches from passive observation to working mode — no need to click Load in the WebUI.
  4. 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 1

Just the password string:

docker logs snowluma 2>&1 | \
  sed -nE 's/.*(临时密码: |initial credentials: user=admin password=)([^[:space:]]+).*/\2/p' | tail -n 1

Info

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.

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

On 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 starting

Temporarily 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 HOME between 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=1g is fine for one; bump to 2gb+ 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_info returns the new UINs once login completes.
  • Do not rely on desktop autostart folders: the image uses fluxbox, not an XFCE/GNOME session. ~/.config/autostart/*.desktop may 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:latest

Or 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 bash

Follow logs:

docker logs -f snowluma

Just 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 snowluma

Port 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 3000

Or via env (the Compose template already supports it):

ONEBOT_HTTP_PORT=8080 docker compose up -d

Building 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.sh

Or let the script pull the latest release:

SNOWLUMA_TAG=v1.10.0 ./scripts/build-image.sh

Multi-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_PTRACE and seccomp=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

On this page