Write the Code. Change the World.

分类目录
7月 23

jenkins 太占 cpu,内存了,小服务器难搞。准备试试 Woodpecker CI。

官网: https://woodpecker-ci.org/docs/administration/installation/docker-compose

安装

  1. 创建 woodpecker 目录,创建 docker-compose.yaml 文件。
mkdir woodpecker

cd woodpecker

vim docker-compose.yaml
  1. 填入 docker-compose.yaml 内容。
services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:v3
    volumes:
      - ./data/woodpecker-server-data:/var/lib/woodpecker/
    environment:
      - WOODPECKER_OPEN=true
      - WOODPECKER_HOST=${WOODPECKER_HOST}
      - WOODPECKER_GITHUB=true
      - WOODPECKER_GITHUB_CLIENT=${WOODPECKER_GITHUB_CLIENT}
      - WOODPECKER_GITHUB_SECRET=${WOODPECKER_GITHUB_SECRET}
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
    networks:
      - xrnet

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:v3
    command: agent
    restart: always
    depends_on:
      - woodpecker-server
    volumes:
      - ./data/woodpecker-agent-config:/etc/woodpecker
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
    networks:
      - xrnet

networks:
  xrnet:
    external: true
  1. 配置 .env 文件
vim .env

# 配置
WOODPECKER_GITHUB_CLIENT=
WOODPECKER_GITHUB_SECRET=
WOODPECKER_AGENT_SECRET=
7月 21

Jenkins 是一个开源的自动化服务器,它支持软件开发的持续集成和持续部署(CI/CD)。它允许开发团队自动化构建、测试和部署应用程序,从而提高软件开发的效率和质量。

下边来使用 docker 安装 jenkins,并配置 nginx 代理,进行子路径部署。就是 jenkins 部署到非根目录下,jenkins 只是主域名访问的一部分。

jenkins 官网下载地址:
https://www.jenkins.io/download/

安装

  1. 先创建部署目录和存储数据目录,设置权限。
mkdir jenkins

cd jenkins

mkdir jenkins_home

chown 1000:1000 jenkins_home
  1. 创建 docker-compose.yaml 文件。
services:
  jenkins:
    image: jenkins/jenkins:2.568.1
    container_name: jenkins
    restart: always
    volumes:
      - ./jenkins_home:/var/jenkins_home
      # 挂载宿主机 docker.sock,让 Jenkins 能调用宿主机的 docker
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker
      # 挂载宿主机的 SSH 密钥(git clone Codeberg 用)
      - /root/.ssh:/var/jenkins_home/.ssh:ro
    environment:
      - TZ:Asia/Shanghai
    command: >
      --prefix=/cicd
    networks:
      - xrnet

networks:
  xrnet:
    external: true

这里的网络必须和宿主机中 nginx 的网络相同。因为要部署到子路径下,所以要配置 prefix。

  1. 运行起来。
docker compose up -d
  1. 修改已存在的 nginx 配置,添加代理,就使用 cicd 作为子路径代理。新增代理配置。
    location /cicd/ {
        proxy_pass http://jenkins:8080;

        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_set_header X-Forwarded-Host $host:$server_port;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-Context /cicd;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_connect_timeout 60s;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }

    location = /cicd {
        return 301 /cicd/;
    }
  1. 重启 nginx。
docker restart xr-nginx

运行并设置用户名和密码

  1. 打开网站: https://xxx.xxx.com/cicd
  2. 查找密码,输入密码,继续。
    # 查看密码
    cat ./jenkins home/secrets/initialAdminPassword

  3. 安装插件,可以选择安装默认插件。安装过程到最后,如果有失败的可以继续安装,也可以跳过进入到下一步。
  4. 配置第一个管理员用户,这个比较重要,要记住账号和密码。
  5. 继续直到最后。
  6. 安装完成,进入 jenkins 首页。

开始使用

docker compose down

从安装到卸载,jenkins 这玩意,我这破服务器玩不好。

6月 15

