Deploying a QQ Bot on Linux with NapCat, NoneBot2, GsCore, and Caddy
Published
Updated
I wanted a QQ bot setup on Linux that was not just “working for now,” but stable enough to live with. The stack I ended up with was NapCat + NoneBot2 + GsCore, with Caddy in front for HTTPS and reverse proxying. The overall design is straightforward. The actual deployment, though, was shaped by a series of very real and very ordinary problems: terminal encoding, missing CLI tools, local-only bindings, TLS validation, and even something as basic as forgotten DNS records.
This post turns that whole session into a practical guide you can follow from scratch. More importantly, it includes the failures along the way, because those are usually the parts that save the most time later.
The Final Architecture
This is the message path we ended up with:
QQ / group message
-> NapCat
-> NoneBot2
-> nonebot-plugin-genshinuid
-> GsCore
-> actual business plugin (such as GenshinUID)
For admin access from the web, we added another layer:
Browser
-> Caddy (HTTPS / reverse proxy)
-> NapCat WebUI / GsCore WebConsole
Two design decisions mattered a lot here:
- Treat the bot message path and the admin panel path as separate concerns.
- Avoid exposing
6099and8765directly to the public internet when a reverse proxy can keep them behind localhost.
What Each Piece Does
NapCat: the protocol side and QQ login layer.NoneBot2: the bot framework itself, responsible for receiving OneBot events and dispatching logic.GsCore: the Sayu core layer that provides the plugin ecosystem and a unified connection model.nonebot-plugin-genshinuid: the official NoneBot2-side plugin used to connect to GsCore.Caddy: the HTTPS and reverse proxy layer for safely exposing the admin panels.
Environment Assumptions
This guide assumes:
- You are on Ubuntu, Debian, or a similar Linux system.
- You have
rootorsudoaccess. - The server has internet access.
- You have a QQ account ready for bot login.
- You have a domain name and plan to manage DNS with Cloudflare.
The directory layout in this guide looks like this:
~/qqbot # NoneBot2 project
~/gsuid_core # GsCore installation
Step 1: Install NapCat
NapCat provides a Linux installer, and the straightforward way to start is:
curl -o napcat.sh https://nclatest.znin.net/NapNeko/NapCat-Installer/main/script/install.sh
bash napcat.sh --tui
After installation, you can usually re-enter its TUI with:
sudo napcat
Problem 1: NapCat Shell showed garbled text
The first thing we saw was not a clean UI, but broken characters like ~@~A and ~M~U. This turned out not to be a NapCat bug at all. It was a classic locale and terminal issue.
Start by checking:
locale
echo $LANG
echo $TERM
If the environment is not using UTF-8, fix it temporarily before launching NapCat:
export LANG=zh_CN.UTF-8
export LC_ALL=zh_CN.UTF-8
export TERM=xterm-256color
sudo napcat
If the server does not have the locale generated yet, install it:
sudo apt update
sudo apt install -y locales
sudo locale-gen zh_CN.UTF-8
sudo update-locale LANG=zh_CN.UTF-8
If you prefer a neutral UTF-8 setup instead of a Chinese locale, C.UTF-8 works well too:
export LANG=C.UTF-8
export LC_ALL=C.UTF-8
export TERM=xterm-256color
sudo napcat
Log in to NapCat WebUI
NapCat’s WebUI typically listens on port 6099. Once it starts, check the logs for the access URL and token, scan the QQ login QR code, and set a proper WebUI password.
Step 2: Create the NoneBot2 Project
Create a Python virtual environment first:
mkdir -p ~/qqbot
cd ~/qqbot
python -m venv .venv --prompt nonebot2
source .venv/bin/activate
Install the base dependencies:
pip install "nonebot2[fastapi]"
pip install nonebot-adapter-onebot
Create .env:
HOST=127.0.0.1
PORT=8080
ONEBOT_ACCESS_TOKEN=replace-this-with-a-long-random-token
COMMAND_START=["/"]
COMMAND_SEP=["."]
Create bot.py:
import nonebot
from nonebot.adapters.onebot.v11 import Adapter as OneBotV11Adapter
nonebot.init()
driver = nonebot.get_driver()
driver.register_adapter(OneBotV11Adapter)
if __name__ == "__main__":
nonebot.run()
Test that it starts:
source .venv/bin/activate
python bot.py
Step 3: Connect NapCat to NoneBot2
In NapCat, create a new WebSocket client network connection and point it to:
ws://127.0.0.1:8080/onebot/v11/ws
Use the same token value as ONEBOT_ACCESS_TOKEN in your .env file.
If you get a 403, the first thing to verify is the token value. A mismatch there is by far the most common cause.
Step 4: Install GsCore
According to the official GsCore docs, install it as a separate project next to your bot:
cd ~
git clone https://github.com/Genshin-bots/gsuid_core.git --depth=1 --single-branch
cd gsuid_core
The official docs recommend uv:
uv python install 3.13
uv sync --python 3.13
uv run python -m ensurepip
Start GsCore:
uv run core
On first startup, it will generate:
~/gsuid_core/data/config.json
~/gsuid_core/data/core_config.json
Step 5: Connect GsCore to NoneBot2
The official NoneBot2-side connector for GsCore is nonebot-plugin-genshinuid.
Problem 2: nb was not found
We initially followed the official pattern and ran:
nb plugin install nonebot-plugin-genshinuid
That failed with:
-bash: nb: command not found
Nothing was wrong with the project. The issue was simply that nb-cli was not installed. The nb command is not part of Python itself; it belongs to the NoneBot CLI.
There are two reasonable fixes.
Fix A: install nb-cli
cd ~/qqbot
source .venv/bin/activate
pip install nb-cli
nb plugin install nonebot-plugin-genshinuid
Fix B: install the plugin directly with pip
If you already manage your bot with a hand-written bot.py, direct installation is often simpler:
cd ~/qqbot
source .venv/bin/activate
pip install nonebot-plugin-genshinuid
That second route is the one I would recommend in this setup. It is shorter and avoids pulling in an extra layer unless you actually want the CLI workflow.
Add GsCore settings to the NoneBot .env
Append these values to your existing .env:
gsuid_core_ws_token=123
gsuid_core_host=localhost
gsuid_core_port=8765
gsuid_core_botid=NoneBot2
Configure config.json on the GsCore side
Edit:
~/gsuid_core/data/config.json
At minimum, make sure these fields are set:
{
"HOST": "localhost",
"PORT": "8765",
"masters": ["your-qq-number"],
"WS_TOKEN": "123",
"TRUSTED_IPS": ["127.0.0.1"]
}
Two details matter here:
WS_TOKENmust matchgsuid_core_ws_tokenin the NoneBot.envmastersshould include your own QQ number, or a lot of core admin commands will not work for you later
Step 6: Install the Actual Business Plugin
GsCore by itself is the platform layer. The actual bot features come from business plugins.
Once your account is listed in masters, you can install one from chat:
core安装插件GenshinUID
If you prefer manual installation:
cd ~/gsuid_core/plugins
git clone -b v4 https://github.com/KimigaiiWuyi/GenshinUID.git --depth=1 --single-branch
Restart GsCore once the plugin is installed.
Step 7: Access the GsCore Admin Panel
GsCore’s admin backend is the Web Console, usually available at:
http://127.0.0.1:8765/app
That is enough if you are accessing it locally.
Problem 3: public access returned 502
When we tried to access the admin panel from the public internet, we ran into HTTP ERROR 502. The clue came from GsCore itself:
WebConsole挂载于本地, 如想外网访问请修改data/config.json中host为0.0.0.0!
That message means the web console is bound to localhost only, which is why outside access fails. The most direct fix is to change:
"HOST": "localhost"
to:
"HOST": "0.0.0.0"
That said, exposing 8765 directly is not the design I would recommend. A better pattern is to keep GsCore bound to localhost and let a reverse proxy expose it safely.
Step 8: Put Caddy in Front of NapCat and GsCore
Operationally, it is cleaner not to expose 6099 and 8765 to the public internet at all. The safer layout is:
- Keep NapCat WebUI and GsCore WebConsole bound to localhost
- Expose only
80and443 - Let
Caddyhandle HTTPS and reverse proxying
Install Caddy
On Ubuntu or Debian, one clean installation route is:
sudo apt update
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
Recommended upstream layout
Keep the backend services local:
- NapCat WebUI ->
127.0.0.1:6099 - GsCore WebConsole ->
127.0.0.1:8765
Then configure /etc/caddy/Caddyfile like this:
{
email yourmail@example.com
}
napcat.example.com {
reverse_proxy 127.0.0.1:6099
}
gscore.example.com {
reverse_proxy 127.0.0.1:8765
}
Format, validate, and reload:
sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl restart caddy
sudo systemctl status caddy
Step 9: Problem 4 — Caddy could not obtain certificates
Caddy itself started just fine, but the logs kept showing challenge failed. The root cause was not in Caddy at all. I had forgotten to add the DNS records in Cloudflare.
That type of failure usually comes down to one of these:
- The domain does not actually resolve to this server
- Ports
80and443are not publicly reachable - Another service is already occupying
80or443 - DNS changes have not propagated yet
Useful troubleshooting commands
Check recent Caddy logs:
sudo journalctl -u caddy -n 80 --no-pager
Check DNS resolution:
dig +short napcat.your-domain.com A
dig +short gscore.your-domain.com A
Check what is listening:
ss -lntp | grep ':80\|:443'
Check the firewall:
sudo ufw status
If the DNS record simply does not exist in Cloudflare, ACME validation obviously cannot succeed. It is a basic mistake, but also an extremely common one.
The Security Posture I Would Actually Recommend
If the admin panels are mostly for your own use, I would tighten things up this way:
- Do not expose
6099or8765directly to the internet - Expose only
80and443 - Keep NapCat and GsCore bound to localhost
- Put
Caddyin front of them - Keep strong passwords and tokens on the panels themselves
- Add IP allowlisting or another auth layer if you can
One point is worth stating explicitly: your email address is not an authentication mechanism. It is only a contact address for certificate issuance. The things that actually protect your admin panels are HTTPS, reverse proxy boundaries, panel passwords, tokens, and whether you avoided exposing the raw upstream ports.
Recommended Startup Order
Once everything is deployed, I recommend starting and debugging in this order:
NapCatNoneBot2GsCore- business plugin
Caddy
And when something breaks, trace the same chain in order:
Is QQ login working?
-> Is the OneBot WebSocket connected?
-> Is NoneBot running?
-> Is GsCore connected?
-> Is the plugin loaded?
-> Is the admin panel reachable?
-> Are HTTPS, DNS, and Caddy behaving correctly?
The Four Things Most Worth Remembering
1. Garbled TUI output usually means the terminal environment is wrong
Check LANG, LC_ALL, and TERM before assuming the software is broken.
2. A missing nb command usually just means nb-cli is not installed
And if you are already managing your bot with a custom bot.py, direct pip install may be the simpler path anyway.
3. If a backend is bound to localhost, public access is supposed to fail
Once you see localhost bindings or “mounted locally” warnings, stop guessing at public-network issues and fix the binding model first.
4. TLS failures are often not “proxy problems” at all
Even in a stack with reverse proxies, certificates, and HTTPS, the most common failure point is still the boring layer underneath: DNS.
References
- NapCat docs
- NoneBot2 docs
- OneBot adapter docs
- GsCore docs
- GsCore repository
- Caddy docs
- Caddy Automatic HTTPS
- Caddy reverse_proxy
Closing Thoughts
By the end of this deployment, the biggest takeaway was not “the bot is finally installed.” It was that the hard part of this kind of work is rarely the install commands themselves. What determines whether the setup stays pleasant later is whether the boundaries are clear: whether the protocol layer is separated from the framework, whether admin surfaces are exposed carelessly, whether the domains really point where you think they do, and whether the logs tell you where the failure actually is.
Once those boundaries are in place, NapCat + NoneBot2 + GsCore + Caddy is a very workable combination. It is not the lightest stack, and it is not the flashiest one either, but it is clear, stable, and perfectly suited to long-term maintenance.