12 KiB
Soft Serve 安装指南(Docker Compose + Traefik TCP + CNAME)
1. 目标与最终形态
-
域名:
repo.windy.me -
DNS:
repo.windy.meCNAME →us2.wsvc.info -
部署主机:
us2.wsvc.info对应的 VPS(本文称 “us2”) -
Soft Serve 镜像:
ghcr.io/charmbracelet/soft-serve:latest -
数据持久化:宿主机
./data→ 容器/var/lib/soft-serve -
访问方式:SSH(Soft Serve SSH 服务端口为容器内
23231) -
暴露方式(推荐):Traefik TCP entrypoint
ssh监听宿主机2222,转发到容器23231
2. 前置条件清单
2.1 DNS(CNAME)
你已设置:
repo.windy.meCNAME →us2.wsvc.info
关键含义:
-
用户访问
repo.windy.me时,最终会解析到 us2 的公网 IP -
只要 us2 上对外开放 SSH 入口端口(示例:2222),访问就成立
建议验证(任意机器):
dig +short repo.windy.me CNAME
dig +short repo.windy.me A
2.2 网络与防火墙
在 us2 上确保对外放行你用于 Soft Serve SSH 的端口(示例 2222):
- 入站允许:TCP 2222
2.3 Traefik 已存在并使用外部网络
你当前 compose 使用:
- external network:
vw-net
确保 Traefik 容器也在同一个 vw-net 网络内。
3. 准备目录与配置文件
在 us2 上:
mkdir -p /opt/soft-serve
cd /opt/soft-serve
mkdir -p data
最终结构:
/opt/soft-serve/
compose.yml
.env
data/
4. 准备初始化管理员公钥(必须)
Soft Serve 首次启动会根据环境变量写入初始管理员 key。你已经验证的公钥写法如下(单行):
.env:
SOFT_SERVE_INITIAL_ADMIN_KEYS=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE9irsGu03p+1xrwIfzrzjGZCcExJ/XFEgkqsgfEN70j windy@windy-mbp
注意事项:
-
必须是 完整公钥的一整行
-
只在 数据目录首次初始化(空目录) 时生效
5. Docker Compose(推荐:Traefik TCP 暴露 SSH)
compose.yml(与你当前成功的结构一致,并保留注释):
services:
soft-serve:
image: ghcr.io/charmbracelet/soft-serve:latest
container_name: soft-serve
restart: unless-stopped
environment:
SOFT_SERVE_DATA_PATH: /var/lib/soft-serve
SOFT_SERVE_INITIAL_ADMIN: windy
SOFT_SERVE_INITIAL_ADMIN_KEYS: ${SOFT_SERVE_INITIAL_ADMIN_KEYS}
volumes:
- ./data:/var/lib/soft-serve
# 方案B:直连端口映射(不走 Traefik)
# ports:
# - "2222:23231"
networks:
- traefik
labels:
- traefik.enable=true
# SSH over TCP via Traefik (entryPoint ssh -> container port 23231)
- traefik.tcp.routers.softserve-ssh.entrypoints=ssh
- traefik.tcp.routers.softserve-ssh.rule=HostSNI(`*`)
- traefik.tcp.routers.softserve-ssh.tls=false
- traefik.tcp.services.softserve-ssh.loadbalancer.server.port=23231
networks:
traefik:
external: true
name: vw-net
关于 “SSH 不能走 Traefik 代理域名分流”的结论
-
SSH 不是 HTTP;Traefik 在这里是 TCP 转发
-
不要使用
HostSNI(repo.windy.me)之类的规则来“按域名”分流 SSH(会引发 TLS/HostSNI 相关报错) -
最稳妥的做法就是:
-
tls=false -
HostSNI('*') -
依赖端口入口(2222)
-
6. Traefik 静态配置要求(必须有 entrypoint)
你必须在 Traefik 的静态配置中定义 ssh entrypoint,并监听对外端口(示例:2222)。
示例(只示意关键段):
entryPoints:
ssh:
address: ":2222"
如果缺失,会出现典型错误:
EntryPoint doesn't exist entryPointName=ssh
7. 首次启动与“只初始化一次”的规则
7.1 首次启动
在 /opt/soft-serve:
docker compose up -d
docker compose ps
7.2 初始化只发生一次(关键规则)
如需重新初始化(比如 .env 修改后不生效),必须清空数据目录:
docker compose down
rm -rf ./data
mkdir -p ./data
docker compose up -d
8. 客户端连接与“user not found”修正方法
8.1 强制使用指定 key(排错与首次推荐)
你最终验证成功的关键点是:固定 key + IdentitiesOnly。
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 -p 2222 repo.windy.me info
若成功会输出类似:
Username: admin (或 windy)
Admin: true
Public keys: ...
8.2 把默认用户名从 admin 改成 windy
你已成功的改名命令(注意同样要固定 key):
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 -p 2222 repo.windy.me set-username windy
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 -p 2222 repo.windy.me info
解释:“user not found” 的真实根因通常不是 Soft Serve 没用户,而是 SSH 客户端未固定 key 时选用了另一把 key,导致 Soft Serve 无法把该连接映射到已存在的用户。
8.3 永久固化:写 ~/.ssh/config
在本机写入:
Host repo.windy.me
HostName repo.windy.me
Port 2222
User git
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
之后即可:
ssh repo.windy.me info
ssh repo.windy.me repo list
9. 创建仓库与 Git clone/push
9.1 创建仓库
ssh repo.windy.me repo create test
ssh repo.windy.me repo list
9.2 Clone(推荐写法)
写法 A(最清晰):
git clone ssh://repo.windy.me:2222/test.git
写法 B(scp 风格,依赖 ssh config 的 Port):
git clone repo.windy.me:test.git
9.3 Push 验证
cd test
echo "# test" > README.md
git add .
git commit -m "init"
git push
10. 常见故障排查(快速定位)
10.1 连接到错误端口
现象:你以为是 23231,但实际对外是 2222(由 Traefik entrypoint 决定)。
验证(在 us2 上):
ss -lntp | grep :2222
应看到 Traefik 监听 2222。
10.2 EntryPoint doesn't exist entryPointName=ssh
原因:Traefik 静态配置未定义 entryPoints.ssh。
修复:给 Traefik 增加:
entryPoints:
ssh:
address: ":2222"
并重启 Traefik。
10.3 Error: user not found
高概率原因:SSH 客户端用了“另一把 key”。
修复(强制固定 key):
ssh -vvv -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 -p 2222 repo.windy.me info
观察日志中是否出现:
-
Offering public key: ... id_ed25519 -
Server accepts key: ... id_ed25519
11. 备份与恢复(生产建议)
11.1 需要备份的内容
Soft Serve 核心数据都在宿主机 ./data(映射自 /var/lib/soft-serve):
-
soft-serve.db(用户/设置) -
repos/(仓库数据,如存在) -
ssh/(host keys 等)
11.2 最简单备份命令
在 us2 上:
cd /opt/soft-serve
tar -czf soft-serve-backup-$(date +%F).tar.gz ./data
恢复流程:
-
docker compose down -
解压覆盖
./data -
docker compose up -d
12. 推荐的“最终检查清单”
-
repo.windy.meCNAME 指向us2.wsvc.info,并能解析到 us2 IP -
us2 对外开放 TCP 2222
-
Traefik 静态配置存在
entryPoints.ssh=:2222 -
Soft Serve 数据目录持久化:
./data:/var/lib/soft-serve -
客户端
~/.ssh/config固定IdentityFile+IdentitiesOnly yes -
ssh repo.windy.me info输出Username: windy且Admin: true
如果你希望我把这份指南进一步“落地化”,我可以再给你两个附录(可选):
-
一份生产级 compose(healthcheck、日志限制、只读 filesystem、资源限制)
-
一份 Traefik 静态配置片段(与你当前 Traefik 容器的真实配置方式对齐:file provider / command args / labels)
Use user name and password for nghttpx
User name:
windy
Password:
meeQuan4jeinging
htpasswd -c ./passwords windy
Passwords
windy:$apr1$5usfjVkQ$Zsd27eX..5sZjFtRVTVjn1
Squid. Conf
http_port 3128
# Define the authentication program
auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/passwords
auth_param basic realm windy private https proxy
auth_param basic children 5
auth_param basic credentialsttl 2 hours
# Define the ACL (Access Control List) for authenticated users
acl authenticated_users proxy_auth REQUIRED
# Allow access only to authenticated users
http_access allow authenticated_users
# 关闭 Via 头 (隐私保护)
via off
forwarded_for off
# 日志设置
access_log stdio:/var/log/squid/access.log combined
Compose
squid:
image: ubuntu/squid:latest
container_name: squid-backend
restart: always
volumes:
- ./squid.conf:/etc/squid/squid.conf
- ./passwords:/etc/squid/passwords:ro
- squid_cache:/var/spool/squid
command: ["squid", "-N", "-d", "1"]
networks:
- internal-net
You are absolutely correct to verify the documentation. I apologize for the previous error—nghttpx does NOT support native Basic Authentication (username/password) for frontend clients. The option frontend-http-user (and basic-auth-file) does not exist in the official nghttpx documentation.
Since your architecture is Client -> nghttpx (TLS) -> Squid (Proxy), the correct place to handle authentication is in Squid, not nghttpx.
Here is the corrected, documentation-verified setup.
1. The Strategy
We will configure Squid to handle the username/password verification. nghttpx will simply pass the credentials (the Proxy-Authorization header) from the user to Squid.
2. Create the Password File
You need a tool like htpasswd (from apache2-utils) to create a hashed password file. Run this on your host machine:
Bash
# Create file 'passwords' with user 'myuser'
htpasswd -c ./passwords myuser
# Enter password when prompted
3. Update docker-compose.yml
You need to mount this password file into the Squid service (not nghttpx). You also need to mount a custom squid.conf.
YAML
services:
# ... nghttpx service remains the same ...
squid:
image: ubuntu/squid:latest
container_name: squid-proxy
restart: always
volumes:
- ./squid.conf:/etc/squid/squid.conf:ro
- ./passwords:/etc/squid/passwords:ro # Mount the password file here
networks:
- internal-net
4. Create squid.conf
Create a squid.conf file in the same directory. This configuration enables Basic Authentication using the file we just created.
Code snippet
# Define the authentication program
auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/passwords
auth_param basic realm My Private Proxy
auth_param basic children 5
auth_param basic credentialsttl 2 hours
# Define the ACL (Access Control List) for authenticated users
acl authenticated_users proxy_auth REQUIRED
# Allow access only to authenticated users
http_access allow authenticated_users
# Deny everything else
http_access deny all
# Listen on port 3128
http_port 3128
(Note: The path /usr/lib/squid/basic_ncsa_auth is standard for Ubuntu/Debian based Squid images. If you use Alpine, it might be /usr/lib/squid/basic_ncsa_auth or similar, check apk info -L squid if it fails).
5. Clean up nghttpx.conf
Revert nghttpx.conf to its clean state (remove the invalid auth lines). nghttpx must be configured to pass the traffic transparently.
Code snippet
# nghttpx.conf
# Frontend: Listen on 3000, no TLS (Traefik handles TLS)
frontend=0.0.0.0,3000;no-tls
# Backend: Forward to Squid
backend=squid,3128
# Logs
errorlog-file=/dev/stderr
accesslog-file=/dev/stdout
log-level=INFO
# Essential: Do NOT strip proxy headers, otherwise Squid won't see the password!
strip-incoming-x-forwarded-for=no
6. Verify and Apply
-
Restart:
docker-compose up -d --force-recreate -
Test:
Bash
curl -v -x https://us2.wsvc.info:443 -U myuser:mypassword http://google.com
Why this works:
-
The client sends a request with a
Proxy-Authorizationheader. -
Traefik decrypts TLS and passes it to
nghttpx. -
nghttpxforwards the request (including the header) tosquid. -
squidchecks the header against the/etc/squid/passwordsfile. -
If valid,
squidallows the traffic.
Would you like me to generate the squid.conf specifically for an Alpine based Squid image if you are using one to keep the image size small?