虽然 dify 支持创建多个 api-token,也修改了 api_tokens 表,加上了 account_id 进行关联。但是,通过 api-token 转换成 current_user 的时候,这个 user 总是 tenant_account_joins 表里 role 字段是 owner 的用户。既然想给每个用户都有单独的 token,就要 current_user 能对应到每个用户身上。

api-key 到 account 的转换

从 post("/v1/datasets") 接口里,往回找,就可以找到用户绑定的地方:api\controllers\service_api\dataset\dataset.py

这里就有当前用户的获取: assert isinstance(current_user, Account)

assert isinstance(current_user, Account)

api\controllers\service_api\wraps.py 里,有用户查询相关的逻辑。

        tenant_account_join = db.session.execute(
            select(Tenant, TenantAccountJoin)
            .where(Tenant.id == api_token.tenant_id)
            .where(TenantAccountJoin.tenant_id == Tenant.id)
            .where(TenantAccountJoin.role.in_(["owner"]))
            .where(Tenant.status == TenantStatus.NORMAL)
        ).one_or_none()  # TODO: only owner information is required, so only one is returned.
        if tenant_account_join:
            tenant, ta = tenant_account_join
            account = db.session.get(Account, ta.account_id)
            # Login admin
            if account:
                account.current_tenant = tenant
                current_app.login_manager._update_request_context_with_user(account)  # type: ignore
                user_logged_in.send(current_app._get_current_object(), user=current_user)  # type: ignore
            else:
                raise Unauthorized("Tenant owner account does not exist.")

既然之前在 api_tokens 表中加了 account_id 字段,那么优先就应该从 api_tokens 表查询。如果这个表列没查出对应的 account_id, 再走原来的逻辑。

def validate_dataset_token[R](view: Callable[..., R]) -> Callable[..., R]:
    positional_parameters = [
        parameter
        for parameter in inspect.signature(view).parameters.values()
        if parameter.kind
        in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
    ]
    expects_bound_instance = bool(
        positional_parameters and positional_parameters[0].name in {"self", "cls"}
    )

    @wraps(view)
    def decorated(*args: object, **kwargs: object) -> R:
        api_token = validate_and_get_api_token("dataset")

        if api_token.account_id:
            account = db.session.get(Account, api_token.account_id)
            if not account:
                raise Unauthorized(
                    "By account_id. Token associated account does not exist."
                )

            tenant = db.session.get(Tenant, api_token.tenant_id)
            if not tenant or tenant.status == TenantStatus.ARCHIVE:
                raise Forbidden("Workspace is invalid or archived")

            account.current_tenant = tenant
            current_app.login_manager._update_request_context_with_user(account)  # type: ignore
            user_logged_in.send(current_app._get_current_object(), user=account)
        else:
            # Flask may pass URL path parameters positionally, so inspect both kwargs and args.
            dataset_id = kwargs.get("dataset_id")

            if not dataset_id and args:
                potential_id = args[0]
                try:
                    str_id = str(potential_id)
                    if len(str_id) == 36 and str_id.count("-") == 4:
                        dataset_id = str_id
                except Exception:
                    logger.exception("Failed to parse dataset_id from positional args")

            if dataset_id:
                dataset_id = str(dataset_id)
                dataset = db.session.scalar(
                    select(Dataset)
                    .where(
                        Dataset.id == dataset_id,
                        Dataset.tenant_id == api_token.tenant_id,
                    )
                    .limit(1)
                )
                if not dataset:
                    raise NotFound("Dataset not found.")
                if not dataset.enable_api:
                    raise Forbidden("Dataset api access is not enabled.")

            tenant_account_join = db.session.execute(
                select(Tenant, TenantAccountJoin)
                .where(Tenant.id == api_token.tenant_id)
                .where(TenantAccountJoin.tenant_id == Tenant.id)
                .where(TenantAccountJoin.role.in_(["owner"]))
                .where(Tenant.status == TenantStatus.NORMAL)
            ).one_or_none()  # TODO: only owner information is required, so only one is returned.
            if tenant_account_join:
                tenant, ta = tenant_account_join
                account = db.session.get(Account, ta.account_id)
                # Login admin
                if account:
                    account.current_tenant = tenant
                    current_app.login_manager._update_request_context_with_user(account)  # type: ignore
                    user_logged_in.send(current_app._get_current_object(), user=current_user)  # type: ignore
                else:
                    raise Unauthorized("Tenant owner account does not exist.")
            else:
                raise Unauthorized("Tenant does not exist.")

        if expects_bound_instance:
            if not args:
                raise TypeError(
                    "validate_dataset_token expected a bound resource instance."
                )
            return view(args[0], api_token.tenant_id, *args[1:], **kwargs)

        return view(api_token.tenant_id, *args, **kwargs)

    return decorated

