Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 53 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,37 +170,75 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Validate the production Compose file
run: docker compose config --quiet
- name: Build the image
run: docker build --build-arg VERSION="${RELEASE:-dev-${GITHUB_SHA::7}}" -t miaopanel:ci .
- name: Start it and check the login page answers
- name: Start it behind an exact trusted HTTPS proxy
run: |
docker run -d --name miaopanel -p 127.0.0.1:18765:18765 -v miaopanel-ci-data:/data \
-e MIAO_TRUSTED_PROXIES=172.16.0.0/12 miaopanel:ci
for i in $(seq 1 20); do curl -fsS -H 'X-Miao: 1' http://127.0.0.1:18765/api/auth/state && break; sleep 1; done
mkdir -p /tmp/miaopanel-proxy
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
-keyout /tmp/miaopanel-proxy/key.pem -out /tmp/miaopanel-proxy/cert.pem \
-subj '/CN=127.0.0.1' -addext 'subjectAltName=IP:127.0.0.1'
cat > /tmp/miaopanel-proxy/default.conf <<'EOF'
server {
listen 443 ssl;
ssl_certificate /etc/nginx/tls/cert.pem;
ssl_certificate_key /etc/nginx/tls/key.pem;
location / {
proxy_pass http://172.30.0.20:18765;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
}
EOF
docker network create --subnet 172.30.0.0/24 miaopanel-ci
docker run -d --name miaopanel --network miaopanel-ci --ip 172.30.0.20 \
-v miaopanel-ci-data:/data -e MIAO_TRUSTED_PROXIES=172.30.0.10 miaopanel:ci
docker run -d --name miaopanel-proxy --network miaopanel-ci --ip 172.30.0.10 \
-p 127.0.0.1:18443:443 -v /tmp/miaopanel-proxy/default.conf:/etc/nginx/conf.d/default.conf:ro \
-v /tmp/miaopanel-proxy:/etc/nginx/tls:ro nginx:alpine
for i in $(seq 1 20); do curl -kfsS -H 'X-Miao: 1' https://127.0.0.1:18443/api/auth/state && break; sleep 1; done
docker logs miaopanel 2>&1 | grep -q '初始化码'
curl -fsS -H 'X-Miao: 1' http://127.0.0.1:18765/api/auth/state | grep -q '"install":true'
curl -fsS -H 'X-Miao: 1' -H 'X-Forwarded-Proto: https' \
http://127.0.0.1:18765/api/auth/state | grep -q '"https":true'
- name: Check the install wizard and password login in a browser
curl -kfsS -H 'X-Miao: 1' https://127.0.0.1:18443/api/auth/state | grep -q '"install":true'
curl -kfsS -H 'X-Miao: 1' https://127.0.0.1:18443/api/auth/state | grep -q '"https":true'
- name: Check HTTPS setup, secure cookie, password, 2FA, and browser scrolling
env:
MIAO_TEST_BASE_URL: https://127.0.0.1:18443
MIAO_TEST_ENABLE_TOTP: "1"
MIAO_TEST_TOTP_FILE: /tmp/miaopanel-totp
run: python3 scripts/browser_smoke.py
- name: Restore a stopped data volume
- name: Restore a checked backup into a new volume
run: |
docker exec miaopanel sha256sum /data/secrets.json | cut -d' ' -f1 > /tmp/miaopanel-secrets.sha256
docker stop miaopanel
mkdir -p /tmp/miaopanel-backup
docker run --rm --user 0 --volumes-from miaopanel -v /tmp/miaopanel-backup:/backup \
--entrypoint sh miaopanel:ci -c 'umask 077; tar -C /data -czf /backup/miaopanel.tar.gz .'
sudo sha256sum /tmp/miaopanel-backup/miaopanel.tar.gz > /tmp/miaopanel-backup/miaopanel.tar.gz.sha256
sudo sha256sum -c /tmp/miaopanel-backup/miaopanel.tar.gz.sha256
docker volume create miaopanel-ci-restored
docker run --rm --user 0 -v miaopanel-ci-restored:/data -v /tmp/miaopanel-backup:/backup:ro \
--entrypoint sh miaopanel:ci -c 'tar -C /data -xzf /backup/miaopanel.tar.gz'
--entrypoint sh miaopanel:ci -c 'test -z "$(ls -A /data)" && tar -C /data -xzf /backup/miaopanel.tar.gz'
docker rm miaopanel
docker run -d --name miaopanel-restored -p 127.0.0.1:18765:18765 \
-v miaopanel-ci-restored:/data -e MIAO_TRUSTED_PROXIES=172.16.0.0/12 miaopanel:ci
docker run -d --name miaopanel-restored --network miaopanel-ci --ip 172.30.0.20 \
-v miaopanel-ci-restored:/data -e MIAO_TRUSTED_PROXIES=172.30.0.10 miaopanel:ci
for i in $(seq 1 20); do
if curl -fsS -H 'X-Miao: 1' http://127.0.0.1:18765/api/auth/state | grep -q '"setup":false'; then exit 0; fi
if curl -kfsS -H 'X-Miao: 1' https://127.0.0.1:18443/api/auth/state | grep -q '"setup":false'; then break; fi
sleep 1
done
docker logs miaopanel-restored
exit 1
curl -kfsS -H 'X-Miao: 1' https://127.0.0.1:18443/api/auth/state | grep -q '"setup":false'
test "$(docker exec miaopanel-restored sha256sum /data/secrets.json | cut -d' ' -f1)" = "$(cat /tmp/miaopanel-secrets.sha256)"
test "$(docker exec miaopanel-restored stat -c %a /data/secrets.json)" = 600
- name: Check restored account, 2FA, server record, key file, and secure cookie in a browser
env:
MIAO_TEST_BASE_URL: https://127.0.0.1:18443
MIAO_TEST_MODE: restored
MIAO_TEST_TOTP_FILE: /tmp/miaopanel-totp
run: python3 scripts/browser_smoke.py

release:
if: (github.ref_type == 'tag' && startsWith(github.ref_name, 'v')) || (github.event_name == 'workflow_dispatch' && startsWith(inputs.version, 'v'))
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- **访问统计**:「今天下午流量为什么涨了?」→ 基于 EO 数据分析自动归因(Top IP / URL / UA / 地区)。
- **服务器状态**:CPU、内存、磁盘、带宽、到期时间一屏总览;「现在是什么在吃内存?」直接问。
- **AI 排障**:「网站打不开」→ 按 DNS → EO → 网络 → 主机 → 应用逐层取证,给出结论、证据和修复方案。
- **优化建议,同意后自动执行**:先用数据判断是否真有问题,再给出具体改法;你同意后自动执行,失败自动回滚,事后汇报效果。1Panel、宝塔上的修改走面板自己的 API,面板里看得到、不会被覆盖。
- **优化建议,同意后自动执行**:先用数据判断是否真有问题,再给出具体改法;你同意后自动执行。支持保护的单个步骤失败时会恢复该步骤已经做出的改动;清单里此前成功的步骤保留,可在日志中手动回滚。1Panel、宝塔上的修改走面板自己的 API,面板里看得到、不会被覆盖。
- **看不懂命令也能安全使用**:AI 只能提交计划;模板覆盖不到时可以开启受限的 AI 自由命令,由系统替你检查命令、试运行、做快照,并设置「5 分钟保险」自动恢复。

打开就是**总览**:服务器、网站、证书、安全各一张卡片,下面是「需要你处理」的事(没封禁的高风险 IP、快到期的证书、等你确认的清单、服务器的问题),以及今天的访问和最近的变更。侧边栏按「服务器 · 网站 · 云服务」分组,服务器前面的小圆点表示状态;AI 给的清单、提醒和日报都在「待处理」里。**AI 侧栏**在任何页面都能打开(右下角「问 AI」,终端和文件页在标题栏右边,或者按 Ctrl+J / ⌘J),窗口够宽时页面会让出位置,它知道你正在看哪个页面、哪个网站,不用跳走。点侧边栏的服务器打开它的工作台,分「概览 · 网站 · 应用 · 安全与备份 · 执行记录 · 连接设置」:概览里有 CPU、内存、磁盘和流量包的圆环,体检发现的问题旁边就是「加 swap」「清理磁盘」这样的按钮(交给 AI 先查再出清单),还有这台服务器的网站、占内存最多的程序和最近的修改。按 Ctrl+K / ⌘K 可以搜服务器、网站和页面,输入「终端 blog」直接打开终端。
Expand Down Expand Up @@ -120,7 +120,7 @@ AI 能看存储桶的设置和文件列表,也能生成修改设置的清单

**对话记录**:和 AI 的对话会自动保存,关掉程序再打开也在,还能接着问;在 AI 助手页左侧的「对话记录」里切换或删除。

**日志和回滚**:「日志 → 执行日志」里能查到 Miao Panel 在服务器上执行过的每一条命令(包括 AI 的只读检查)和结果;改过的东西可以在这里一键回滚。能回滚的修改还会在服务器的备份目录里留一个 `rollback.sh`,就算电脑上的 Miao Panel 不在了,也能在服务器上用 root 执行它恢复。
**日志和回滚**:「日志 → 执行日志」里能查到 Miao Panel 在服务器上执行过的每一条命令(包括 AI 的只读检查)和结果;标明可以回滚的修改可在这里一键回滚。由服务器脚本执行并产生本地备份的修改,还会在服务器的备份目录里留一个 `rollback.sh`,就算电脑上的 Miao Panel 不在了,也能在服务器上用 root 执行它恢复;1Panel 和云端 API 操作需通过 Miao Panel 回滚。

**1Panel 用户**:在 1Panel「面板设置 → API 接口」开启 API,IP 白名单填 `127.0.0.1`,把密钥粘贴到 Miao Panel 服务器页「连接设置」里的「1Panel 接口」并点「测试」。这样 PHP、MySQL 的修改会走 1Panel 自己的接口,面板里看得到、不会被覆盖。

Expand Down
11 changes: 4 additions & 7 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@ services:
image: miaopanel:latest
container_name: miaopanel
restart: unless-stopped
ports:
# 只让本机的反向代理(1Panel、宝塔、Nginx、Caddy)访问,由它提供 HTTPS。
# 一定要直接从外面访问的话改成 "18765:18765",并且只通过 HTTPS 使用。
- "127.0.0.1:18765:18765"
# Linux 服务器上直接使用主机网络,让本机反向代理的来源保持为 127.0.0.1,
# 不需要把整个 Docker 私网段列为可信代理。
network_mode: host
volumes:
- miaopanel-data:/data # 数据库和密钥都在这里,请定期备份
environment:
TZ: Asia/Shanghai # 日报按这个时区生成
# 端口仅绑定主机回环地址;容器里看到的主机代理连接来自 Docker 网桥。
# 如改成向公网发布端口,须去掉此项,并用 --tls-cert/--tls-key 提供 HTTPS。
MIAO_TRUSTED_PROXIES: ${MIAO_TRUSTED_PROXIES:-172.16.0.0/12}
MIAO_LISTEN: 127.0.0.1:18765

volumes:
miaopanel-data:
39 changes: 35 additions & 4 deletions docs/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ docker compose up -d --build
docker logs miaopanel # 找到「初始化码」
```

默认只在本机的 `127.0.0.1:18765` 上监听,需要再配一个带 HTTPS 的反向代理(见下文)才能从外面访问。
Docker Compose 在 Linux 服务器上使用主机网络,程序只监听本机的 `127.0.0.1:18765`,需要再配一个带 HTTPS 的反向代理(见下文)才能从外面访问。
数据(数据库和密钥)在 Docker 卷 `miaopanel-data` 里,删除容器不会丢;**删除这个卷就全没了**。
在安装向导里选 MySQL 时,地址不能填 `127.0.0.1`(那是容器自己),要填 MySQL 所在机器的内网 IP,或者同一个 Docker 网络里 MySQL 容器的名字。
默认主机网络下,安装向导中的 `127.0.0.1` 指宿主机;MySQL 在宿主机监听或已向宿主机发布端口时可使用这个地址。远程数据库填其内网 IP;主机网络不使用 Docker 网桥网络内的容器名称解析。

升级:在「设置 → 版本和诊断」点「更新到最新版本」,会从 GitHub 下载新程序、核对校验值后自动重启。新程序放在数据卷的 `/data/bin` 里,重建容器也还在;以后换用更新的镜像(`git pull && docker compose up -d --build`)时自动用较新的那个。有新版本时总览也会提醒(每天问一次 GitHub,可以关掉)。

Expand Down Expand Up @@ -107,12 +107,14 @@ docker compose stop miaopanel
docker compose run --rm --no-deps --user 0 \
-v "$PWD/backups:/backup" --entrypoint sh miaopanel \
-c 'umask 077; tar -C /data -czf /backup/miaopanel-backup.tar.gz .'
sudo sha256sum backups/miaopanel-backup.tar.gz > backups/miaopanel-backup.tar.gz.sha256
docker compose start miaopanel
```

在新机器或**新的、空的数据卷**里恢复。把备份放在新项目目录的 `backups/` 下,先不要运行 `docker compose up`:

```bash
sudo sha256sum -c backups/miaopanel-backup.tar.gz.sha256
docker compose run --rm --no-deps --user 0 \
-v "$PWD/backups:/backup:ro" --entrypoint sh miaopanel \
-c 'test -z "$(ls -A /data)" || { echo "数据卷必须为空"; exit 1; }; tar -C /data -xzf /backup/miaopanel-backup.tar.gz'
Expand All @@ -121,6 +123,17 @@ docker compose up -d

恢复演练要检查原账号能登录、服务器列表和密钥可用、执行日志仍在。

systemd 安装包的数据目录是 `/opt/miaopanel/data`;手动配置的旧服务可能是 `/var/lib/miaopanel`,备份前先核对服务配置。

```bash
sudo systemctl stop miaopanel
sudo sh -c 'umask 077; tar -C /opt/miaopanel/data -czf /root/miaopanel-backup.tar.gz .'
sudo sh -c 'cd /root && sha256sum miaopanel-backup.tar.gz > miaopanel-backup.tar.gz.sha256'
sudo systemctl start miaopanel
```

在新机器恢复时,安装包会自动启动服务,安装后先运行 `sudo systemctl stop miaopanel`。核对服务的数据目录,把自动生成的初始目录保留到其他位置,再准备同一路径的空目录。先用 `sha256sum -c` 检查备份,再解包到这个空目录;安装包默认路径需运行 `sudo chown -R miaopanel:miaopanel /opt/miaopanel/data`,然后启动服务。手动安装请替换为服务实际使用的数据目录。恢复演练要检查原账号和两步验证能登录、服务器列表和密钥可用、执行日志及回滚信息仍在。至少保留一份不在原服务器磁盘上的备份。不要把含密钥的备份提交到 GitHub。

## 方式三:手动配置 systemd

安装包(方式一)做的就是这些,一般不需要手动配置。想自己控制每一步的话:
Expand All @@ -147,6 +160,7 @@ Miao Panel 本身只说 HTTP,由前面的网站服务提供 HTTPS 证书。登

```nginx
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_request_buffering off;
Expand All @@ -161,9 +175,26 @@ client_max_body_size 0;
这几行的作用:`X-Real-IP` 让登录失败按访客 IP 限制;`X-Forwarded-Proto` 让 Miao Panel 知道是 HTTPS;
关闭缓冲让 AI 的回答和终端输出实时显示;超时时间让终端能长时间开着;`client_max_body_size 0` 让「文件」和「存储」页能上传大文件。

直接运行程序时,默认只信任来自本机回环地址的反向代理。Docker Compose 的端口只绑定主机回环地址,但容器里看到的主机代理连接来自 Docker 网桥,因此 `docker-compose.yml` 已信任常见的 Docker 网桥网段。如果你的网桥不在该网段,或反向代理从另一台机器连接,设置 `MIAO_TRUSTED_PROXIES` 为代理的 IP(或尽量小的 CIDR),多个地址用逗号分隔;例如 `MIAO_TRUSTED_PROXIES=172.20.0.5`。如果把 Docker 端口改成对公网开放,必须移除 Compose 里的网桥信任配置,并用程序自带的 HTTPS。不要让代理原样转发客户端提供的 `X-Real-IP` 和 `X-Forwarded-Proto`。
默认只信任来自本机回环地址的反向代理,Docker Compose 也通过主机网络保持这个边界。如果反向代理在另一台机器,设置 `MIAO_TRUSTED_PROXIES` 为代理的准确 IP(或尽量小的 CIDR),多个地址用逗号分隔;例如 `MIAO_TRUSTED_PROXIES=172.20.0.5`。代理必须覆盖客户端传来的 `X-Real-IP` 和 `X-Forwarded-Proto`,并覆盖或追加 `X-Forwarded-For`;不要把这些请求头原样转发。

直接运行程序且不想用反向代理时,也可以让 Miao Panel 自己提供 HTTPS:`miaopanel serve --listen 0.0.0.0:443 --tls-cert 证书.pem --tls-key 私钥.pem`。Docker 部署请保持默认的本机监听和 HTTPS 反向代理,不要直接发布应用端口。

## 上线验收

仓库 CI 会在 Docker 中自动检查:精确到单个 IP 的可信反向代理、HTTPS 登录和安全 Cookie、开启并再次使用两步验证登录,以及停机备份到空卷后恢复账号、两步验证、服务器记录和密钥文件。单元测试还会用本地模拟 SSH 检查执行日志、服务器端 `rollback.sh` 和一键回滚。它们不能代替下面的真实环境验收。

首次上线或更换代理、数据目录、运行用户后,在一台可丢弃的测试服务器上完成以下检查:

| 项目 | 真实环境通过标准 |
|---|---|
| 反向代理与 HTTPS | 公网 HTTP 会跳转到 HTTPS,浏览器证书链有效;`/api/auth/state` 返回 `"https":true`。从外网伪造 `X-Real-IP`、`X-Forwarded-For`、`X-Forwarded-Proto` 后故意登录失败,操作记录仍显示测试机的真实公网 IP,且 Cookie 带 `Secure`、`HttpOnly`、`SameSite=Strict`。 |
| 登录与两步验证 | 开启两步验证前必须再次输入当前密码;退出后用密码加 TOTP 能登录,错误 TOTP 不能登录;服务器上的 `reset-password` 能恢复账号并退出已有会话。 |
| SSH 与权限 | 用计划中的实际账号(包括非 root 加 `sudo -n`)测试连接、识别环境;确认主机密钥后重连成功。需要 AI 自由命令时,另测 `systemd-run` 和五分钟保险。 |
| 执行日志与回滚 | 在可丢弃服务器上执行一个标明可回滚的脚本操作。执行日志应先出现“运行中”再给出完整命令和结果;服务器备份目录应有 `rollback.sh`。点回滚后确认状态确实恢复、日志互相引用,文件改名为 `rollback.sh.done`,再次回滚被拒绝。 |
| 备份与恢复 | 先产生一条可回滚执行日志并保存一个可用的 SSH 密钥,再按上文停机备份。恢复到另一台机器或新空卷后,用原账号和 TOTP 登录,测试 SSH 密钥、服务器列表、执行日志和回滚信息;校验和与 `secrets.json` 的 `0600` 权限必须保持。 |
| 面板与云资源 | 如实际使用 1Panel、宝塔或腾讯云,在专用测试站点/实例上各执行一次可逆变更并回滚;核对面板/云控制台的最终状态、Miao Panel 执行日志和审计记录一致。不要用生产资源做首次演练。 |

不想用反向代理,也可以让 Miao Panel 自己提供 HTTPS:`miaopanel serve --listen 0.0.0.0:443 --tls-cert 证书.pem --tls-key 私钥.pem`。
把日期、版本或提交号、代理地址、测试资源、日志 ID、备份校验和和结果记入上线记录。任何一项未通过,都不要把该版本当作已完成生产验收。

## 第一次打开:安装向导

Expand Down
4 changes: 3 additions & 1 deletion internal/actions/actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ func TestRunScriptDetachedApplyAndUndo(t *testing.T) {
if undone.Status != StatusUndone || !strings.Contains(strings.Join(undone.Log, "\n"), "undo token=["+tricky+"]") {
t.Fatalf("undo: %+v", undone)
}
RetireRollbackFile(ctx, env, out.RollbackFile)
if _, err := RetireRollbackFile(ctx, env, out.RollbackFile); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(out.RollbackFile + ".done"); err != nil {
t.Fatalf("rollback file not retired: %v", err)
}
Expand Down
Loading