只加了:

        if api_token.account_id:
            account = db.session.get(Account, api_token.account_id)
            if not account:
                raise Unauthorized(
                    "By account_id. Token associated account does not exist."
                )

            tenant = db.session.get(Tenant, api_token.tenant_id)
            if not tenant or tenant.status == TenantStatus.ARCHIVE:
                raise Forbidden("Workspace is invalid or archived")

            account.current_tenant = tenant
            current_app.login_manager._update_request_context_with_user(account)  # type: ignore
            user_logged_in.send(current_app._get_current_object(), user=account)

到这里看似是 ok 了。但是还是会报错。在 api_token = validate_and_get_api_token("dataset") 里,api_token 可能来自 ApiTokenCache。如果来自 ApiTokenCache 这里就会报错。在 ApiTokenCache 里根本就没有定要 account_id。如果不走缓存,之前在修改数据库表的时候,在模型中已经加入了 account_id 字段,自然不会报错。现在只需要在ApiTokenCache 中加入 account_id 即可。

# api\services\api_token_service.py

    id: str
    app_id: str | None
    tenant_id: str | None
    account_id: str | None
    type: str
    token: str
    last_used_at: datetime | None
    created_at: datetime | None

        cached = CachedApiToken(
            id=str(api_token.id),
            app_id=str(api_token.app_id) if api_token.app_id else None,
            tenant_id=str(api_token.tenant_id) if api_token.tenant_id else None,
            account_id=str(api_token.account_id) if api_token.account_id else None,
            type=api_token.type,
            token=api_token.token,
            last_used_at=api_token.last_used_at,
            created_at=api_token.created_at,
        )
12月 31

安装

CentOS/Rocky/Alibaba Cloud Linux

sudo yum install -y iftop nethogs

Ubuntu/Debian

sudo apt update && sudo apt install -y iftop nethogs

使用

# 查看流量
sudo iftop -i eth0 -P

12月 24

示例:

linux 、macOs

curl -o /dev/null -s \
-w "\n\
DNS 解析:       %{time_namelookup} s\n\
TCP 握手:       %{time_connect} s\n\
TLS 握手:       %{time_appconnect} s\n\
首字节 TTFB:    %{time_starttransfer} s\n\
总耗时:         %{time_total} s\n\
下载大小:       %{size_download} bytes\n\
HTTP 状态:      %{http_code}\n" \
https://xxx.xxx/xxx.js

win :

curl.exe -o nul -s \
-w "\n\
DNS 解析:       %{time_namelookup} s\n\
TCP 握手:       %{time_connect} s\n\
TLS 握手:       %{time_appconnect} s\n\
首字节 TTFB:    %{time_starttransfer} s\n\
总耗时:         %{time_total} s\n\
下载大小:       %{size_download} bytes\n\
HTTP 状态:      %{http_code}\n" \
https://xxx.xxx/xxx.js

-s 静默模式,屏蔽进度条和错误信息,只输出 -w 的内容。

输出结果示例如下:

DNS 解析:       0.029579 s
TCP 握手:       0.033574 s
TLS 握手:       0.044148 s
首字节 TTFB:    0.052129 s
总耗时:         0.491568 s
下载大小:       2159257 bytes
HTTP 状态:      200
11月 12

好几年前,ssl 免费证书有效期为 3 个月。搞一个能生成证书的环境就比较方便。

构建带 certbot 的 nginx 镜像

  1. 准备 Dockerfile 文件。
FROM nginx:1.29.3

# 安装 certbot https://certbot.eff.org/instructions?ws=nginx&os=snap
# 也可以在 nginx 之外使用 certbot 镜像 https://hub.docker.com/r/certbot/certbot
RUN apt-get update && \
    apt-get install -y python3-venv && \
    python3 -m venv /opt/certbot && \
    /opt/certbot/bin/pip install certbot certbot-nginx && \
    ln -s /opt/certbot/bin/certbot /usr/local/bin/certbot && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
  1. 打包镜像

    docker build -t xr-nginx:1.29.3 .
  2. 运行镜像,生成容器服务。然后打开浏览器,输入 http://localhost 查看情况。

docker run --rm -it -p 80:80 --name xr-nginx xr-nginx:1.29.3

# 查看日志
docker run --rm -it -p 80:80 xr-nginx:1.29.3

# 进入容器
received

然后镜既有 nginx 的功能,还能生成证书。

生成证书

  1. 生成证书需要域名解析指向服务器。
  2. 需要挂载好证书存储位置,防止 docker 关闭启动后 丢失证书。( ./conf/nginx/letsencrypt:/etc/letsencrypt)
# 容器外运行
docker exec -i xr-nginx sh -c "certbot -d xxx.com"

# 进入容器运行
docker exec -it xr-nginx bash
certbot -d xxx.com

其他

https://blog.vini123.com/1299

https://blog.vini123.com/1293

11月 12

默认情况下,win10 打包的 docker 镜像是 x86 架构平台的。有的服务器 cpu 是 arm 架构的,这个时候打包成 arm 架构的镜像就很有必要。或者是在服务器上去构建镜像。

win10 打包 arm 平台 docker 镜像

  1. 创建并启用 arm64 构建器. arm-builder 是构建器名称。

    docker buildx create --name arm-builder --use --platform linux/amd64,linux/arm64
  2. 准备好 Dockerfile 文件。 进行构建。 my-app-arm64 是镜像名字,1.0 是镜像版本。

    docker buildx build --platform linux/arm64 -t my-app-arm64:1.0 --load .
  3. 构建成功后,验证一下。

    docker run --rm -it --platform linux/arm64 my-app-arm64:1.0 uname -m
  4. 推送到远程仓库

    docker tag my-app-arm64:1.0 xxxx仓库地址/my-app-arm64:1.0
    docker push xxxx仓库地址/my-app-arm64:1.0

后续

# 查看构建器列表
docker buildx ls

# 切换构建器 
docker buildx use default (默认构建器)
docker buildx use desktop-linux (desktop-linux 构建器)
# 删除镜像构建器
docker buildx rm arm-buildx
8月 27

服务器突然拉不下 github 代码,报下边错误,ping github 也 ping 不通。

git pull

ssh: connect to host github.com port 22: Connection timed out
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

这个是 github 22 端口号访问不了。既保留了 SSH 协议的便利(如使用 SSH 密钥认证),又绕开了被封锁的 22 端口。GitHub 允许通过 443 端口进行 SSH 连接。

1. 编辑 ssh 配置文件。

vim ~/.ssh/config

在文件中添加以下内容:

Host github.com
  Hostname ssh.github.com
  Port 443
  User git
  IdentityFile ~/.ssh/id_rsa  # 这里指向你的私钥文件,如果默认是 id_rsa 通常不用改

保存退出,然后测试 ssh 连接是否畅通。

ssh -T git@github.com

# 输出以下信息
Hi 17vision! You've successfully authenticated, but GitHub does not provide shell access.

# 然后拉代码
git pull
5月 08

如果不需要泛域名证书,可以不用安装对应域名运营商的 cli 工具

nginx 镜像

仅安装 certbot 的 Dockerfile

ARG NGINX_IMAGE
FROM ${NGINX_IMAGE}

# 安装 certbot https://certbot.eff.org/instructions?ws=nginx&os=snap
# 也可以在 nginx 之外使用 certbot 镜像 https://hub.docker.com/r/certbot/certbot
RUN apt-get update && \
    apt-get install -y python3-venv && \
    python3 -m venv /opt/certbot && \
    /opt/certbot/bin/pip install certbot certbot-nginx && \
    ln -s /opt/certbot/bin/certbot /usr/local/bin/certbot && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

安装 certbot 和 aliyun cli 的Dockerfile

ARG NGINX_IMAGE
FROM ${NGINX_IMAGE}

# 安装 certbot https://certbot.eff.org/instructions?ws=nginx&os=snap
# 也可以在 nginx 之外使用 certbot 镜像 https://hub.docker.com/r/certbot/certbot
RUN apt-get update && \
    apt-get install -y wget python3-venv && \
    python3 -m venv /opt/certbot && \
    /opt/certbot/bin/pip install certbot certbot-nginx && \
    ln -s /opt/certbot/bin/certbot /usr/local/bin/certbot && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 安装阿里云 cli https://help.aliyun.com/zh/cli/install-cli-on-linux?spm=a2c4g.11174283.0.0.5df9478dHNZ4T9
# https://help.aliyun.com/zh/cli/run-alibaba-cloud-cli-in-a-docker-container
RUN mkdir -p /soft && \
    cd /soft && \
    wget -q https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz && \
    tar -xzvf aliyun-cli-linux-latest-amd64.tgz && \
    cp aliyun /usr/local/bin/ && \
    rm -rf /soft/aliyun-cli-linux-latest-amd64.tgz

# 验证阿里云 CLI 是否安装成功
RUN aliyun version

构建镜像,当前 nginx 最新版本是 1.28.0

docker build --build-arg NGINX_IMAGE=nginx:1.28.0 -t nginx-certbot .

手动生成证书

假设服务器上已经使用了该 nginx 镜像。并且配置文件目录是 /etc/nginx/。假如某个 .conf 文件使用了 vini123.com 的证书。就可以通过一下命令生成。

# 找到容器的名字或 id 前三位
docker ps

# 生成证书
docker exec -i nginx1.28.0 certbot --nginx --nginx-server-root=/etc/nginx -d vini123.com -d www.vini123.com

docker exec 常用 option 说明。

常用选项
-d:在后台运行命令。
-i:保持 STDIN 打开,即使没有附加。
-t:分配一个伪终端。
-u:指定在容器内执行命令的用户。
--workdir:指定容器内的工作目录。

注意

因为 certbot 安装在了 nginx 容器内。如果执行 docker-compose down 再执行 docker-compose up -d 就会丢失证书。所以在构建 nginx 容器的时候,一定要配置好证书的 volume。就是 certbot 生成的证书位置和本地宿主机的一个映射。

4月 28

https://certbot.eff.org/instructions?ws=nginx&os=snap&tab=standard

安装

安装 snapd

https://snapcraft.io/docs/installing-snap-on-centos

https://snapcraft.io/docs/installing-snapd

yum install snapd

systemctl enable --now snapd.socket

ln -s /var/lib/snapd/snap /snap

看看 snapd 状态。 snap changes

安装 certbot

snap install --classic certbot

建立软链

ln -s /snap/bin/certbot /usr/bin/certbot

生成证书

必须服务器先安装好 nginx。

生成单个域名证书

  • 执行 nginx -t 先查找 nginx 的配置文件位置。比如:/tianwanggaidhu/service/nginx/conf/nginx.conf
  • 执行生成命令。比如生成证书的域名是 vini123.com。
    certbot --nginx --nginx-server-root=/tianwanggaidhu/service/nginx/conf -d vini123.com -d www.vini123.com

    需要注意的是,--nginx-server-root 对应的是 nginx.conf 的配置目录,而不是具体的文件。

生第一个域名证书的时候,会让你输入 email,然后同意协议(y 回车即可)。生成一次后,后边的域名就直接生成了。

可以在任意位置执行该命令。

生成成功的同时,nginx 也一起重启了。也就是生成后,证书就 ok了。

证书续期

生成的证书有效期是 3 个月的。3个月后,需要新的证书。这里用续期处理。

certbot renew --dry-run

可以通过定时任务来续订。

生泛域名证书

参考

https://www.cnblogs.com/michaelshen/p/18538178