[{"content":"","date":"2026年8月5日","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"5 八月 2026","externalUrl":null,"permalink":"/en/tags/commandline/","section":"Tags","summary":"","title":"Commandline","type":"tags"},{"content":"","date":"2026年8月5日","externalUrl":null,"permalink":"/tags/oh-my-posh/","section":"Tags","summary":"","title":"Oh-My-Posh","type":"tags"},{"content":"","date":"2026年8月5日","externalUrl":null,"permalink":"/post/","section":"Posts","summary":"","title":"Posts","type":"post"},{"content":"","date":"2026年8月5日","externalUrl":null,"permalink":"/tags/powershell/","section":"Tags","summary":"","title":"PowerShell","type":"tags"},{"content":"","date":"5 八月 2026","externalUrl":null,"permalink":"/en/categories/software/","section":"Categories","summary":"","title":"Software","type":"categories"},{"content":"","date":"2026年8月5日","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"2026年8月5日","externalUrl":null,"permalink":"/tags/zsh/","section":"Tags","summary":"","title":"Zsh","type":"tags"},{"content":" 为什么迁移？ # 之前我的 Shell 配置使用 Oh My Zsh 和 Powerlevel10k。它精致而且速度 很快，但提示符绑定在 Zsh 上。我希望在 macOS、Linux、HPC 集群和 Windows PowerShell 中使用同一套视觉风格，因此将提示符迁移到了 Oh My Posh。\nOh My Posh 将提示符主题与 Shell 分离，所以同一个 JSON 配置可以由 Zsh 和 PowerShell 初始化。现在使用的是锦鲤主题 jinli.omp.json， 配色基于 Catppuccin。\nJinli 主题展示什么？ # 这个主题希望在不让提示符变得拥挤的前提下，始终显示有用的上下文信息：\n路径区会根据上下文显示家目录、不可写目录、GitHub、Git、npm、Downloads、 Pictures 和普通目录图标； Git 区显示仓库、分支、工作区状态和变更数量； 当前项目相关时，在右侧显示 Python 和 Node 版本； SSH 会话会显示远程图标，方便识别当前 Shell 是否位于远程机器； 电池、时间、命令耗时和退出状态会根据机器环境自适应，并保持右对齐； 同一个主题可用于 Zsh 和 PowerShell，周边配置也支持 Linux、macOS、Windows 和 NixOS。 当前配置结构 # 现在最重要的变化，是将稳定的公共配置与工具可以修改的本地配置分开：\n仓库中的 .zshrc.common 保存共享的 Zsh 配置； ~/.zshrc 是安装脚本创建的普通本地文件。它会先加入 Oh My Posh 的可执行 文件目录，再加载 .zshrc.common； Conda、OpenClaw 等工具可以向 ~/.zshrc 追加配置，而不会修改 Git 管理的 公共文件； 为兼容旧版安装，如果已有 ~/.zshrc.local，它会在公共配置之后继续被加载； 在 macOS/Linux 上，主题会链接到 ~/.config/oh-my-posh/jinli.omp.json。 机器专属配置应放在 ~/.zshrc 中，例如：\n1 2 export PATH=\u0026#34;$HOME/.local/bin:$PATH\u0026#34; alias connect-hpc=\u0026#39;ssh user@example.org\u0026#39; 这样私有变量、集群 module、工作站专属别名和本地工具路径就不会进入共享仓库。\nOh My Zsh 仍然是可选的 # Oh My Posh 是提示符引擎，并不取代所有 Zsh 插件。共享配置会启用 zsh-autosuggestions 和 fast-syntax-highlighting。\n如果已经安装 Oh My Zsh，脚本会将两个插件安装并加载到它的自定义插件目录； 否则会将插件安装到 ~/.local/share/zsh/plugins 并直接加载。为了使用这些插件， 不需要额外安装 Oh My Zsh。\n共享 Zsh 配置还提供不区分大小写、支持子串匹配的补全。例如输入 cd dev\u0026lt;Tab\u0026gt;，可以补全为 itrip-dev-doc。配置还通过 AUTO_PUSHD 和 PUSHD_IGNORE_DUPS 维护目录栈，可以使用 cd -\u0026lt;数字\u0026gt; 和 dirs -v 浏览历史目录。\n安装 # macOS 和 Linux # 请以普通用户运行。macOS 请先安装 Homebrew：\n1 2 3 curl -fsSLO https://raw.githubusercontent.com/jin-li/ShellConfig/main/install-oh-my-posh.sh chmod +x install-oh-my-posh.sh ./install-oh-my-posh.sh 脚本会询问仓库目录。直接按 Enter 使用 ~/Documents/GitHub/ShellConfig，也可以 输入其他目录。运行前设置 SHELL_CONFIG_DIR 时，它会作为建议的安装目录。\n脚本会安装 Oh My Posh 和 Zsh 依赖，安装插件，创建主题链接，创建本地 ~/.zshrc，并询问是否安装 Meslo Nerd Font。完成后请重启终端，或运行 exec zsh。\n已有安装请从仓库目录运行更新脚本：\n1 2 cd /path/to/ShellConfig ./update.sh 更新脚本使用自身所在目录定位仓库，因此即使仓库不在默认目录也可以正常更新。 它会更新仓库和主题链接，同时保留本地 ~/.zshrc。\nWindows PowerShell # 以普通用户运行：\n1 2 3 Set-ExecutionPolicy -Scope Process Bypass Invoke-WebRequest https://raw.githubusercontent.com/jin-li/ShellConfig/main/install-oh-my-posh.ps1 -OutFile install-oh-my-posh.ps1 .\\install-oh-my-posh.ps1 PowerShell 脚本会在缺少 Oh My Posh 或 Git 时通过 WinGet 安装，询问仓库目录， 并且只更新 $PROFILE 中由 ShellConfig 管理的区块，保留用户和其他工具的配置。 主题直接从所选仓库目录加载；Windows 不使用 Unix 风格的 ~/.config 主题链接。\nNixOS # 本仓库也通过 Flake 提供 NixOS 模块：\n1 2 3 4 5 inputs.shell-config.url = \u0026#34;github:jin-li/ShellConfig\u0026#34;; modules = [ inputs.shell-config.nixosModules.default ]; programs.shellConfig.enable = true; users.users.\u0026lt;username\u0026gt;.shell = pkgs.zsh; 然后重建系统并启动新的会话：\n1 2 sudo nixos-rebuild switch --flake .#\u0026lt;host\u0026gt; exec zsh 字体与终端设置 # 主题使用 Nerd Font 图标。请在终端配置中安装并选择 MesloLGM Nerd Font， 否则图标可能显示为方框。使用 WSL 时，请在 WSL 内运行 Linux 安装脚本，但在 Windows 中安装并配置字体。\n没有 sudo 权限的 HPC 集群 # macOS/Linux 安装脚本会在尝试安装系统软件前询问 sudo，并检查 curl、git、 unzip 和 zsh。如果没有 sudo 且只有 Zsh 缺失，脚本可以将 ncurses 和 Zsh 编译到默认的 ~/.local，也可以通过 $SHELL_CONFIG_PREFIX 指定其他目录。编译器 和 make 必须已经可用，通常可以通过集群 module 加载。\n其他缺失依赖需要由集群提供，或由用户安装到自己的环境中。可以在本地 ~/.zshrc 中加载 module，并将用户目录下的可执行文件目录加入 PATH。\nNeovim 保持独立 # LazyVim 安装脚本与提示符安装脚本保持独立：Oh My Posh 负责 Shell 提示符，LazyVim 负责管理 Neovim 配置和编辑器插件。\n1 2 3 curl -fsSLO https://raw.githubusercontent.com/jin-li/ShellConfig/main/install_LazyVim.sh chmod +x install_LazyVim.sh ./install_LazyVim.sh 脚本会检查 Neovim 版本是否为 0.11.2 或更高，备份已有的 Neovim 配置和数据， 并将 LazyVim starter 克隆到 ~/.config/nvim。它不再管理旧版 ~/.vimrc。之后 启动 nvim 安装插件，并运行 :checkhealth 或 :LazyHealth。\n最终效果 # 现在 Zsh 和 PowerShell 使用同一个主题，共享配置保存在 Git 中，机器专属工具可以 安全地修改本地 ~/.zshrc。这样配置就能更容易地在个人电脑、Linux 工作站、HPC 集群和 Windows 之间迁移，同时避免机器专属改动与公共配置发生冲突。\n","date":"2026年8月5日","externalUrl":null,"permalink":"/p/%E4%BB%8E-powerlevel10k-%E8%BF%81%E7%A7%BB%E5%88%B0-oh-my-posh/","section":"Posts","summary":" 为什么迁移？ # 之前我的 Shell 配置使用 Oh My Zsh 和 Powerlevel10k。它精致而且速度 很快，但提示符绑定在 Zsh 上。我希望在 macOS、Linux、HPC 集群和 Windows PowerShell 中使用同一套视觉风格，因此将提示符迁移到了 Oh My Posh。\n","title":"从 Powerlevel10k 迁移到 Oh My Posh","type":"post"},{"content":"","date":"2026年8月5日","externalUrl":null,"permalink":"/","section":"锦李本鲤","summary":"","title":"锦李本鲤","type":"page"},{"content":"","date":"2026年8月5日","externalUrl":null,"permalink":"/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/","section":"Tags","summary":"","title":"命令行","type":"tags"},{"content":"","date":"2026年8月5日","externalUrl":null,"permalink":"/categories/%E8%BD%AF%E4%BB%B6/","section":"Categories","summary":"","title":"软件","type":"categories"},{"content":"","date":"2026年7月5日","externalUrl":null,"permalink":"/tags/cloudflare-dns-challenge/","section":"Tags","summary":"","title":"Cloudflare DNS Challenge","type":"tags"},{"content":"","date":"2026年7月5日","externalUrl":null,"permalink":"/tags/lets-encrypt/","section":"Tags","summary":"","title":"Let's Encrypt","type":"tags"},{"content":"","date":"5 七月 2026","externalUrl":null,"permalink":"/en/categories/network/","section":"Categories","summary":"","title":"Network","type":"categories"},{"content":"","date":"5 七月 2026","externalUrl":null,"permalink":"/en/series/reverse-proxy-series/","section":"Series","summary":"","title":"Reverse Proxy Series","type":"series"},{"content":"","date":"2026年7月5日","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","date":"5 七月 2026","externalUrl":null,"permalink":"/en/tags/ssl-certificate/","section":"Tags","summary":"","title":"SSL Certificate","type":"tags"},{"content":"","date":"2026年7月5日","externalUrl":null,"permalink":"/tags/ssl%E8%AF%81%E4%B9%A6/","section":"Tags","summary":"","title":"SSL证书","type":"tags"},{"content":"","date":"2026年7月5日","externalUrl":null,"permalink":"/tags/tls/","section":"Tags","summary":"","title":"TLS","type":"tags"},{"content":"","date":"2026年7月5日","externalUrl":null,"permalink":"/tags/traefik/","section":"Tags","summary":"","title":"Traefik","type":"tags"},{"content":"","date":"2026年7月5日","externalUrl":null,"permalink":"/series/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"反向代理系列","type":"series"},{"content":" 背景 # 我之前一直使用Cloudflare Tunnel免费版来反向代理Nextcloud容器。虽然Cloudflare Tunnel使用方便，但免费版有 100MB上传限制，这成为我使用Nextcloud时的主要障碍。\n本文记录了我从Cloudflare Tunnel迁移至直接使用Traefik反向代理，并配置Let\u0026rsquo;s Encrypt TLS证书的完整过程，包括设置、遇到的问题和解决方案。\n迁移原因 # Cloudflare Tunnel免费版的优点 # ✅ 自动SSL/TLS终止 ✅ 无需端口转发 ✅ 支持动态IP ✅ 内置DDoS防护 Cloudflare Tunnel免费版的缺点 # ❌ 100MB上传限制（主要问题） ❌ 带宽限制 ❌ 有tunnel额外延迟 ❌ 依赖Cloudflare基础设施 为什么选择Traefik + Let\u0026rsquo;s Encrypt # ✅ 无上传限制（可配置） ✅ 直接连接（更低延迟） ✅ 自动证书管理 ✅ 完全控制路由 ✅ 可处理多个服务 当前架构 # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ┌────────────────────────────────────────────────────────────┐ │ Internet │ └────────────────────────────┬───────────────────────────────┘ │ ┌──────────▼──────────┐ │ 路由器 │ │ 端口转发 │ │ 80, 443 → 服务器 │ └──────────┬───────────┘ │ ┌──────────▼──────────┐ │ Traefik │ │ (TLS 终止) │ └──────────┬──────────┘ │ ┌──────────▼──────────┐ │ Nextcloud │ │ (Docker) │ └─────────────────────┘ 之前: 互联网 → Cloudflare Tunnel → Traefik → Nextcloud\n现在: 互联网 → Traefik → Nextcloud\n前置条件 # Docker和docker-compose已安装 Traefik容器正在运行 域名（如nextcloud2.example.com） Cloudflare账户且域名DNS由其管理 服务器有公网IP（或使用动态DNS） 步骤1: 移除Cloudflare Tunnel # 停止cloudflared容器 # 1 2 cd /path/to/cloudflared docker compose down 更新cloudflared配置（可选） # 如果想保留cloudflared为其他服务，从config.yaml中移除Nextcloud条目：\n1 2 3 4 5 6 7 8 9 10 11 12 ingress: # 删除以下行： # - hostname: \u0026#34;cloud.example.com\u0026#34; # service: https://traefik:443 # originRequest: # noTLSVerify: true # - hostname: \u0026#34;nextcloud2.example.com\u0026#34; # service: https://traefik:443 # originRequest: # noTLSVerify: true # 保留其他条目... - service: http_status:404 步骤2: 在Cloudflare更新DNS # 为每个要使用Traefik的域名：\n进入 Cloudflare仪表板 → DNS 找到DNS记录（如nextcloud2.example.com） 从橙色云（代理）改为灰色云（仅DNS） 指向服务器的公网IP地址 重要： 为了让Let\u0026rsquo;s Encrypt DNS challenge正常工作，流量必须直接到达你的服务器，而不是通过Cloudflare代理。\n步骤3: 配置Traefik的Let\u0026rsquo;s Encrypt DNS Challenge # 更新traefik.yml # 确保有ACME配置和Cloudflare DNS challenge：\n1 2 3 4 5 6 7 8 9 10 certificatesResolvers: le: acme: email: your-email@example.com storage: /letsencrypt/acme.json dnsChallenge: provider: cloudflare resolvers: - \u0026#34;1.1.1.1:53\u0026#34; - \u0026#34;1.0.0.1:53\u0026#34; 设置Cloudflare API Token # 创建或更新traefik目录下的.env文件：\n1 2 3 CF_API_KEY=your_cloudflare_api_token CF_API_EMAIL=your-email@example.com TZ=UTC 注意： DNS challenge需要Cloudflare API Token（不是全局API密钥），具有以下权限：\nZone DNS: 编辑区域DNS 更新docker-compose.yml # 确保环境变量传递给Traefik：\n1 2 3 4 environment: - CF_API_KEY=${CF_API_KEY} - CF_API_EMAIL=${CF_API_EMAIL} - TZ=${TZ:-UTC} 步骤4: 配置动态路由器带TLS # 在traefik/dynamic/目录下创建文件（如nextcloud2.yml）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 http: routers: nextcloud2-router-http: rule: \u0026#34;Host(`nextcloud2.example.com`)\u0026#34; entryPoints: - web middlewares: - redirect-https service: nextcloud2-service nextcloud2-router-https: rule: \u0026#34;Host(`nextcloud2.example.com`)\u0026#34; entryPoints: - websecure tls: certResolver: le # 重要：指定Let\u0026#39;s Encrypt解析器 service: nextcloud2-service services: nextcloud2-service: loadBalancer: servers: - url: \u0026#34;http://nextcloud:80\u0026#34; # Nextcloud容器名称 passHostHeader: true middlewares: redirect-https: redirectScheme: scheme: https permanent: true 关键配置点 # 需要两个路由器：\nHTTP路由器用于重定向（端口80） HTTPS路由器用于实际流量（端口443） certResolver: le至关重要 - 没有它，Traefik使用内部自签名证书\npassHostHeader: true - 保留原始Host头\n步骤5: 重启Traefik # 1 2 cd /path/to/traefik docker compose up -d --force-recreate traefik Traefik将：\n监听新配置文件 通过DNS challenge请求Let\u0026rsquo;s Encrypt证书 创建_acme-challenge.nextcloud2.example.com TXT记录 等待DNS传播 验证所有权 下载证书 步骤6: 验证设置 # 检查证书是否已颁发 # 1 2 3 4 5 # 查看所有证书 cat /path/to/traefik/acme.json | python3 -m json.tool # 检查特定域名 cat /path/to/traefik/acme.json | grep -A 5 \u0026#39;nextcloud2\u0026#39; 测试HTTPS连接 # 1 2 3 4 5 6 7 8 # 验证证书 echo | openssl s_client -connect nextcloud2.example.com:443 -servername nextcloud2.example.com 2\u0026gt;/dev/null | openssl x509 -noout -issuer -dates # 验证域名匹配 echo | openssl s_client -connect nextcloud2.example.com:443 -servername nextcloud2.example.com 2\u0026gt;/dev/null | openssl x509 -noout -subject -text | grep -E \u0026#34;(Subject:|DNS:)\u0026#34; # 测试连接 curl -I https://nextcloud2.example.com 添加更多域名 # 要添加更多域名，只需创建更多配置文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 # 为另一个域名创建配置 cat \u0026gt; /path/to/traefik/dynamic/blog.yml \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; http: routers: blog-router-http: rule: \u0026#34;Host(`blog.example.com`)\u0026#34; entryPoints: - web middlewares: - redirect-https service: blog-service blog-router-https: rule: \u0026#34;Host(`blog.example.com`)\u0026#34; entryPoints: - websecure tls: certResolver: le service: blog-service services: blog-service: loadBalancer: servers: - url: \u0026#34;http://blog:80\u0026#34; passHostHeader: true middlewares: redirect-https: redirectScheme: scheme: https permanent: true EOF Traefik每10秒自动重载配置，不需要重启。\n常见问题及解决方案 # 1. 证书显示为自签名 # 问题： 浏览器显示\u0026quot;不安全\u0026quot;或自签名证书警告\n解决方案： 在路由器配置中添加certResolver: le。没有它，Traefik使用内部证书。\n2. DNS Challenge失败 # 问题： 证书未颁发，日志中有错误\n解决方案：\n验证Cloudflare API token有正确权限 检查Traefik能否通过环境变量访问API token 确保DNS未被Cloudflare代理（灰色云） 查看日志：docker logs traefik 3. 证书存在但不适用于域名 # 问题： Let\u0026rsquo;s Encrypt证书存在，但不是你的域名\n解决方案： Traefik是按需创建证书的。只需访问该域名，Traefik将自动请求证书。\n4. HTTP到HTTPS重定向不工作 # 问题： HTTP请求不重定向到HTTPS\n解决方案： 确保你配置了两个路由器（HTTP和HTTPS）并带有重定向中间件。\n5. Nextcloud显示\u0026quot;受信任域名错误\u0026quot; # 问题： Nextcloud不信任新域名\n解决方案： 更新Nextcloud的受信任域名：\n在nextcloud/docker-compose.yml中更新：\n1 2 environment: - NEXTCLOUD_TRUSTED_DOMAINS=nextcloud2.example.com,cloud.example.com,previous-domain.com 或在Nextcloud配置中（config/config.php）：\n1 2 3 4 5 \u0026#39;trusted_domains\u0026#39; =\u0026gt; [ \u0026#39;nextcloud2.example.com\u0026#39;, \u0026#39;cloud.example.com\u0026#39;, \u0026#39;previous-domain.com\u0026#39;, ], 高级配置 # 启用调试日志 # 1 2 3 # traefik.yml log: level: DEBUG 配置证书TTL # 1 2 3 4 5 6 7 8 9 certificatesResolvers: le: acme: email: your-email@example.com storage: /letsencrypt/acme.json dnsChallenge: provider: cloudflare caServer: \u0026#34;https://acme-v02.api.letsencrypt.org/directory\u0026#34; # 生产 # caServer: \u0026#34;https://acme-staging-v02.api.letsencrypt.org/directory\u0026#34; # 测试 多个域名对应单个服务 # 1 2 3 4 5 6 7 8 9 http: routers: multi-domain-router-https: rule: \u0026#34;Host(`domain1.com`) || Host(`domain2.com`) || Host(`domain3.com`)\u0026#34; entryPoints: - websecure tls: certResolver: le service: my-service 监控与维护 # 检查证书过期 # 1 2 3 4 5 6 7 8 # 脚本检查所有证书 cat /path/to/traefik/acme.json | python3 \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; import json, sys, datetime data = json.load(sys.stdin) for domain in data[\u0026#39;le\u0026#39;][\u0026#39;Certificates\u0026#39;]: cert = domain[\u0026#39;certificate\u0026#39;] print(f\u0026#34;Domain: {domain[\u0026#39;domain\u0026#39;][\u0026#39;main\u0026#39;]}\u0026#34;) EOF 手动证书续期 # Let\u0026rsquo;s Encrypt证书每90天自动续期。强制续期：\n1 docker compose exec traefik traefik renew-certs 备份acme.json # 1 cp /path/to/traefik/acme.json /path/to/traefik/acme.json.backup-$(date +%Y%m%d) 性能对比 # 指标 Cloudflare Tunnel Traefik + Let\u0026rsquo;s Encrypt 上传限制 100MB ❌ 无限制 ✅ 延迟 较高（tunnel开销） 较低（直接） ✅ SSL证书 自动 ✅ 自动 ✅ 成本 免费（有限制） 免费（无限制） ✅ 配置 简单 中等 DDoS防护 内置 ✅ 需单独添加 总结 # 从Cloudflare Tunnel迁移到Traefik + Let\u0026rsquo;s Encrypt是直截了当的，提供：\n无上传限制 更好的性能 完全控制 自动证书管理 关键要点：\n移除Cloudflare Tunnel for the domain 更新DNS为仅DNS（非代理） 配置Traefik with ACME + Cloudflare DNS challenge 在路由器配置中设置certResolver: le 重启Traefik请求证书 这个设置非常适合自托管服务，并且随着你添加更多域名而扩展。\n参考 # Traefik ACME文档 Let\u0026rsquo;s Encrypt DNS Challenge Cloudflare DNS Challenge Provider 更新日志 # 2026-07-05 - 初始发布，记录迁移过程 ","date":"2026年7月5日","externalUrl":null,"permalink":"/p/%E4%BD%BF%E7%94%A8traefik%E5%92%8Clets-encrypt%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%8A%A8tls%E8%AF%81%E4%B9%A6/","section":"Posts","summary":" 背景 # 我之前一直使用Cloudflare Tunnel免费版来反向代理Nextcloud容器。虽然Cloudflare Tunnel使用方便，但免费版有 100MB上传限制，这成为我使用Nextcloud时的主要障碍。\n","title":"使用Traefik和Let's Encrypt实现自动TLS证书","type":"post"},{"content":"","date":"2026年7月5日","externalUrl":null,"permalink":"/categories/%E7%BD%91%E7%BB%9C/","section":"Categories","summary":"","title":"网络","type":"categories"},{"content":"","date":"2026年6月2日","externalUrl":null,"permalink":"/categories/ai/","section":"Categories","summary":"","title":"AI","type":"categories"},{"content":"","date":"2026年6月2日","externalUrl":null,"permalink":"/tags/deepseek/","section":"Tags","summary":"","title":"DeepSeek","type":"tags"},{"content":"","date":"2026年6月2日","externalUrl":null,"permalink":"/tags/gemma/","section":"Tags","summary":"","title":"Gemma","type":"tags"},{"content":"","date":"2026年6月2日","externalUrl":null,"permalink":"/tags/llm/","section":"Tags","summary":"","title":"LLM","type":"tags"},{"content":"","date":"2 六月 2026","externalUrl":null,"permalink":"/en/tags/local-ai/","section":"Tags","summary":"","title":"Local AI","type":"tags"},{"content":"","date":"2026年6月2日","externalUrl":null,"permalink":"/tags/ollama/","section":"Tags","summary":"","title":"Ollama","type":"tags"},{"content":"","date":"2026年6月2日","externalUrl":null,"permalink":"/tags/openclaw/","section":"Tags","summary":"","title":"OpenClaw","type":"tags"},{"content":"","date":"2 六月 2026","externalUrl":null,"permalink":"/en/tags/speculative-decoding/","section":"Tags","summary":"","title":"Speculative Decoding","type":"tags"},{"content":"","date":"2026年6月2日","externalUrl":null,"permalink":"/tags/%E6%9C%AC%E5%9C%B0ai/","section":"Tags","summary":"","title":"本地AI","type":"tags"},{"content":"","date":"2026年6月2日","externalUrl":null,"permalink":"/tags/%E6%8E%A8%E6%B5%8B%E8%A7%A3%E7%A0%81/","section":"Tags","summary":"","title":"推测解码","type":"tags"},{"content":" 引言 # 随着量化技术和硬件能力的进步，在本地运行大语言模型（LLM）变得越来越实用。对于像OpenClaw和Hermes这样的代理框架，拥有一个具有大上下文窗口（10万+令牌）的本地LLM对于维护对话历史和执行复杂的多步骤任务至关重要。\n本指南涵盖了2026年6月适用于128GB显存机器（如Strix Halo配置）的最佳模型，如何使用Docker中的Ollama部署它们，以及如何利用多令牌预测（MTP）进行推测解码以实现更快的推理。\n为什么要本地运行LLM？ # 在深入具体模型之前，以下是本地运行LLM用于代理的关键原因：\n隐私：数据永远不会离开您的机器 延迟：无网络往返意味着更快的响应时间 成本：初始硬件投资后无需API费用 定制化：无限制地微调和修改模型 离线能力：代理无需互联网连接即可工作 128GB显存的最佳模型（2026年6月） # 以下是可以在128GB显存机器上运行、上下文窗口为10万+令牌的顶级模型：\nCohere Command A+（总参数218B，活跃参数25B） # 上下文：128k输入 / 64k输出 显存：4位量化下约110GB 优势：专门为对话式工具使用而训练，非常适合需要调用外部API和工具的代理框架 DeepSeek Coder V2（总参数236B，活跃参数20.9B） # 上下文：128k 显存：4位量化下约118GB 优势：专注于代码生成，支持338种语言，非常适合以代码为中心的代理 DeepSeek V4 Flash（总参数284B，活跃参数13B） # 上下文：128k原生（使用推测解码可达1M） 显存：4位量化下约80GB 优势：旗舰MoE模型，具有出色的推理能力 Gemma 4 26B/31B # 上下文：128k（E4B）至256k（26B/31B） 显存：4位量化下约33GB 优势：Google最新的开放模型，具有出色的长上下文支持 Qwen3-8B-128K # 上下文：128k 显存：4位量化下约4GB 优势：超轻量级，非常适合运行多个代理实例 使用Docker中的Ollama进行部署 # 在本地运行这些模型最简单的方法是使用Docker容器中的Ollama。以下是开始步骤：\n步骤1：获取GGUF模型 # 大多数模型在Hugging Face上以GGUF文件形式提供。例如，获取DeepSeek V4 Flash Q2模型：\n1 2 3 git clone https://github.com/antirez/ds4 cd ds4 ./download_model.sh q2 # 下载约80GB的Q2量化模型 步骤2：在Docker中设置Ollama # 创建模型目录并启动Ollama容器：\n1 2 3 4 5 6 7 8 9 10 11 # 创建模型目录 mkdir -p ~/models cp ds4/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2.gguf ~/models/ # 启动带有模型挂载的Ollama容器 docker run -d \\ --name ollama \\ -p 11434:11434 \\ -v ~/models:/models \\ -e OLLAMA_HOST=0.0.0.0 \\ ollama/ollama 步骤3：将模型导入Ollama # 1 2 3 4 5 6 # 导入GGUF文件 docker exec -it ollama bash -c \\ \u0026#34;ollama import deepseek-v4-flash-q2 /models/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2.gguf\u0026#34; # 运行模型 docker exec -it ollama bash -c \u0026#34;ollama run deepseek-v4-flash-q2\u0026#34; 理解量化：Q2与Q4 # 部署大型模型时，量化对于将其放入显存至关重要。以下是量化级别的含义：\n量化 文件大小 显存使用 质量 FP16 ~570GB ~600GB 基线 Q4 ~150GB ~160GB 最小损失 Q2 ~80GB ~90GB 明显降质 对于128GB显存机器：\nQ4量化推荐用于参数高达约236B的模型 Q2量化允许您运行更大的模型（284B+），但会有一些质量权衡 Imatrix与标准量化 # 某些模型提供使用量化过程中重要性矩阵的\u0026quot;imatrix\u0026quot;变体：\n标准Q2：使用基于权重能量的合成回退启发式方法 Q2-imatrix：使用来自校准数据集的真实激活统计信息 imatrix版本在相同文件大小下提供更好的质量，因为它更智能地将位分配给最重要的参数。\n使用MTP进行推测解码 # 本地LLM推理中最令人兴奋的进步之一是多令牌预测（MTP）与推测解码的结合。以下是其工作原理：\n标准推理的问题 # 传统的自回归生成一次产生一个令牌：\n1 2 3 4 步骤1：预测令牌1（完整前向传播） 步骤2：预测令牌2（完整前向传播） 步骤3：预测令牌3（完整前向传播） ... 这是顺序的，没有有效利用GPU并行性。\nMTP +推测解码的工作原理 # 草稿阶段：MTP头使用单次前向传播并行预测多个候选令牌 验证阶段：基础模型在一次额外前向传播中验证所有候选 接受/拒绝：匹配的令牌保留；不匹配触发从该点重新生成 加速效果 # 根据DeepSeek报告的85-90%接受率，您可以实现：\n1.8倍吞吐量改进（每秒令牌数） 多令牌生成的延迟降低 在Ollama中使用MTP # 要在Ollama中启用MTP，请创建一个引用基础模型和MTP辅助文件的Modelfile：\n1 2 FROM /models/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2.gguf MTP /models/DeepSeek-V4-Flash-MTP-Q4K-Q8_0-F32.gguf 然后创建并运行模型：\n1 2 ollama create deepseek-v4-flash-q2-mtp -f Modelfile ollama run deepseek-v4-flash-q2-mtp 为您的用例选择合适的模型 # 以下是快速参考指南：\n用例 推荐模型 原因 带工具调用的代理编排 Cohere Command A+ 专门为工具使用而训练 代码生成和调试 DeepSeek Coder V2 专注于代码，128k上下文 快速、低延迟响应 Qwen3-8B-128K 仅需4GB显存，可运行多个实例 平衡推理+长上下文 Gemma 4 26B/31B 最高256k上下文，适中显存 最大性能 DeepSeek V4 Flash 13B活跃参数，支持MTP加速 性能技巧 # 量化到4位以获得质量和显存使用的最佳平衡 使用GGUF格式与基于llama.cpp的运行器，实现自动CPU卸载 为DeepSeek V4 Flash启用MTP以获得1.8倍速度改进 运行多个较小模型而不是一个大模型（如果您需要并发代理） 使用nvidia-smi监控显存使用情况以避免内存不足错误 结论 # 本地运行LLM用于代理从未如此简单。拥有128GB显存，您可以灵活选择多个具有128k+上下文窗口的优秀模型。无论您优先考虑工具使用能力（Command A+）、代码生成（DeepSeek Coder V2）还是原始性能（DeepSeek V4 Flash），都有适合您需求的模型。\nOllama的简单部署与推测解码的快速推理相结合，使本地代理部署既实用又高效。随着量化技术的不断改进，我们可以期待更大的模型在消费级硬件上变得可用。\n参考资料 # Cohere Command A+ 模型卡 DeepSeek V4 Flash GGUF Gemma 4 模型卡 Ollama 文档 推测解码研究 ","date":"2026年6月2日","externalUrl":null,"permalink":"/p/%E4%B8%BA%E4%BB%A3%E7%90%86%E9%83%A8%E7%BD%B2%E6%9C%AC%E5%9C%B0%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%8C%E6%95%B4%E6%8C%87%E5%8D%97/","section":"Posts","summary":" 引言 # 随着量化技术和硬件能力的进步，在本地运行大语言模型（LLM）变得越来越实用。对于像OpenClaw和Hermes这样的代理框架，拥有一个具有大上下文窗口（10万+令牌）的本地LLM对于维护对话历史和执行复杂的多步骤任务至关重要。\n","title":"为代理部署本地大语言模型：完整指南","type":"post"},{"content":"","date":"2026年5月12日","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"AI","type":"tags"},{"content":"","date":"12 五月 2026","externalUrl":null,"permalink":"/en/series/ai-assistant-series/","section":"Series","summary":"","title":"AI Assistant Series","type":"series"},{"content":"","date":"2026年5月12日","externalUrl":null,"permalink":"/series/ai%E5%8A%A9%E6%89%8B%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"AI助手系列","type":"series"},{"content":"","date":"12 五月 2026","externalUrl":null,"permalink":"/en/categories/artificial-intelligence/","section":"Categories","summary":"","title":"Artificial Intelligence","type":"categories"},{"content":" 缘起 # 在配置了自己的AI助手OpenClaw后，记录一下整个设置过程，包括配置在线模型、本地模型、测试模型性能、添加技能等。\n前提 # 已有一台服务器或本地电脑 已安装Docker和docker-compose 已配置好网络（Tailscale VPN） 已安装Ollama（用于运行本地模型） OpenClaw概述 # OpenClaw是一个AI助手框架，支持多种模型提供商，包括在线模型和本地模型。主要特点：\n支持多种在线模型提供商（OpenRouter、DeepSeek等） 支持本地模型（Ollama） 支持技能系统 支持多节点部署 配置在线模型 # OpenRouter # 配置OpenRouter作为在线模型提供商，可以访问多种开源和商业模型：\nopenrouter/qwen3.6-plus:free - 免费Qwen模型 openrouter/free - 免费模型池 deepseek/deepseek-chat - DeepSeek模型 模型配置 # 在~/.openclaw/openclaw.json中配置模型提供商和模型列表：\n1 2 3 4 5 6 7 8 9 10 11 12 { \u0026#34;models\u0026#34;: { \u0026#34;providers\u0026#34;: { \u0026#34;openrouter\u0026#34;: { \u0026#34;models\u0026#34;: [\u0026#34;qwen3.6-plus:free\u0026#34;, \u0026#34;free\u0026#34;] }, \u0026#34;deepseek\u0026#34;: { \u0026#34;models\u0026#34;: [\u0026#34;deepseek-chat\u0026#34;] } } } } 模型回退链 # 配置模型回退链，当主模型不可用时自动切换到备用模型：\nopenrouter/qwen3.6-plus:free ollama/qwen3.5-35b-iq2（本地模型） ollama/gemma-4-e4b-q5（本地模型） openrouter/free deepseek/deepseek-chat 配置本地模型 # Ollama配置 # Ollama运行在Docker容器中，地址为100.64.0.3:11434：\n1 2 3 4 5 6 7 8 9 services: ollama: container_name: ollama image: ollama/ollama ports: - \u0026#34;11434:11434\u0026#34; volumes: - ollama:/root/.ollama restart: always 本地模型列表 # qwen3.5-35b-iq2 # 架构：MoE（混合专家模型），35B总参数，~3B活跃参数 量化：UD-IQ2_XXS，10.7GB 优势：轻量计算，完全适配16GB VRAM 工具调用：需要Modelfile中添加RENDERER qwen3.5和PARSER qwen3.5 gemma-4-e4b-q5 # 架构：Dense E4B，7.5B总参数，4.5B有效参数 量化：Unsloth Q5_K_M，6.3GB 优势：128K上下文，原生工具调用支持 注意：需要Ollama v0.20+支持 Modelfile配置 # 自定义GGUF模型需要添加RENDERER和PARSER：\n1 2 3 FROM ./qwen3.5-35b-iq2.gguf RENDERER qwen3.5 PARSER qwen3.5 测试模型性能 # 长上下文测试 # 测试gemma-4-e4b-q5的128K上下文能力：\n5/5 needles at 96K tokens 测试耗时：51.8s 推理测试 # Qwen MoE在Einstein谜题测试中表现更好，但Gemma 4在长上下文场景更有优势。\n工具调用测试 # Qwen 3.5-35b-IQ2：结构化输出和工具使用表现更好 Gemma 4：长上下文工具调用表现良好 添加技能 # deep-read技能 # 位置：skills/deep-read/\n用于深度阅读书籍、论文、文章，生成结构化笔记。\ncf-fetcher技能 # 位置：skills/cf-fetcher/\n减少HTML→Markdown转换的token消耗65-80%，适用于新闻网站、博客、论坛。\n浏览器自动化技能 # 位置：skills/browser-automation/\n用于控制网页的多步骤流程。\n技能配置 # 在~/.openclaw/openclaw.json中注册技能：\n1 2 3 4 5 6 7 8 9 10 { \u0026#34;skills\u0026#34;: { \u0026#34;deep-read\u0026#34;: { \u0026#34;location\u0026#34;: \u0026#34;skills/deep-read/\u0026#34; }, \u0026#34;cf-fetcher\u0026#34;: { \u0026#34;location\u0026#34;: \u0026#34;skills/cf-fetcher/\u0026#34; } } } Xiaomi MiMo模型 # 订阅了Xiaomi token plan（$6/月，60M credits）：\nmimo-v2-pro - 1M上下文，推理，信用=2×token（ctx\u0026lt;256K） mimo-v2-omni - 1M上下文，多模态（文本+图片） mimo-v2-tts - 32K上下文，TTS，最大1024 tokens 关键经验总结 # 配置源真相：~/.openclaw/openclaw.json，不是~/.openclaw/agents/main/agent/models.json MoE模型：关键在于完全适配VRAM，IQ2_XXS质量可接受因为只有~3B参数激活 Gemma 4 GGUF：需要Ollama v0.20+，旧版本无法加载 模型回退：仅在有认证失败、速率限制、超时、计费错误时触发 Node配置：ws://连接需要OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1 Ollama Modelfile：自定义GGUF必须添加RENDERER和PARSER 相关链接 # OpenClaw文档 OpenClaw GitHub Ollama OpenRouter ","date":"2026年5月12日","externalUrl":null,"permalink":"/p/openclaw-ai%E5%8A%A9%E6%89%8B%E8%AE%BE%E7%BD%AE%E6%80%BB%E7%BB%93/","section":"Posts","summary":" 缘起 # 在配置了自己的AI助手OpenClaw后，记录一下整个设置过程，包括配置在线模型、本地模型、测试模型性能、添加技能等。\n","title":"OpenClaw AI助手设置总结","type":"post"},{"content":"","date":"2026年5月12日","externalUrl":null,"permalink":"/tags/openrouter/","section":"Tags","summary":"","title":"OpenRouter","type":"tags"},{"content":"","date":"12 五月 2026","externalUrl":null,"permalink":"/en/tags/skills/","section":"Tags","summary":"","title":"Skills","type":"tags"},{"content":"","date":"2026年5月12日","externalUrl":null,"permalink":"/tags/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B/","section":"Tags","summary":"","title":"大语言模型","type":"tags"},{"content":"","date":"2026年5月12日","externalUrl":null,"permalink":"/tags/%E6%8A%80%E8%83%BD/","section":"Tags","summary":"","title":"技能","type":"tags"},{"content":"","date":"2026年5月12日","externalUrl":null,"permalink":"/categories/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/","section":"Categories","summary":"","title":"人工智能","type":"categories"},{"content":"","date":"2026年3月17日","externalUrl":null,"permalink":"/tags/container/","section":"Tags","summary":"","title":"Container","type":"tags"},{"content":"","date":"2026年3月17日","externalUrl":null,"permalink":"/tags/docker/","section":"Tags","summary":"","title":"Docker","type":"tags"},{"content":"","date":"2026年3月17日","externalUrl":null,"permalink":"/tags/portainer/","section":"Tags","summary":"","title":"Portainer","type":"tags"},{"content":"","date":"2026年3月17日","externalUrl":null,"permalink":"/categories/%E5%BC%80%E5%8F%91/","section":"Categories","summary":"","title":"开发","type":"categories"},{"content":" 缘起 # 我们之前介绍了使用Portainer来监控和管理电脑上运行的容器，Portainer提供了可视化的界面，可以方便地查看容器日志、重启容器、管理镜像文件等。在配置了反向代理后，我们可以方便地使用域名来远程访问，例如https://portainer.example.com。\n但是之前的文章只介绍了用Portainer监控本地电脑上的容器。后来我又在我的其他电脑上安装运行了Docker容器，如果也想用Portainer来监控，一个自然的想法是，在每台运行Docker容器的电脑上都安装Portainer，用不同的域名来反向代理，例如portainer1.example.com、portainer2.example.com、portainer3.example.com等等。\n这种方法当然是行得通的，就是比较碎片化。Portainer是支持远程监控其他电脑上的Docker容器的，这就是Portainer里的Agent。\n简介 # Docker提供了一个位于/var/run/docker.sock的套接口（socket）,用于Docker守护进程与其他工具之间的通信。其他工具可以通过这个套接口获取Docker容器的运行状态，也可以向这个套接口发送命令来管理Docker容器。\n工作原理 # Portainer可以通过访问远程电脑上的Docker套接口来管理远程电脑上的Docker容器。具体过程如下：\n首先我们确定一个主Portainer，例如本地电脑A上运行的Portainer。其他远程电脑运行Agent。 Agent1向主Portainer报告自己这台电脑上的Docker容器的状态，主Portainer将其显示在Portainer的UI界面上。用户可以在这个界面上看到其他电脑上运行的容器状态。 若用户想要管理其他电脑上的容器，则直接在Portainer的UI界面上操作，Portainer会将用户的操作发送给Agent。Agent完成相应的操作后将新的状态再次报告给主Portainer。 实现方式 # 上述工作原理的实现方式很多，常见的有两种：Portainer Agent和Portainer Edge Agent。\nPortainer Agent # 在远程电脑B上运行一个Portainer Agent容器，这个Agent读取电脑B上的/var/run/docker.sock。 本地电脑A上运行Portainer Server (即普通的Portainer)，添加一个远程环境指向电脑B，向电脑B上的Agent容器索要电脑B上的容器状态，向电脑B上的Agent发送操作指令。电脑B上的Agent执行电脑A通过Portainer发送来的指令。 这种实现方式的关键是本地电脑A的Portainer要能主动访问到远程电脑B的Agent。如果A和B在同一个局域网里，那只需要通过局域网里的IP地址和端口直接访问就行了；如果A和B不在同一个局域网里，我们就需要将电脑B上的Agent容器暴露到公网，可以通过域名反向代理来暴露。\nPortainer Edge Agent # 上述Agent方式将远程电脑的Agent都暴露在公网的方法显然不太安全，尽管我们可以设置口令进行加密，但依然可能被攻击。\n于是有了一种更安全的方法，远程电脑B上的Agent不需要暴露到公网，本地电脑A也不会主动去找B。反之，远程电脑B上的Agent每隔一段时间（默认是5秒）向电脑A上的Portainer汇报一次，并查看是否有什么任务交给自己。\n在本地电脑A看来，它不知道远程电脑B在哪，只知道自己会定时获取到电脑B交给自己的状态，自己给B列的任务也会被B拿走执行。\n这样，我们就只暴露了本地电脑A的Portainer，这个Portainer只负责收取其他远程Agent的状态更新和向其他Agent发布任务。无论是本地电脑A还是其他电脑，他们都不接受别的电脑的指令，这样自然更安全。这种Agent称为Edge Agent。\n对比 # 普通的Agent是本地电脑主动找远程电脑，按需指挥它们；Edge Agent是远程电脑定时向本地电脑汇报，并领取任务。\n因此，普通Agent模式更高效，没有任务时不会去找远程电脑。Edge Agent则是不管远程电脑有没有任务，都定时去找本地电脑。不过，这种通信并不会太消耗电脑性能资源，如果我们设置的汇报时间间隔大一点儿（例如30秒），那么Edge Agent模式的性能开销就微乎其微了。\n综上，这里我们选择使用Edge Agent模式。\nPortainer Edge Agent部署 # 这里我们选用的工具链如下：\nPortainer: 免费的社区版portainer-ce 反向代理：Traefik和免费版的Cloudflare Tunnel Portainer Server配置 # Portainer Server实际上就是我们在之前的文章中讲的Portainer的部署，只不过为了让Edge Agent能连上，我们需要添加一些反向代理的设置。\nPortainer容器的docker-compose.yml文件\n设置Traefik路由规则portainer.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 http: routers: portainer: rule: \u0026#34;Host(`550w.jinli.io`)\u0026#34; entryPoints: - websecure service: portainer-api tls: {} portainer-edge: rule: \u0026#34;Host(`edge.jinli.io`)\u0026#34; entryPoints: - websecure tls: {} service: portainer-edge services: portainer-api: loadBalancer: servers: - url: \u0026#34;http://portainer:9000\u0026#34; portainer-edge: loadBalancer: servers: - url: \u0026#34;http://portainer:8000\u0026#34; 之前的路由规则只暴露了9000端口，用于Portainer的网页界面。 现在我们暴露8000端口，用于Edge Agent的连接。\n在Portainer中创建Edge Agent环境时，Portainer默认使用Portainer的域名加8000端口（如portainer.example.com:8000）进行连接。但由于我们用Cloudflare Tunnel来做反向代理，Cloudflare Tunnel不会监听8000端口，因此，我们需要再给portainer:8000分配一个域名（如edge.example.com，并且让Edge Agent连接新的域名，而非portainer.example.com:8000。\nCloudflare Tunnel\n先在Cloudflare网页客户端中为edge.example.com添加DNS记录（如果之前没添加portainer.example.com的话，也创建一个），记录是指向Cloudflare Tunnel的CNAME。\nCloudflare Tunnel里我们需要同时代理域名portainer.example.com和edge.example.com，将下面的代码加入到原来的config.yml中：\n1 2 3 4 5 6 7 8 - hostname: edge.jinli.io service: https://traefik:443 originRequest: noTLSVerify: true - hostname: edge.jinli.io service: https://traefik:443 originRequest: noTLSVerify: true 这些全部设置好之后，启动这些容器服务，就可以通过https://portainer.example.com在浏览器中访问Portainer界面了。\nPortainer网页界面添加Edge Agent环境 # 在Portainer的网页界面左侧，管理员（Administration）区域有个Environment-related选项，打开这个选项就可以看到如下界面。\n点击“添加环境”（Add Environment），选择Docker Standalone，点击Start Wizard，选择Edge Agent，根据提示创建一个Edge Agent。\n创建完成后，会显示一个Docker命令告诉你怎样在远程电脑上创建Edge Agent容器。由于我们使用docker-compose.yml来管理容器，所以我们不用这个命令。但我们需要记下来这个命令里出现的EDGE_ID和EDGE_KEY，将这两个值保存下来。\n创建Edge Agent # 重新编码EDGE_KEY\nEDGE_KEY保存了Portainer Server的信息，Edge Server就靠这里面的信息来寻找Portainer Server。\n在命令行运行如下命令对EDGE_KEY进行解码（把命令中的your_edge_key换成你在上一步获取的EDGE_KEY的值）：\n1 echo your_edge_key | base64 -d 你会得到格式如下的一串字符：\n1 https://portainer.example.com|portainer.example.com:8000|xxxxxxxxfinger_print|3 其中第一段https://portainer.example.com就是告诉Edge Server去https://portainer.example.com找Portainer Server。\n第二段的portainer.example.com:8000表示在Edge Agent找到Portainer Server后，向portainer.example.com:8000发送请求建立连接。\n我们刚才说了，我们的portainer.example.com是用Cloudflare Tunnel代理的，Cloudflare Tunnel不会监听8000端口。所以我们创建了个新域名edge.example.com来代理portainer:8000。于是我们用新域名替换掉第二段里的portainer.example.com:8000，替换后的字符串长这样：\n1 https://portainer.example.com|edge.example.com|xxxxxxxxfinger_print|3 最后，我们把替换后的字符串重新编码：\n1 echo -n https://portainer.example.com|edge.example.com|xxxxxxxxfinger_print|3 | base64 -w0 将重新编码的字符串保存下来，这就是新的EDGE_KEY。\n注意，重新编码的字符串末尾如果有=，Edge Agent容器很可能不认，可以删掉。\nEdge Agent容器部署\n接下来我们在远程电脑上部署Edge Agent。\n先创建一个.env文件，写入EDGE_ID和魔改后的EDGE_KEY，也可以把要多长时间汇报一次状态也定义到环境变量里（这里是每30秒）：\n1 2 3 EDGE_ID=xxxxxxxxxx EDGE_KEY=xxxxxxxxxxxxxxxxxxxxxxx EDGE_POLL_INTERVAL=30 在创建一个docker-compose.yml文件:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 version: \u0026#34;3.8\u0026#34; services: portainer-edge-agent: image: portainer/agent:2.39.0 container_name: portainer-edge-agent restart: unless-stopped environment: EDGE: \u0026#34;1\u0026#34; EDGE_ID: ${EDGE_ID} EDGE_KEY: ${EDGE_KEY} EDGE_INSECURE_POLL: \u0026#34;1\u0026#34; EDGE_POLL_INTERVAL: ${EDGE_POLL_INTERVAL} volumes: - /var/run/docker.sock:/var/run/docker.sock - /var/lib/docker/volumes:/var/lib/docker/volumes 直接启动这个容器就行了，不需要设置代理，因为Portainer Server不需要去找它，它会根据EDGE_KEY里的信息去寻找Portainer Server。\n部署完毕 # 完成上述步骤后，就可以在本地电脑的Portainer界面里看到远程电脑的容器了:\n点击进入，看到的界面和我们在本地电脑上的Portainer容器管理界面是一样的。\n添加更多Edge Agent # 如果你想把更多电脑添加到本地的Portainer界面，只需要重复上面的步骤就行了。\n注意，添加每台电脑时都需要重新创建一个新的环境，并获取新的EDGE_ID和EDGE_KEY，环境不能共用。\n常见问题 # 如果在部署Portainer Edge Agent容器后容器内报类似下面所示的错误：\n1 2026/03/19 08:30:37.506PM FTL github.com/portainer/agent/cmd/agent/main.go:262 \u0026gt; unable to associate Edge key | error=\u0026#34;invalid key format\u0026#34; 这说明在对EDGE_KEY重新编码时使用的base64设置不对。我们需要将EDGE_KEY编码为Base64URL格式，需要使用base64url命令，但Linux默认只有base64命令，为此我们加了-w0参数，但这个参数只是为了禁用换行功能。\n如果出现了上述错误，可以使用某些可以进行Base64URL编码的网站来编码，例如：https://www.base64encode.org/，记得勾选Perform URL safe encoding选项。\n","date":"2026年3月17日","externalUrl":null,"permalink":"/p/%E5%AE%B9%E5%99%A88%E4%BD%BF%E7%94%A8portainer-agent%E7%9B%91%E6%8E%A7%E8%BF%9C%E7%A8%8B%E7%94%B5%E8%84%91%E4%B8%8A%E7%9A%84docker%E5%AE%B9%E5%99%A8/","section":"Posts","summary":" 缘起 # 我们之前介绍了使用Portainer来监控和管理电脑上运行的容器，Portainer提供了可视化的界面，可以方便地查看容器日志、重启容器、管理镜像文件等。在配置了反向代理后，我们可以方便地使用域名来远程访问，例如https://portainer.example.com。\n","title":"容器（8）：使用Portainer Agent监控远程电脑上的Docker容器","type":"post"},{"content":"","date":"2026年3月17日","externalUrl":null,"permalink":"/series/%E5%AE%B9%E5%99%A8%E6%8A%80%E6%9C%AF%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"容器技术系列","type":"series"},{"content":"","date":"13 三月 2026","externalUrl":null,"permalink":"/en/series/large-language-model-series/","section":"Series","summary":"","title":"Large Language Model Series","type":"series"},{"content":"","date":"2026年3月13日","externalUrl":null,"permalink":"/tags/open-webui/","section":"Tags","summary":"","title":"Open WebUI","type":"tags"},{"content":"","date":"2026年3月13日","externalUrl":null,"permalink":"/tags/vllm/","section":"Tags","summary":"","title":"VLLM","type":"tags"},{"content":" 缘起 # 之前我们讨论过自建大语言模型服务，当时使用的是Ollama作为后端服务，使用Lobechat作为前端，通过API调用后端的Ollama来实现对话。\n最近我在尝试使用OpenClaw，但它烧Token的速度实在太快了，就想着自己运行一个本地的大语言模型来提供Token。跟ChatGPT聊过之后，它告诉我这种情况下最好使用vLLM而非Ollama，因为vLLM提供与OpenAI兼容的API，支持Copilot，且性能更好。于是我就试着转向了vLLM。\n简介 # vLLM # vLLM和Ollama都是在自己电脑上或者在服务器上运行大语言模型（LLM）的工具，然而他们的使用场景却有所不同。简单来讲，vLLM致力于高效地运行大语言模型，而Ollama专注于使大语言模型方便地运行起来。\n另外，vLLM提供与OpenAI兼容的API，如果我们要运行的服务依赖于OpenAI之类的商业模型API，而且支持用户自定义模型提供商，那我们就可以接入我们自己部署的vLLM。\nOllama # 关于Ollama的介绍和部署请参考本系列的第一篇文章。\n由于Ollama使用的是一些自定义的REST API，和OpenAI的API不兼容，不能直接接入很多使用大语言模型的服务。不过，有开发者开发了一款可以调用Ollama并兼容OpenAI API的工具LiteLLM。\n我们只需要将这个工具和Ollama容器一块儿部署，再做一些设置，它就可以提供与OpenAI的API兼容的API。\nvLLM和Ollama的比较及注意事项 # 显存占用\n在运行时，我们能感知到的vLLM与Ollama最大的区别是，部署vLLM需要将整个模型加载到GPU的显存里，即使模型处于闲置（未被调用）的状态，vLLM依然会占用这部分显存。\n也就是说，vLLM几乎不能和其他重度使用显卡的程序一块儿运行。例如，运行vLLM后，如果你想打游戏，那就需要先下线vLLM。\n而Ollama则没有这个问题。Ollama只有在被调用时才会把模型加载到显存里，在你想打游戏时，你不需要关闭Ollama，只要在你打游戏期间不去调用Ollama，那它就能和你的游戏相安无事。\n可调用模型数量\n在部署Ollama后，我们可以下载多个模型，用户可以决定调用哪一个。\n但在部署vLLM后，加载到显存中的模型是固定的，一个vLLM容器只能加载一个模型，要想选择模型调用，就需要同时部署多个vLLM容器。这对于我们个人用户显然是不现实的，我们的显卡一般是做不到的。如果想加载多个模型，那就只能用很小的模型，意义不大。\n部署大语言模型及前端 # 总览 # 我有两台有16GB显存显卡的电脑，因此我选择在电脑A上部署vLLM，在电脑B上部署带有vLLM接口的Ollama（我们暂且称其为vOllama）。通过域名将vLLM和vOllama的接口暴露于公网。\n在任一台电脑上（可以是A或B，也可以是另一台电脑C）上部署Open WebUI前端，它可以方便地接入自己部署的大语言模型。\nvLLM部署 # docker-compose # docker-compose.yml文件如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 services: vllm: image: vllm/vllm-openai:v0.17.0 container_name: vllm restart: unless-stopped runtime: nvidia deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu] environment: - HUGGING_FACE_HUB_TOKEN=${HF_TOKEN} - PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True - HF_HUB_DISABLE_XET=1 - HF_HUB_ENABLE_HF_TRANSFER=0 - VLLM_API_KEY=${VLLM_API_KEY:-dummy} - TZ=${TZ:-UTC} volumes: - ~/.cache/huggingface:/root/.cache/huggingface ipc: host command: \u0026gt; ${MODEL} ${MODEL_PARAMETERS} networks: traefik-net: aliases: - vllm networks: traefik-net: external: true 这里的MODEL、MODEL_PARAMETERS和其他变量在环境变量文件.env文件中定义：\n1 2 3 4 5 HF_TOKEN=hf_your_token MODEL=Qwen/Qwen2.5-Coder-14B-Instruct-AWQ MODEL_PARAMETERS=\u0026#34;--host 0.0.0.0 --port 8000 --gpu-memory-utilization 0.81 --dtype auto --kv-cache-dtype fp8 --enforce-eager\u0026#34; VLLM_API_KEY=your_api_key TZ=Europe/Paris 其中，HF_TOKEN是Huggingface的token，我们需要它以便从Huggingface下载大语言模型。\nvLLM只能加载一个模型，我们选了Qwen/Qwen2.5-Coder-14B-Instruct-AWQ，加载它大约需要14GB的显存。\n测试 # 在命令行中输入类似下面的命令来测试我们的vLLM是否部署成功：\n1 2 3 4 5 6 7 curl https://vllm.example/v1/chat/completions \\ -H \u0026#34;Authorization: Bearer your_vllm_api_key\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;Qwen/Qwen2.5-Coder-14B-Instruct-AWQ\u0026#34;, \u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Hello\u0026#34;}] }\u0026#39; 如果收到类似下面的回复就表示vLLM已成功部署并可以通过域名访问：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 { \u0026#34;id\u0026#34;: \u0026#34;chatcmpl-82d9387336a446bd\u0026#34;, \u0026#34;object\u0026#34;: \u0026#34;chat.completion\u0026#34;, \u0026#34;created\u0026#34;: 1774276907, \u0026#34;model\u0026#34;: \u0026#34;Qwen/Qwen2.5-Coder-14B-Instruct-AWQ\u0026#34;, \u0026#34;choices\u0026#34;: [ { \u0026#34;index\u0026#34;: 0, \u0026#34;message\u0026#34;: { \u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Hello! How can I assist you today?\u0026#34;, \u0026#34;refusal\u0026#34;: null, \u0026#34;annotations\u0026#34;: null, \u0026#34;audio\u0026#34;: null, \u0026#34;function_call\u0026#34;: null, \u0026#34;tool_calls\u0026#34;: [], \u0026#34;reasoning\u0026#34;: null }, \u0026#34;logprobs\u0026#34;: null, \u0026#34;finish_reason\u0026#34;: \u0026#34;stop\u0026#34;, \u0026#34;stop_reason\u0026#34;: null, \u0026#34;token_ids\u0026#34;: null } ], \u0026#34;service_tier\u0026#34;: null, \u0026#34;system_fingerprint\u0026#34;: null, \u0026#34;usage\u0026#34;: { \u0026#34;prompt_tokens\u0026#34;: 30, \u0026#34;total_tokens\u0026#34;: 40, \u0026#34;completion_tokens\u0026#34;: 10, \u0026#34;prompt_tokens_details\u0026#34;: null }, \u0026#34;prompt_logprobs\u0026#34;: null, \u0026#34;prompt_token_ids\u0026#34;: null, \u0026#34;kv_transfer_params\u0026#34;: null } 在命令行里这个JSON会被压缩在一行显示。\nvOllama部署 # 配置文件 # docker-compose.yml文件如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 services: ollama: container_name: ollama image: docker.1ms.run/ollama/ollama:0.17.7 environment: - OLLAMA_ORIGINS=* - OLLAMA_HOST=0.0.0.0 - TZ=America/New_York deploy: resources: reservations: devices: - driver: nvidia capabilities: [\u0026#34;gpu\u0026#34;] count: all volumes: - ./ollama-data:/root/.ollama restart: always networks: - traefik-net litellm: image: ghcr.io/berriai/litellm:main-latest container_name: litellm restart: unless-stopped command: [\u0026#34;--config\u0026#34;, \u0026#34;/app/config.yaml\u0026#34;] volumes: - ./litellm-config.yaml:/app/config.yaml networks: - traefik-net networks: traefik-net: external: true 这里我们部署了ollama和litellm两个容器，它们都使用我们之前创建的网络traefik-net，我们依然使用Cloudflare Tunnel来反向代理vOllama服务。\nlitellm需要一个配置文件litellm-config.yml，如下所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 model_list: - model_name: qwen3.5:0.8b litellm_params: model: ollama/qwen3.5:0.8b api_base: http://ollama:11434 - model_name: mistral:7b litellm_params: model: ollama/mistral:7b api_base: http://ollama:11434 general_settings: master_key: your_ollama_api_key 配置文件主要有以下作用：\nmodel_list告诉litellm我们在ollama里面运行了哪些大语言模型。当我们给Ollama下载安装了其他模型，我们需要更新这个文件以加入新的模型。 由于我们将模型暴露在了公网上，我们需要设置一个API key（即master_key）来控制访问，不提供API key的不能调用我们的模型。 安装大语言模型 # 使用docker compose up -d来部署Ollama容器。 部署成功后，Ollama还是空的，里面没有下载任何大语言模型。我们使用docker exec -it ollama ollama run qwen3.5:0.8b来下载用运行qwen3.5:0.8b这个模型。 安装完成后可以继续下载其他模型 使用docker exec -it ollama ollama list可以列出安装了哪些模型。 测试 # 可以使用如下命令测试是否部署成功：\n1 2 3 4 5 6 7 curl https://ollama.example/v1/chat/completions \\ -H \u0026#34;Authorization: Bearer your_ollama_api_key\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;qwen3.5:0.8b\u0026#34;, \u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Hello\u0026#34;}] }\u0026#39; 如果收到类似下面的回复就表示vOllama已成功部署并可以通过域名访问：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { \u0026#34;id\u0026#34;: \u0026#34;chatcmpl-5844b507-f967-4412-b8fa-815c4e3235b1\u0026#34;, \u0026#34;created\u0026#34;: 1774276549, \u0026#34;model\u0026#34;: \u0026#34;qwen3.5:0.8b\u0026#34;, \u0026#34;object\u0026#34;: \u0026#34;chat.completion\u0026#34;, \u0026#34;choices\u0026#34;: [ { \u0026#34;finish_reason\u0026#34;: \u0026#34;stop\u0026#34;, \u0026#34;index\u0026#34;: 0, \u0026#34;message\u0026#34;: { \u0026#34;content\u0026#34;: \u0026#34;Greetings! How\u0026#39;s going? 😊 Feel free to ask me anything—whether you need help, just wanted to say hello, or maybe something else. I\u0026#39;m here for fun and helpful!\u0026#34;, \u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34; } } ], \u0026#34;usage\u0026#34;: { \u0026#34;completion_tokens\u0026#34;: 111, \u0026#34;prompt_tokens\u0026#34;: 15, \u0026#34;total_tokens\u0026#34;: 126 } } 在命令行里这个JSON会被压缩在一行显示。\nOpen WebUI部署 # 只用curl或者API来调用部署的大模型显然是不方便的，我们需要部署一个网页版的客户端。这里我们选用一个开源的客户端Open WebUI。\nOpen WebUI简介 # Open WebUI是一个开源的大语言模型网页版客户端，风格跟OpenUI的ChatGPT页面很像。它允许我们接入自己部署的vLLM或者Ollama模型，也支持接入商用的各种模型（需要API key）。\nOpen WebUI部署 # docker-compose.yml文件如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 version: \u0026#34;3.9\u0026#34; services: open-webui: image: ghcr.io/open-webui/open-webui:0.8.10 container_name: open-webui volumes: - ${DATA_DIR}:/app/backend/data environment: - OPENAI_API_BASE_URL=http://vllm:8000/v1 - OPENAI_API_KEY=${VLLM_API_KEY:-dummy} - TZ=${TZ:-UTC} restart: unless-stopped networks: traefik-net: aliases: - open-webui networks: traefik-net: external: true 这里的OPENAI_API_BASE_URL和OPENAI_API_KEY填了我们在本地部署的vLLM。不填也可以，待会儿可以在图形界面设置。\n使用下面的命令启动open-webui容器：\n1 docker compose up -d Open WebUI配置 # 设置反向代理后，我们就可以通过域名访问Open WebUI了。第一次访问时，Open WebUI会要求我们设置一个管理员账号和密码。\n用管理员账号登录后，点击右上角的头像，可以看到\u0026quot;Admin Panel\u0026quot;选项，点击进入可以设置Open WebUI所需要连接的后端模型服务。\n在“Admin Panel\u0026quot;的\u0026quot;Settings\u0026quot;页面里，有”Connections\u0026quot;选项，在这里可以添加我们之前部署的Ollama服务、vLLM服务，或者vOllama服务，以及商用的大语言模型服务（如OpenAI、Azure、Anthropic等）。\n添加完成后，点击\u0026quot;settings\u0026quot;页面里的\u0026quot;Models\u0026quot;选项，你可以看到Ollama服务或者vLLM服务中可用的模型。这些模型默认的权限是\u0026quot;private\u0026quot;，我们可以点击某个模型右侧的编辑按钮，在模型具体设置里点击\u0026quot;Access\u0026quot;选项，具体设置哪些用户可以访问这个模型。\n最后，在设置好模型后，就可以像使用ChatGPT一样使用Open WebUI了，输入问题后，Open WebUI会调用我们之前部署的vLLM或者Ollama服务来获取回复：\n问题解决 # Ollama安装Qwen3.6 # 在使用Ollama安装去审核版Qwen3.6模型Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive时，可能会遇到安装失败的问题。报错信息如下：\n1 llama_model_load: error loading model: error loading model architecture: unknown model architecture: \u0026#39;qwen35moe\u0026#39; 这个问题在GitHub上有讨论：\nhttps://github.com/ollama/ollama/issues/14503\n其原因是在下载Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive模型完成后，Ollama生成的模型文件Modelfile并不正确。里面有两行From /root/.ollama/xxx，这表明Ollama试图加载两个模型文件，但实际上只需要加载一个模型文件。我们需要将第二个From /root/.ollama/xxx删除掉或用#注释掉，保存后重新运行这个模型就可以了。\n但实际上Modelfile似乎并没有被显式地创建，我们需要进入容器\n1 docker exec -it ollama bash 然后在容器内内执行下面的命令来生成这个文件：\n1 ollama show --modelfile hf.co/HauhauCS/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive:IQ2_M | cat \u0026gt; /root/Modelfile 注释掉或者删除掉第二行From /root/.ollama/xxx后，保存退出，然后重新创建这个模型：\n1 ollama create Qwen3.6-35B-A3B-Uncensored-IQ2M -f /root/Modelfile ","date":"2026年3月13日","externalUrl":null,"permalink":"/p/%E6%9C%AC%E5%9C%B0%E8%BF%90%E8%A1%8C%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E4%BA%8Cvllm%E5%92%8Collama%E7%9A%84%E6%AF%94%E8%BE%83%E5%8F%8A%E9%83%A8%E7%BD%B2open-webui%E5%89%8D%E7%AB%AF/","section":"Posts","summary":" 缘起 # 之前我们讨论过自建大语言模型服务，当时使用的是Ollama作为后端服务，使用Lobechat作为前端，通过API调用后端的Ollama来实现对话。\n","title":"本地运行大语言模型（二）：vLLM和Ollama的比较及部署Open WebUI前端","type":"post"},{"content":"","date":"2026年3月13日","externalUrl":null,"permalink":"/series/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"大语言模型系列","type":"series"},{"content":"","date":"2026年3月4日","externalUrl":null,"permalink":"/page/","section":"Pages","summary":"","title":"Pages","type":"page"},{"content":"","date":"2026年3月4日","externalUrl":null,"permalink":"/%E5%85%B3%E4%BA%8E/","section":"Pages","summary":"","title":"关于","type":"page"},{"content":"","date":"11 二月 2026","externalUrl":null,"permalink":"/en/series/container-technology-series/","section":"Series","summary":"","title":"Container Technology Series","type":"series"},{"content":"","date":"11 二月 2026","externalUrl":null,"permalink":"/en/categories/development/","section":"Categories","summary":"","title":"Development","type":"categories"},{"content":" 缘起 # 由于国内的网络环境问题，我们无法直接从dockerhub拉取容器镜像。为了能在国内的电脑上方便地部署一些容器服务，我决定在一台位于国外的服务器上自建一个容器镜像仓库（即Registry）。\n方案 # Docker官方提供了容器镜像仓库的镜像Registry，我们可以用这个镜像来部署自己的容器镜像仓库。\n我们还需要给这个仓库配置一个图形界面，便于查看。网上有不少第三方的图形界面，这里我选用了Joxit/docker-registry-ui。\n部署、配置与使用 # 部署镜像仓库服务 # 部署容器\n使用下面的docker-compose.yml文件来部署镜像仓库服务：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 services: registry-ui: image: joxit/docker-registry-ui:main restart: always ports: - 8002:80 environment: - SINGLE_REGISTRY=true - REGISTRY_TITLE=Docker Registry UI - DELETE_IMAGES=true - SHOW_CONTENT_DIGEST=true - NGINX_PROXY_PASS_URL=http://registry:5000 - SHOW_CATALOG_NB_TAGS=true - CATALOG_MIN_BRANCHES=1 - CATALOG_MAX_BRANCHES=1 - TAGLIST_PAGE_SIZE=100 - REGISTRY_SECURED=false - CATALOG_ELEMENTS_LIMIT=1000 container_name: registry-ui networks: traefik-net: aliases: - registry-ui registry: image: registry:3 container_name: registry restart: always environment: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: \u0026#39;[http://registry-ui.jinli.io]\u0026#39; REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods: \u0026#39;[HEAD,GET,OPTIONS,DELETE]\u0026#39; REGISTRY_HTTP_HEADERS_Access-Control-Allow-Credentials: \u0026#39;[true]\u0026#39; REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers: \u0026#39;[Authorization,Accept,Cache-Control]\u0026#39; REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers: \u0026#39;[Docker-Content-Digest]\u0026#39; REGISTRY_STORAGE_DELETE_ENABLED: \u0026#39;true\u0026#39; volumes: - ${DATA_DIR}:/var/lib/registry networks: traefik-net: aliases: - registry networks: traefik-net: external: true 这里我们使用了两个镜像：\nregistry: Docker官方提供了容器镜像仓库的镜像 docker-registry-ui：Joxit开发的第三方镜像仓库图形界面 反向代理\n我们还是使用之前的Traefik加Cloudflare Tunnel的方案，我们需要为仓库服务和图形界面服务分别配置Traefik。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 http: routers: registry-router: rule: \u0026#34;Host(`registry.example.com`)\u0026#34; entryPoints: - web service: registry-service #tls: # certResolver: le services: registry-service: loadBalancer: servers: - url: \u0026#34;http://registry:5000\u0026#34; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 http: routers: registry-ui-router: rule: \u0026#34;Host(`registry-ui.example.com`)\u0026#34; entryPoints: - web service: registry-ui-service #tls: # certResolver: le services: registry-ui-service: loadBalancer: servers: - url: \u0026#34;http://registry-ui:80\u0026#34; 测试 # 部署完成后我们可以测试是否进入图形界面，例如上述设置将图形界面发布到了https://registry-ui.example.com。打开浏览器输入上述网址，如果之前设置都正确的话，可以看到下面的页面：\n我们还没往仓库里推送镜像，所以显示仓库是空的。\n使用 # 假设容器镜像仓库服务部署到了电脑A上，我们想在电脑B上拉取使用某个镜像。其步骤如下：\n电脑A：将镜像发布到仓库 # 在电脑Ａ上需要如下操作：\n在本地编译镜像，或者从dockerhub/github等平台拉取镜像。例如从dockerhub拉取registry镜像： 1 docker pull registry:3 给镜像打上新的标签： 1 docker tag registry:3 registry.example.com/registry:3 将镜像推送到自建仓库： 1 docker push registry.example.com/registry:3 完成上述操作后，可以在https://registry-ui.example.com上看到registry镜像已经成功上传：\n电脑B：从我们的仓库拉取镜像 # 在电脑Ｂ上使用Docker时，只需要在所需要的镜像名前加上我们的仓库名registry.example.com/就行了，例如：\n1 docker pull registry.example.com/registry:3 如果使用docker compose也一样，将docker-compose.yml里的镜像名做相应的替换即可。\n","date":"2026年2月11日","externalUrl":null,"permalink":"/p/%E5%AE%B9%E5%99%A87%E8%87%AA%E5%BB%BA%E5%AE%B9%E5%99%A8%E9%95%9C%E5%83%8F%E4%BB%93%E5%BA%93/","section":"Posts","summary":" 缘起 # 由于国内的网络环境问题，我们无法直接从dockerhub拉取容器镜像。为了能在国内的电脑上方便地部署一些容器服务，我决定在一台位于国外的服务器上自建一个容器镜像仓库（即Registry）。\n","title":"容器（7）：自建容器镜像仓库","type":"post"},{"content":"","date":"2026年2月9日","externalUrl":null,"permalink":"/tags/cms/","section":"Tags","summary":"","title":"CMS","type":"tags"},{"content":"","date":"9 二月 2026","externalUrl":null,"permalink":"/en/tags/content-management-system/","section":"Tags","summary":"","title":"Content Management System","type":"tags"},{"content":"","date":"2026年2月9日","externalUrl":null,"permalink":"/tags/decap-cms/","section":"Tags","summary":"","title":"DeCap CMS","type":"tags"},{"content":"","date":"2026年2月9日","externalUrl":null,"permalink":"/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"","date":"9 二月 2026","externalUrl":null,"permalink":"/en/tags/personal-website/","section":"Tags","summary":"","title":"Personal Website","type":"tags"},{"content":"","date":"9 二月 2026","externalUrl":null,"permalink":"/en/series/personal-website-building-series/","section":"Series","summary":"","title":"Personal Website Building Series","type":"series"},{"content":"","date":"9 二月 2026","externalUrl":null,"permalink":"/en/categories/website/","section":"Categories","summary":"","title":"Website","type":"categories"},{"content":"","date":"2026年2月9日","externalUrl":null,"permalink":"/tags/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/","section":"Tags","summary":"","title":"个人网站","type":"tags"},{"content":"","date":"2026年2月9日","externalUrl":null,"permalink":"/series/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"个人网站搭建系列","type":"series"},{"content":" 缘起 # 这个网站已经上线五年了，一直使用Hugo框架。Hugo框架非常好用，但经过几年的使用，我还是感受到了一些不便之处。\n具体来说，由于我这个网站用docker容器部署，博客的内容则用GitHub仓库管理，因此每次更新博客内容都需要在本地电脑上修改markdown文件，然后提交到GitHub仓库，最后再在服务器上拉取更新，重新构建容器并启动。\n这一系列操作虽然不算麻烦，但写博客时必须得在有这个博客Git仓库的电脑上进行。有时突然有了些灵感，想写点儿东西，但手机上或者电脑上没有这个博客的Git仓库，就只能先写在其他地方，之后再复制到博客的markdown文件中。这确实有点儿麻烦，但更让人不舒服的点是，有时想继续写之前没写完的博客，但如果没有这个博客的Git仓库，就看不到之前写的内容，没法接着写。\n所以我就在想，有没有什么办法，能给博客加个编辑系统，能够将博客的草稿保存在服务器上，这样无论在哪个电脑上，甚至在手机上，都可以打开草稿继续写，写完了之后再发布到博客上。\n根据ChatGPT的提示，这样的系统叫做内容管理系统（CMS），并且建议我可以使用DeCap CMS这个开源的CMS系统来搭建博客的编辑系统。\nDeCap CMS简介 # DeCap CMS是一个基于Git的内容管理系统，支持多种静态网站生成器（如Hugo、Jekyll、Gatsby等）。它提供了一个用户友好的界面，允许用户通过浏览器编辑和管理博客内容，并将更改直接提交到Git仓库中。\nDeCap CMS的安装也非常简单，只需要在静态网站的根目录下放一个admin文件夹，里面放入一个HTML文件和一个配置文件。比较麻烦的部分是要配置一个沟通DeCap CMS和GitHub仓库的后端服务。\nDeCap CMS的工作原理 # DeCap CMS的工作流程 # DeCap CMS的工作流程大致可分为4个步骤：\n网站管理员登录界面：在静态网站的根目录下放一个admin文件夹，就可以通过访问https://blog.example.com/admin/来进入DeCap CMS的登录界面。 用户认证：DeCap CMS支持多种认证方式，包括GitHub OAuth、GitLab OAuth、Bitbucket OAuth等。用户可以选择适合自己的认证方式来登录。 内容编辑：登录成功后，用户可以通过DeCap CMS的界面来编辑博客内容。DeCap CMS提供了一个所见即所得的编辑器，用户可以直接在浏览器中编辑博客内容，并且可以预览编辑的效果。 内容发布：当用户完成编辑后，可以点击发布按钮。利用我们给DeCapCMS提供的可以访问Git仓库的令牌，DeCap CMS会将更改提交到Git仓库中，并触发静态网站生成器（如Hugo）的构建过程，最终将更新后的博客内容发布到网站上。 DeCap CMS的构件 # 由此可见，我们需要准备好4个东西：\nDeCap CMS的前端文件：需要在博客的根目录下创建一个admin文件夹，并在其中放入DeCap CMS的前端文件──一个HTML文件和一个配置文件。 登录认证：需要选择一种认证方式，并配置相应的认证信息。 Git仓库访问令牌：需要生成一个可以访问博客Git仓库的令牌，并将其配置到DeCap CMS中。 将DeCap CMS的请求转发到后端的服务：需要配置一个后端服务来处理DeCap CMS的请求，并将其转发到Git仓库中。 以我的需求为例，我使用Hugo作为博客框架，博客的内容保存在GitHub仓库中，因此我需要配置DeCap CMS来支持Hugo。DeCap CMS支持GitHub OAuth认证，因此我可以选择使用GitHub OAuth来进行用户认证。最后，我需要配置一个后端服务来处理DeCap CMS的请求，并将其转发到GitHub仓库中。\nDeCap CMS的安装与配置 # 安装DeCap CMS的前端文件 # 对于Hugo框架，DeCap CMS要求把前端文件放在Hugo项目根目录的static文件夹下（对于其他框架，可能需要放在public、src、site等文件夹下，具体需要根据DeCap CMS的文档来确定）。因此我们在博客的Hugo项目根目录下创建一个static/admin文件夹，并在其中放入DeCap CMS的前端文件：\nindex.html：DeCap CMS的主界面文件，包含了DeCap CMS的前端逻辑和界面设计。\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;!doctype html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34; /\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1\u0026#34; /\u0026gt; \u0026lt;title\u0026gt;Jin Li Misc Admin\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;script src=\u0026#34;https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; config.yml：DeCap CMS的配置文件，用于配置DeCap CMS的认证方式、Git仓库访问令牌等信息。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 backend: name: github repo: jin-li/blog branch: main base_url: https://decap.example.com auth_endpoint: /auth media_folder: \u0026#34;hugosite/static/images\u0026#34; public_folder: \u0026#34;/images\u0026#34; site_url: https://blog.example.com display_url: https://blog.example.com publish_mode: editorial_workflow collections: - name: \u0026#34;blog\u0026#34; label: \u0026#34;Blog Posts\u0026#34; folder: \u0026#34;blog/content/posts\u0026#34; create: true slug: \u0026#34;{{year}}-{{month}}-{{day}}-{{slug}}\u0026#34; fields: - { label: \u0026#34;Title\u0026#34;, name: \u0026#34;title\u0026#34;, widget: \u0026#34;string\u0026#34; } - { label: \u0026#34;Date\u0026#34;, name: \u0026#34;date\u0026#34;, widget: \u0026#34;datetime\u0026#34; } - { label: \u0026#34;Draft\u0026#34;, name: \u0026#34;draft\u0026#34;, widget: \u0026#34;boolean\u0026#34;, default: true } - { label: \u0026#34;Tags\u0026#34;, name: \u0026#34;tags\u0026#34;, widget: \u0026#34;list\u0026#34;, required: false } - { label: \u0026#34;Body\u0026#34;, name: \u0026#34;body\u0026#34;, widget: \u0026#34;markdown\u0026#34; } 其中backend里配置了后端的服务用GitHub，本网站的GitHub仓库是jin-li/blog，分支是main，后端服务的地址是https://decap.example.com，认证接口是/auth。media_folder和public_folder分别配置了媒体文件的存储路径和访问路径。site_url和display_url配置了网站的URL。publish_mode配置了发布模式，这里使用了编辑工作流模式。最后，collections配置了内容集合，这里配置了一个名为“blog”的集合，用于管理博客文章。\n这样，当我们访问http://yourwebsite.com/admin/时，DeCap CMS会加载管理界面，并显示GitHub OAuth的登录选项。\n配置GitHub OAuth认证 # 要使用GitHub OAuth认证，我们需要在GitHub上创建一个OAuth应用，并获取相应的客户端ID和客户端密钥。具体步骤如下：\n登录GitHub，点击右上角的头像，选择“Settings”。 在左侧菜单中选择“Developer settings”。 在左侧菜单中选择“OAuth Apps”，然后点击“New OAuth App”。 在“Register a new OAuth application”页面中，填写应用的名称、主页和回调URL。其中主页URL应该是你的博客地址，例如这里是https://blog.example.com，回调URL应该是DeCap CMS后端服务的认证接口地址，例如这里是https://decap.example.com/callback。 点击“Register application”按钮，完成应用的注册。注册完成后，你会看到应用的客户端ID（Client ID）和客户端密钥（Client Secret）。记录下这两个值，后续配置DeCap CMS时需要用到。 配置DeCap CMS的代理 # 在我们访问https://blog.example.com/admin/，并点击GitHub OAuth登录按钮后，DeCap CMS会向https://decap.example.com/auth发送一个认证请求。这个请求需要被转发到GitHub的OAuth认证接口，以完成认证流程。因此，我们需要配置一个代理来处理这个请求。\n网上有网友开发的Docker容器来处理DeCap CMS的请求转发，例如：\ndecap-cms-github-backend docker-decap-cms-standalone 我试着部署了这两个容器，像其他容器一样用Traefik和Cloudflare Tunnel来反向代理，但都没能成功。\n最后我发现还有网友使用Cloudflare Workers来实现这个代理功能，例如这个GitHub仓库decap-proxy所示。Cloudflare Workers是Cloudflare提供的一种无服务器计算平台，可以让我们在Cloudflare的边缘网络上运行JavaScript代码，从而实现请求的处理和转发。\n具体步骤可参考GitHub仓库里的说明，大致步骤如下：\n克隆decap-proxy仓库到本地：\n1 git clone https://github.com/sterlingwes/decap-proxy.git 进入decap-proxy目录，可见一个wrangler.toml.sample文件，这是Cloudflare Workers的配置文件。我们需要将其复制一份，并命名为wrangler.toml：\n1 cp wrangler.toml.sample wrangler.toml 编辑wrangler.toml文件，配置Cloudflare Workers的相关信息，例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #:schema node_modules/wrangler/config-schema.json # \u0026#34;compatibility_date\u0026#34; and \u0026#34;main\u0026#34; are values you are unlikely to need to change compatibility_date = \u0026#34;2025-11-17\u0026#34; # schema version main = \u0026#34;src/index.ts\u0026#34; # entry point for the worker # The \u0026#34;name\u0026#34; parameter defines the name of the worker in your Cloudflare # Dashboard. It also specifies the first element of the default URL that # will reach your worker. name = \u0026#34;decap-proxy\u0026#34; # optional: uncomment and alter the following lines if using a custom domain. # route = { pattern = \u0026#34;decap.example.com\u0026#34;, zone_name = \u0026#34;example.com\u0026#34;, custom_domain = true } # optional: uncomment the following line if you don\u0026#39;t want wrangler to set up # the worker to be available at the default workers.dev url of # \u0026lt;worker-name\u0026gt;.\u0026lt;account-name\u0026gt;.workers.dev # where \u0026lt;worker-name\u0026gt; is the \u0026#34;name\u0026#34; parameter configured above. # workers_dev = false # optional: this worker template uses Web Crypto API natively and doesn\u0026#39;t require # nodejs_compat you can add this flag if you need Node.js polyfills, # but please be aware that those polyfills may have vulnerabilities # compatibility_flags = [\u0026#34;nodejs_compat\u0026#34;] # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) # Docs: # - https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables # Note: Use secrets to store sensitive data. # - https://developers.cloudflare.com/workers/configuration/secrets/ [vars] GITHUB_REPO_PRIVATE = \u0026#34;1\u0026#34; # Should be set to \u0026#34;1\u0026#34;, if your website repo is private 里面的注释已经解释得很清楚了，我们主要需要修改route，配置成我们的DeCap CMS后端服务的地址，例如这里是decap.example.com。如果不想让Cloudflare自动帮我们设置默认的workers.dev域名，可以将workers_dev设置为false。最后，我们需要在[vars]部分配置一个变量GITHUB_REPO_PRIVATE，如果我们的博客GitHub仓库是私有的，就将其设置为1，否则可以不设置。\n配置好wrangler.toml文件后，我们就可以使用Cloudflare Workers CLI工具wrangler来部署我们的Worker了。GitHub仓库里说可以将Cloudflare的账号和令牌设置为环境变量就可以直接在命令行登录，但我试了并没有登录成功。于是我用unset命令将环境变量清除掉了，之后再运行npx wrangler login命令登录。\n如果你之前没有安装过wrangler，首次运行npx wrangler login命令时，系统会提示你安装wrangler，你可以按照提示进行安装。安装完成后，再次运行npx wrangler login命令，会在命令行中显示一个URL，提示你打开这个URL来完成登录。\n注意：你需要在与运行npx wrangler login命令相同的电脑中打开这个URL，因为在用你的Cloudflare账号完成验证后，wrangler跳转的URL是一个本地地址，例如http://localhost:8787/callback?code=xxx\u0026amp;state=yyy，这个URL会被wrangler监听到，并从中获取认证信息来完成登录。如果你在其他电脑上打开这个URL，虽然也能完成验证，但wrangler无法监听到这个URL，因此登录会失败。\n登录成功后会看到如下所示的浏览器界面：\n完成验证后，wrangler会提示你登录成功。之后，我们把之前获得的GitHub OAuth应用的客户端ID和客户端密钥设置为Cloudflare Worker的环境变量：\n1 2 npx wrangler secret put GITHUB_CLIENT_ID npx wrangler secret put GITHUB_CLIENT_SECRET 运行上述命令后，系统会提示你输入相应的值。输入完成后，wrangler会将这些值作为秘密环境变量存储在Cloudflare中。\n最后，我们就可以部署我们的Worker了：\n1 npx wrangler publish 部署完成后，我们可以打开浏览器访问https://decap.example.com，如果看到页面显示Hello 👋，说明我们的Cloudflare Worker已经成功部署并运行。\n测试访问DeCap CMS # 完成上述安装配置后，我们的DeCap CMS应该已经可以正常工作了。我们可以通过访问https://blog.example.com/admin/来测试访问DeCap CMS的登录界面：\n如果能够看到GitHub OAuth的登录选项，并且能够成功登录，会看到DeCap CMS的内容编辑界面：\n在编辑界面中，我们可以创建新的博客文章，编辑现有的博客文章，并且可以预览编辑的效果。当我们完成编辑后，点击发布按钮，DeCap CMS会将更改提交到GitHub仓库中，并触发Hugo的构建过程，最终将更新后的博客内容发布到网站上。\n","date":"2026年2月9日","externalUrl":null,"permalink":"/p/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99%E7%9A%84%E5%BB%BA%E7%AB%8B%E8%BF%87%E7%A8%8B%E4%BA%94%E5%86%85%E5%AE%B9%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9Fcms%E7%9A%84%E9%80%89%E6%8B%A9%E4%B8%8E%E9%85%8D%E7%BD%AEhugo%E6%A1%86%E6%9E%B6/","section":"Posts","summary":" 缘起 # 这个网站已经上线五年了，一直使用Hugo框架。Hugo框架非常好用，但经过几年的使用，我还是感受到了一些不便之处。\n","title":"个人网站的建立过程（五）：内容管理系统（CMS）的选择与配置（Hugo框架）","type":"post"},{"content":"","date":"2026年2月9日","externalUrl":null,"permalink":"/tags/%E5%86%85%E5%AE%B9%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F/","section":"Tags","summary":"","title":"内容管理系统","type":"tags"},{"content":"","date":"2026年2月9日","externalUrl":null,"permalink":"/categories/%E7%BD%91%E7%AB%99/","section":"Categories","summary":"","title":"网站","type":"categories"},{"content":"","date":"2025年11月14日","externalUrl":null,"permalink":"/tags/conky/","section":"Tags","summary":"","title":"Conky","type":"tags"},{"content":"","date":"2025年11月14日","externalUrl":null,"permalink":"/tags/linux/","section":"Tags","summary":"","title":"Linux","type":"tags"},{"content":" 缘起 # 在使用Linux系统时，很多时候需要实时监测系统的状态，例如在打游戏时监测CPU和GPU的使用率、温度等信息，在训练机器学习模型时监测显存使用情况，在下载大文件时监测网络速度等。虽然已经有System Monitor等图形化工具，以及htop、nvidia-smi等命令行工具可以用来监测系统状态，但这些工具大多需要手动打开，并不能实时显示在桌面上，有时不太方便。\nConky是一款轻量级的系统监控软件，可以将各种系统状态信息实时显示在桌面上，方便用户随时查看系统状态。我已经使用了很久了，感觉比较好用。但之前使用的是一个魔改的主题，能够满足我的需求，但可移植性比较糟糕。最近换了电脑，决定重新制作一个优雅、好用、可移植性强的Conky主题，并在这里分享一下Conky的安装和使用方法。\n安装Conky # Conky版本 # 主要的Linux发行版的软件源中基本都提供Conky的软件包，但版本普遍较旧。很多Conky包不含Lua支持、不支持NVIDIA GPU监测，或者对Wayland支持不好。如果你使用apt（如Ubuntu、Debian等）、dnf（如Fedora等）、pacman（如Arch Linux等）等包管理器安装Conky，建议一定要检查安装的Conky包是否支持你需要的特性。\n除了发行版的软件源外，Conky的GitHub仓库中提供了AppImage格式的Conky二进制包，版本较新，且包含了Lua支持和NVIDIA GPU监测支持，推荐使用这个版本。可以在Releases页面下载。\n安装步骤 # 使用包管理器安装 # 可以参看GitHub仓库中的Installation Guide，下面是一些常见发行版的安装方法。\nDebian/Ubuntu # 在Debian和Ubuntu中，可以通过以下命令来搜索可用的Conky包：\n1 apt search conky 通常会看到多个Conky相关的软件包，例如conky-all、conky-lua等。可以查看conky-all的描述信息，确认其版本号、支持的特性等信息：\n1 apt show conky-all 最后，可以通过以下命令来安装Conky：\n1 sudo apt install conky-all Fedora # 在Fedora中可以使用以下命令搜索可用的Conky包：\n1 dnf search conky 然后通过下面的命令查看conky包的描述信息：\n1 dnf info conky 最后，可以通过以下命令来安装Conky：\n1 sudo dnf install conky Arch Linux # 如果你使用Arch Linux，可以通过以下命令来搜索可用的Conky包：\n1 pacman -Ss conky 然后通过下面的命令查看conky包的描述信息：\n1 pacman -Si conky 最后，可以通过以下命令来安装Conky：\n1 sudo pacman -S conky 使用AppImage安装 # 使用AppImage安装可以参看GitHub仓库中的安装说明，下面是简要的安装步骤：\n从GitHub的Releases页面下载最新版本的Conky AppImage包，例如conky-*.AppImage。\n给下载的AppImage包添加可执行权限：\n1 chmod +x conky-*.AppImage 运行Conky AppImage包：\n1 ./conky-*.AppImage （可选）将Conky AppImage包移动到系统的某个目录下，例如/usr/local/bin/，以便在任何地方都能通过命令行运行Conky：\n1 sudo mv conky-*.AppImage /usr/local/bin/conky （可选）将Conky AppImage包加入开始菜单，方便以后启动。可以创建一个.desktop文件，内容如下：\n1 2 3 4 5 6 [Desktop Entry] Name=Conky Exec=/path/to/conky-*.AppImage Icon=conky Type=Application Categories=Utility;System; 将上述内容保存为~/.local/share/applications/conky.desktop，并将/path/to/conky-*.AppImage替换为实际的AppImage包路径。\n运行Conky，可以看到类似下图所示的系统状态信息显示在桌面上：\n配置Conky # 配置文件 # Conky的配置文件通常位于~/.config/conky/conky.conf，如果没有这个文件，可以运行下述命令来生成一个默认的配置文件：\n1 conky -C \u0026gt; ~/.config/conky/conky.conf 这里生成的默认配置文件比较简单，可以根据需要进行修改和扩展。Conky的配置文件使用类似于INI格式的语法，可以设置各种选项和显示内容。具体的配置选项可以参看Conky官方文档。\n我推荐使用Lua脚本来编写Conky的配置文件，这样可以更灵活地控制显示内容和样式。下面是一个简单的Lua配置文件示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 conky.config = { alignment = \u0026#39;top_right\u0026#39;, background = false, border_width = 1, cpu_avg_samples = 2, default_color = \u0026#39;white\u0026#39;, double_buffer = true, draw_borders = false, draw_graph_borders = true, gap_x = 10, gap_y = 10, minimum_width = 200, net_avg_samples = 2, no_buffers = true, out_to_console = false, out_to_stderr = false, own_window = true, own_window_class = \u0026#39;Conky\u0026#39;, own_window_type = \u0026#39;desktop\u0026#39;, update_interval = 1.0, use_xft = true, xftalpha = 0.8, }; conky.text = [[ ${font Arial:bold:size=12}System Monitor${font} Uptime: ${uptime} CPU: ${cpu cpu0}% ${cpubar cpu0} RAM: ${mem} / ${memmax} ${membar} Disk: ${fs_used /} / ${fs_size /} ${fs_bar /} GPU: ${nvidia gpu0}°C ${nvidia gpu0 usage}% Net: ${downspeed eth0} ↓↑ ${upspeed eth0} ]]; 使用Conky主题 # 自己从零开始配置Conky可能比较麻烦，推荐使用现成的Conky主题。可以在网上找到很多Conky主题，例如：\nConky的GitHub仓库中包含了一些用户分享的主题 Conky Themes网站上有很多用户分享的Conky主题。 DeviantArt上也有很多Conky主题。 这里推荐一个我自己开发的Conky主题，名为jinli-conky。效果如下图所示：\n这个主题支持CPU、内存、磁盘、网络、NVIDIA GPU等多种系统状态的监测，并且样式简洁优雅，纯Lua编写，易于修改和扩展。\njinli-conky主题 # 安装jinli-conky主题 # 安装方法可参看jinli-conky的GitHub仓库主页的说明。主要步骤如下：\n克隆jinli-conky仓库：\n1 git clone https://github.com/jin-li/jinli-conky.git 最好将其克隆到~/.config/conky/目录下。\n安装字体（可选）\njinli-conky主题使用了一些自定义字体来显示一些图标，如果你想要显示这些图标的话，需要先安装这些字体。不安装字体也行，这些图标会显示为文字。\n进入jinli-conky/fonts/目录，就可以看到名为subset-SymbolNF.ttf的字体文件，直接双击安装即可。也可以使用命令行安装：\n1 2 3 cd jinli-conky/fonts/ cp subset-SymbolsNF.ttf ~/.local/share/fonts/ fc-cache -fv 进入jinli-conky目录，运行start.sh脚本来安装主题：\n1 2 cd jinli-conky ./start.sh 运行脚本后，Conky主题就会自动启动并显示在桌面上。\n开机自启动（可选）\n运行上一步的启动脚本会生成自动启动项的文件conky-start.desktop，如果你需要让Conky在每次登录时自动启动，可以将其复制到~/.config/autostart/目录下。先检查~/.config/autostart/目录是否存在，如果不存在可以创建：\n1 mkdir -p ~/.config/autostart/ 然后复制自动启动项文件：\n1 cp conky-start.desktop ~/.config/autostart/ 配置jinli-conky主题 # 在运行start.sh脚本后，会在主题目录下生成一个名为jinli-config.lua的配置文件，这个配置文件是主题默认的jinli-config.example.lua文件的副本。你可以编辑这个配置文件来修改主题的各种选项，例如显示内容、样式等。\n其中比较常用的配置选项包括：\n缩放比例：主题默认的屏幕分辨率是1920x1080，如果你的屏幕分辨率不同，可以修改scaling选项来调整主题的大小。例如，如果你的屏幕分辨率是3840x2160，可以将scaling选项修改为scaling = 2.0（3840/1920）。 整体参数：可以修改整体显示的宽度、高度、位置等参数，也可以修改刷新间隔（默认为1秒）、修改透明度、更换字体、修改距离屏幕边缘的距离等参数。 单个组件：主题包含了时钟、系统信息、CPU、内存、磁盘、网络、GPU这几个组件，可以单独启用或禁用某个组件，或者修改某个组件的显示样式和参数，例如将仪表盘显示在右侧、更改组件的显示顺序等。 修改完配置文件后，保存文件即可立即生效，无需重启Conky。\n","date":"2025年11月14日","externalUrl":null,"permalink":"/p/linux%E7%8A%B6%E6%80%81%E7%9B%91%E6%8E%A7%E8%BD%AF%E4%BB%B6conky%E7%9A%84%E4%BD%BF%E7%94%A8/","section":"Posts","summary":" 缘起 # 在使用Linux系统时，很多时候需要实时监测系统的状态，例如在打游戏时监测CPU和GPU的使用率、温度等信息，在训练机器学习模型时监测显存使用情况，在下载大文件时监测网络速度等。虽然已经有System Monitor等图形化工具，以及htop、nvidia-smi等命令行工具可以用来监测系统状态，但这些工具大多需要手动打开，并不能实时显示在桌面上，有时不太方便。\n","title":"Linux状态监控软件Conky的使用","type":"post"},{"content":"","date":"14 十一月 2025","externalUrl":null,"permalink":"/en/tags/system-monitoring/","section":"Tags","summary":"","title":"System Monitoring","type":"tags"},{"content":"","date":"2025年11月14日","externalUrl":null,"permalink":"/tags/%E7%8A%B6%E6%80%81%E7%9B%91%E6%8E%A7/","section":"Tags","summary":"","title":"状态监控","type":"tags"},{"content":"","date":"7 十月 2025","externalUrl":null,"permalink":"/en/tags/clipboard/","section":"Tags","summary":"","title":"Clipboard","type":"tags"},{"content":"","date":"2025年10月7日","externalUrl":null,"permalink":"/tags/clipcascade/","section":"Tags","summary":"","title":"ClipCascade","type":"tags"},{"content":"","date":"7 十月 2025","externalUrl":null,"permalink":"/en/tags/sharing/","section":"Tags","summary":"","title":"Sharing","type":"tags"},{"content":"","date":"2025年10月7日","externalUrl":null,"permalink":"/tags/%E5%85%B1%E4%BA%AB/","section":"Tags","summary":"","title":"共享","type":"tags"},{"content":"","date":"2025年10月7日","externalUrl":null,"permalink":"/tags/%E5%89%AA%E5%88%87%E6%9D%BF/","section":"Tags","summary":"","title":"剪切板","type":"tags"},{"content":" 缘起 # 我在之前的文章中介绍过多电脑键鼠共享软件Barrier/InputLeap/Deskflow，可以在Windows、macOS和Linux系统间共享键盘和鼠标，极大地方便了多电脑办公的体验。这些软件也支持跨系统共享剪切板内容，但我在使用中发现，共享剪切板的功能在Wayland环境下并不可用。不幸的是，我的主力电脑使用的Fedora 42系统，默认使用的图形界面正是Wayland，因此我无法在这台电脑上使用Barrier/InputLeap/Deskflow的剪切板共享功能。\n后来我在Deskflow的讨论区看到有人提到另一款跨系统剪切板共享软件ClipCascade，说可以使用这个第三方软件来实现跨系统的剪切板共享功能，它支持Wayland环境。我尝试安装使用了一下，发现确实可以满足我的需求，因此在这里分享一下ClipCascade的安装和使用方法。\n关于之前介绍的Barrier/InputLeap/Deskflow的安装和使用，可以参考我的这篇文章：全平台免费键盘鼠标共享软件Barrier/InputLeap/Deskflow的安装与使用。\n简介 # ClipCascade是一款免费开源的跨平台的剪切板共享软件，支持在Windows、macOS、Linux和Android系统间共享剪切板内容。它支持Wayland环境，因此可以在比较新的Linux系统上使用。而且它支持自建搭建服务器，因此数据传输更安全。\n安装 # 共享剪切板服务器 # 在安装ClipCascade之前，我们需要先搭建一个共享剪切板的服务器。\n其实ClipCascade官方提供了一个公共服务器，任何人都可以使用，但数据会经过第三方，如果你对隐私有较高的要求，建议自行搭建服务器。当然使用公共服务器你也可以选择加密传输数据，因此安全性也还不错。但我已经习惯自建各种服务了，所以这里就决定自建一个ClipCascade服务器了。\n用Docker建ClipCascade服务器 # ClipCascade官方提供了Docker镜像，可以直接用Docker来搭建服务器。我习惯了使用Docker Compose来管理Docker容器，因此这里也用Docker Compose来搭建ClipCascade服务器。所需的docker-compose.yml文件在ClipCascade的GitHub仓库里也有提供，可参见这里。下面是我使用的docker-compose.yml文件内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 services: clipcascade: image: sathvikrao/clipcascade:latest ports: - \u0026#34;8084:8080\u0026#34; # Expose the ClipCascade server on port 8080 restart: always # Automatically restart the container if it stops volumes: - ${DATA_DIR}/cc_users:/database # Persistent storage for user data environment: - TZ=${TZ} # Set the timezone from the .env file - CC_MAX_MESSAGE_SIZE_IN_MiB=10 # Maximum message size in MiB (ignored if P2P mode is enabled) - CC_P2P_ENABLED=false # Enables or disables peer-to-peer(P2P) mode # - CC_ALLOWED_ORIGINS=https://clipcascade.example.com # Defines allowed CORS origins for security # - CC_SIGNUP_ENABLED=false # Enables or disables user self-registration networks: - traefik-network networks: traefik-network: external: true 这里的DATA_DIR和TZ是从.env文件中读取的环境变量，DATA_DIR指定了ClipCascade服务器的数据存储目录，TZ指定了时区。你可以根据自己的需要修改这些配置。\n另外，我还指定了traefik-network网络，因为我使用Traefik作为反向代理服务器，所有的服务都连接到这个网络。如果你没有使用Traefik，可以去掉这部分配置。\n用Traefik反向代理ClipCascade服务器 # 关于Docker服务的反向代理，我在之前的文章中也介绍过，可以参考：\n\u0026ldquo;从公网访问个人网站——Nginx反向代理配置\u0026rdquo; 从公网访问个人网站（二）——Traefik反向代理配置 这两篇文章分别介绍了如何使用Nginx和Traefik来反向代理Docker服务。这里我继续使用Traefik来反向代理ClipCascade服务器。\n我使用动态配置文件来配置Traefik的反向代理规则，创建一个名为clipcascade.yml的动态配置文件，内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 http: routers: clipcascade-router: entryPoints: - web rule: \u0026#34;Host(`clipcascade.example.com`)\u0026#34; service: clipcascade-service services: clipcascade-service: loadBalancer: servers: - url: \u0026#34;http://clipcascade:8080\u0026#34; 启动服务与创建用户 # 启动Docker容器\n进入存放docker-compose.yml文件的目录，运行以下命令启动ClipCascade服务器：\n1 docker-compose up -d 反向代理\n如果使用Traefik动态配置文件，配置文件创建好之后Traefik会自动加载配置，无需重启Traefik。\n如果使用Nginx反向代理，需要重启Nginx服务：\n1 sudo systemctl restart nginx 登录与创建用户\n打开浏览器，访问http://clipcascade.example.com（将clipcascade.example.com替换为你的域名），你会看到ClipCascade的登录页面。默认情况下，管理员账号是admin，密码是admin123。登录后会看到下面的页面：\n建议你登录后立即点击上方的“修改密码”链接来修改管理员密码。\n然后可以创建一个普通用户账号，用于日常使用。点击Admin Panel里的Add New User按钮，输入用户名和密码，然后点击Add User按钮即可创建新的用户。\n客户端安装与使用 # ClipCascade支持多种操作系统，包括Windows、macOS、Linux和安卓。你可以根据GitHub仓库里的文档来下载并安装对应系统的客户端。\n需要注意的是，Windows、macOS和Android系统上都有打包好的安装包，但是Linux系统上只有未打包的Python源码，需要自己解决依赖并运行。这里我以Fedora 42为例，介绍一下Linux系统上ClipCascade客户端的安装和使用。\nFedora 42上安装ClipCascade客户端 # Python环境\nClipCascade客户端是用Python编写的，因此需要先安装Python环境。Fedora 42默认安装的是Python 3.12，但ClipCascade客户端依赖的一些Python库在Python 3.12中已经不再支持，因此需要安装Python 3.11。我使用conda来管理Python环境，所以我用conda创建了一个Python 3.11的虚拟环境。至于使用conda管理的优点可以参考我的这篇文章：Python环境管理方式总结。\n1 2 conda create -n clipcascade python=3.11 conda activate clipcascade 然后安装Python依赖：\n1 pip3 install -r requirements.txt 安装其他依赖\n除了Python依赖外，还需要安装一些系统依赖。这些按照照ClipCascade的GitHub仓库里的说明来安装即可：\n1 2 3 4 5 sudo dnf check-update sudo dnf install -y python3 python3-pip python3-gobject xclip wl-clipboard dunst sudo dnf install -y libappindicator-gtk3 sudo dnf install -y python3-xxhash python3-pyperclip python3-requests python3-websocket-client python3-pycryptodomex python3-tkinter python3-pystray python3-pyfiglet python3-beautifulsoup4 sudo dnf install -y ffmpeg ffmpeg-devel 需要注意的是，ClipCascade的安装说明里提到还需要用DNF安装python3-plyer和python3-aiortc，但这两个包在Fedora 42的官方仓库里已经没有了。不过也不用担心，因为这两个包已经在requirements.txt里列出，前面用pip安装Python依赖时已经安装了。\n上述两个问题是ClipCascade的GitHub仓库里的安装说明里没有指出的，我已经给作者提了一个issue，希望作者能更新一下安装说明。\n运行客户端 # 进入ClipCascade客户端的源码目录，运行以下命令启动客户端：\n1 python3 main.py 启动后会弹出一个窗口，提示输入服务器地址、用户名和密码：\n登录成功会弹出一个通知，然后就可以跟登录了同一个ClipCascade账号的其他设备共享剪切板内容了。\n(Bonus)添加到开始菜单 # 截止到上一步，我们已经可以在Fedora 42上使用ClipCascade客户端了，但每次都要打开终端进入源码目录运行python3 main.py命令，还是比较麻烦的。我们可以把ClipCascade添加到开始菜单，这样就可以像其他应用一样通过开始菜单启动了。\nLinux桌面环境使用.desktop文件来管理开始菜单里的应用程序。我使用Fedora 42的KDE桌面环境，开始菜单的.desktop文件存放在~/.local/share/applications/目录下。我们可以在这个目录下创建一个名为clipcascade.desktop的文件，内容如下：\n1 2 3 4 5 6 7 8 9 [Desktop Entry] Version=1.0 Type=Application Name=ClipCascade Comment=Cross-platform clipboard sharing tool Exec=/home/yourusername/miniconda3/envs/clipcascade/bin/python3 /path/to/ClipCascade/main.py Icon=/path/to/ClipCascade/icon.png Terminal=false Categories=Utility; 需要把Exec和Icon字段的路径替换为你自己的路径。Exec字段指定了启动ClipCascade客户端的命令，这里需要指定Python解释器的路径和main.py文件的路径。Icon字段指定了ClipCascade的图标路径，可以下载ClipCascade的图标文件放在合适的位置。\n需要注意的是，Exec字段中的Python解释器路径需要指定为你安装ClipCascade客户端时使用的Python环境中的Python解释器路径。如果你使用的是conda创建的虚拟环境，可以在终端中运行which python3命令来查看Python解释器的路径。\nmacOS上安装ClipCascade客户端 # macOS上ClipCascade客户端的安装比较简单，直接下载打包好的安装包，然后按照ClipCascade的GitHub仓库里的说明来安装即可。我跟着说明一步步操作，并没有遇到额外的问题。\n","date":"2025年10月7日","externalUrl":null,"permalink":"/p/%E5%85%A8%E5%B9%B3%E5%8F%B0%E5%85%8D%E8%B4%B9%E5%89%AA%E5%88%87%E6%9D%BF%E5%85%B1%E4%BA%AB%E8%BD%AF%E4%BB%B6clipcascade%E7%9A%84%E5%AE%89%E8%A3%85%E4%B8%8E%E4%BD%BF%E7%94%A8/","section":"Posts","summary":" 缘起 # 我在之前的文章中介绍过多电脑键鼠共享软件Barrier/InputLeap/Deskflow，可以在Windows、macOS和Linux系统间共享键盘和鼠标，极大地方便了多电脑办公的体验。这些软件也支持跨系统共享剪切板内容，但我在使用中发现，共享剪切板的功能在Wayland环境下并不可用。不幸的是，我的主力电脑使用的Fedora 42系统，默认使用的图形界面正是Wayland，因此我无法在这台电脑上使用Barrier/InputLeap/Deskflow的剪切板共享功能。\n","title":"全平台免费剪切板共享软件ClipCascade的安装与使用","type":"post"},{"content":"","date":"2025年8月20日","externalUrl":null,"permalink":"/tags/cuda/","section":"Tags","summary":"","title":"CUDA","type":"tags"},{"content":"","date":"2025年8月20日","externalUrl":null,"permalink":"/tags/deep-learning/","section":"Tags","summary":"","title":"Deep Learning","type":"tags"},{"content":"","date":"2025年8月20日","externalUrl":null,"permalink":"/tags/gpu/","section":"Tags","summary":"","title":"GPU","type":"tags"},{"content":"","date":"2025年8月20日","externalUrl":null,"permalink":"/tags/nvidia/","section":"Tags","summary":"","title":"Nvidia","type":"tags"},{"content":"","date":"2025年8月20日","externalUrl":null,"permalink":"/series/nvidia-gpu-series/","section":"Series","summary":"","title":"Nvidia GPU Series","type":"series"},{"content":" 缘起 # 这是“Nvidia显卡”系列文章的第四篇，本系列文章主要记录如何使用Nvidia显卡，尤其是在Linux系统上使用Nvidia显卡。在之前的文章中，我记录了在Ubuntu系统上使用Nvidia显卡的相关设置，包括游戏、CUDA编程、深度学习、Docker容器等。后来我转向了Fedora系统，本以为在Fedora系统上配置Nvidia显卡的CUDA编程和深度学习环境会和在Ubuntu系统上类似，而且由于有RPM Fusion的支持，安装和配置会更加方便。然而实际上却遇到了意想不到的困难，因此不得不再开一篇文章来记录这些问题和解决方案。\n简单来讲，上一篇文章中遇到的问题是，Fedora 42系统使用的GCC版本与Nvidia的CUDA版本不兼容，导致无法使用CUDA编译器，详见上一篇文章Nvidia显卡（三）：Fedora下的游戏、CUDA、深度学习、Docker等的最后。为了解决这个问题，我决定在Fedora上使用Docker容器来配置Nvidia显卡的CUDA编程和深度学习环境。\n本系列的其他文章参见：\nNvidia显卡（一）：Ubuntu下的游戏、CUDA、深度学习、Docker等 Nvidia显卡（二）：视频剪辑转码工具FFmpeg使用GPU加速 Nvidia显卡（三）：Fedora下的游戏、CUDA、深度学习、Docker等 容器方案 # 为了绕过Fedora 42系统与CUDA编译器不兼容的问题，我决定使用容器技术用Nvidia官方提供的镜像来创建一个基于Ubuntu的环境。\n但我在创建的过程中仍然走了一些弯路，主要是因为我之前一直想使用Podman来替代Docker，这次就尝试了使用Podman来创建CUDA编程环境。然而由于权限问题、Nvidia的支持问题等等，遇到了一些未能解决的问题。最终我还是放弃了使用Podman，继续使用Docker。之后有机会我会写一篇文章来介绍Podman，未来我也会慢慢将容器服务转移到Podman，在这里就不再赘述了。这里主要记录一下用Docker来搭建CUDA编程和深度学习环境的过程。\n基本框架 # 我们已经在Fedora系统上安装了Nvidia显卡和驱动，为了让Docker容器内的应用能够使用Nvidia显卡，我们还需要安装Nvidia的Docker支持。因此整个环境的基本框架如下：\n最底层是支持CUDA的Nvidia显卡。 其次是安装了Nvidia驱动的宿主机（host machine）。 然后是让容器应用调用宿主机显卡的Nvidia容器工具箱（Nvidia Container Toolkit）。 最上层是容器，里面有CUDA工具箱，用以对GPU进行编程和调用。 CUDA编程和深度学习环境容器的搭建 # 宿主机的准备 # 在开始创建和部署容器前，我们要先保证宿主机上已经安装了Nvidia驱动、Docker和Nvidia容器工具箱（Nvidia Container Toolkit）。\nNvidia驱动：请参考上一篇文章Nvidia显卡（三）：Fedora下的游戏、CUDA、深度学习、Docker等中关于在Fedora系统上通过RPM Fusion安装Nvidia驱动的部分。 可以通过以下命令来确认Nvidia驱动是否安装成功：\n1 nvidia-smi 如果运行上面的命令可以正确输出类似下面所示的GPU当前的状态，就说明驱动已安装成功：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 575.64.05 Driver Version: 575.64.05 CUDA Version: 12.9 | |-----------------------------------------+------------------------+----------------------+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 NVIDIA GeForce RTX 5070 Ti Off | 00000000:02:00.0 On | N/A | | 0% 42C P8 18W / 300W | 2837MiB / 16303MiB | 1% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ +-----------------------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=========================================================================================| | 0 N/A N/A 2540 G /usr/bin/ksecretd 3MiB | 然后记录下与当前驱动相对应的CUDA版本号，例如上面显示支持CUDA 12.9。\nDocker：请参考Docker官方文档，主要步骤如下：\n1 2 3 4 sudo dnf -y install dnf-plugins-core sudo dnf-3 config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo systemctl enable --now docker 可以运行下面的命令来验证是否安装成功：\n1 sudo docker run hello-world 如果不想使用sudo来运行Docker，需要将当前用户添加到docker组中：\n1 2 sudo usermod -aG docker $USER newgrp docker Nvidia容器工具箱（Nvidia Container Toolkit）：请参考Nvidia官方文档进行安装，主要步骤如下：\n1 2 3 4 5 6 7 8 curl -s -L https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo | \\ sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo export NVIDIA_CONTAINER_TOOLKIT_VERSION=1.17.8-1 sudo dnf install -y \\ nvidia-container-toolkit-${NVIDIA_CONTAINER_TOOLKIT_VERSION} \\ nvidia-container-toolkit-base-${NVIDIA_CONTAINER_TOOLKIT_VERSION} \\ libnvidia-container-tools-${NVIDIA_CONTAINER_TOOLKIT_VERSION} \\ libnvidia-container1-${NVIDIA_CONTAINER_TOOLKIT_VERSION} 安装完成后，可以运行以下命令来验证Nvidia容器工具箱是否安装成功（注意这里的CUDA版本号要和你自己的版本号一致）：\n1 docker run --rm --gpus all nvidia/cuda:12.9.1-base-ubuntu24.04 nvidia-smi 如果能够像在宿主机里运行nvidia-smi命令一样正确输出Nvidia显卡的状态信息，就说明Nvidia容器工具箱安装成功。\n创建和部署容器 # 选择镜像\nNvidia官方提供了多种CUDA编程和深度学习的容器镜像，我们可以在他们的Docker镜像站点根据需要选择对应的容器来创建和部署。里面的容器种类很多，大致可以分为以下两类：\n通用镜像：适合定义一些比较通用的CUDA编程和深度学习环境。里面又有不同的种类：\n基础镜像：如nvidia/cuda:12.9.1-base-ubuntu24.04，这是一个基础的CUDA镜像，包含Ubuntu 24.04和CUDA 12.9.1的基础环境，适合需要自定义开发环境的用户。 深度学习框架镜像：如nvidia/cuda:12.9.1-devel-ubuntu24.04，这是一个包含了CUDA开发环境的镜像，用户不需要再安装CUDA开发工具包，适合需要进行CUDA编程的用户。 运行时镜像：如nvidia/cuda:12.9.1-runtime-ubuntu24.04，这是一个只包含运行时环境的镜像，适合只需要运行CUDA应用的用户。 专用镜像：适合一些特定的深度学习框架和应用。里面也有不同的种类：\nTensorFlow镜像：如nvidia/13.0.0-tensorrt-devel-ubuntu24.04，这是一个包含了TensorFlow和CUDA的镜像，适合需要使用TensorFlow进行深度学习的用户。 cudnn镜像：如nvidia/13.0.0-cudnn-devel-ubuntu24.04，这是一个包含了cuDNN和CUDA的镜像，适合需要使用cuDNN进行深度学习的用户。 对我而言，我需要更加通用一些的场景，而且各种工具和库可能都需要用到，因此我选择了nvidia/cuda:12.9.1-devel-ubuntu24.04这个镜像作为基础镜像。这里的12.9.1是我之前在宿主机上安装的Nvidia驱动对应的CUDA版本号。\n设计容器方案\n考虑到我需要使用这个容器开发相当长的时间，因此在扩展了一些工具或者进行了一些开发之后，需要把这些更改都给保存下来。这里有两种方案来保存这些更改：\n提交容器：在容器内安装和配置好所有需要的工具和库后，可以使用docker commit命令将当前容器的状态保存为一个新的镜像。这样可以方便地在以后重新创建相同环境的容器。 保存到磁盘：将一个物理机上的磁盘（或者一个文件夹）挂载到容器内，开发时就在这个挂载点上进行所有的文件操作，这样所有的更改都会直接反映到宿主机上。 这里我决定使用第二种方案，因为万一容器有啥问题，我能方便地换容器镜像，而不用担心开发的数据丢失。所以这里我们使用一个比较基础的镜像，里面安装一些基本的工具，偏定制化的工具我们在容器部署后在容器内进行安装和配置，并把安装位置和配置文件都放在挂载的目录中。\n创建Dockerfile\n在创建容器之前，我们需要先创建一个Dockerfile文件来定义容器的环境。这个文件可以放在任意目录下，我建议放在一个专门的目录中，例如~/docker/cuda-dev。下面是一个简单的Dockerfile示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # Base: CUDA 12.9 + Ubuntu 22.04 FROM nvidia/cuda:12.9.1-devel-ubuntu24.04 ENV DEBIAN_FRONTEND=noninteractive # Install system dependencies and sudo USER root RUN apt-get update \u0026amp;\u0026amp; apt-get install -y --no-install-recommends \\ build-essential \\ gcc-14 g++-14 \\ cmake \\ git \\ wget \\ curl \\ sudo \\ bzip2 \\ ca-certificates \\ zsh \\ \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* # Update alternatives to use GCC 14 RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100 \\ \u0026amp;\u0026amp; update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-14 100 # Give ubuntu user passwordless sudo RUN echo \u0026#34;ubuntu ALL=(ALL) NOPASSWD:ALL\u0026#34; \u0026gt;\u0026gt; /etc/sudoers # Switch to ubuntu user USER ubuntu WORKDIR /workspace CMD [\u0026#34;/bin/bash\u0026#34;] 这个Dockerfile会创建一个基于Nvidia CUDA开发镜像的容器，除了基础的工作，我还做了以下配置：\n将容器默认的用户ubuntu加入到sudo组中，这样可以在容器内使用sudo命令。 安装了一些常用的工具和库，如build-essential、git、wget、curl、vim等。 安装了zsh，毕竟我已经习惯了使用oh-my-zsh作为shell工具。 除此之外，我还比较习惯用oh-my-zsh和powerlevel10k定制终端，以及使用mamba作为开发环境管理工具。但是这些都是比较个性化的配置，按之前说的，我们在容器内进行安装和配置即可。\n创建docker-compose.yml\n为了方便管理和部署容器，我们可以使用docker-compose来创建一个docker-compose.yml文件。这个文件可以放在同一个目录下，例如~/docker/cuda-dev/docker-compose.yml。下面是一个简单的docker-compose.yml示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 version: \u0026#34;3.9\u0026#34; services: cuda-dev: build: . container_name: cuda-dev runtime: nvidia deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu] environment: - NVIDIA_VISIBLE_DEVICES=all - NVIDIA_DRIVER_CAPABILITIES=compute,utility volumes: - /home/${USER}/develop:/home/ubuntu - /run/media/${USER}/Disk1/devdata:/data tty: true 这个配置文件定义了一个名为cuda-dev的服务，使用我们之前创建的Dockerfile来构建镜像。我在宿主机的家目录下创建了一个workspace目录来存放开发文件，并将其挂载到容器的/home/ubuntu目录下。同时，我还将宿主机的一整个磁盘挂载到容器的/data目录下，这样可以方便地在容器内访问宿主机的文件。\n选择这种挂载方式是为了与上面的设计相适应：\n将develop文件夹挂载到容器的/home/ubuntu目录下，这样我们如果在容器内直接安装到/home/ubuntu，或者把开发文件放在这个目录下，所有的更改都会直接保存到宿主机里的develop文件夹中。 将宿主机的/run/media/${USER}/Disk1/devdata目录挂载到容器的/data目录下，这样我们把开发时需要的数据放在硬盘Disk1的devdata目录下，容器内就可以直接访问这些数据。 构建和运行容器\n在终端中进入到docker-compose.yml所在的目录，例如~/docker/cuda-dev，然后运行以下命令来构建容器：\n1 docker compose build 构建完成后用以下命令来运行容器：\n1 docker compose up -d 运行完成后，你可以通过以下命令来查看容器的状态：\n1 docker ps | grep cuda-dev-container 访问和测试容器\n容器运行后，你可以通过以下命令进入容器的终端：\n1 docker exec -it cuda-dev-container bash 查看CUDA编译器\n进入容器后，你可以测试CUDA编程环境是否正常工作，例如运行以下命令来检查CUDA版本：\n1 nvcc --version 如果输出了正确的CUDA版本信息，就说明CUDA编译器工作正常。\n查看容器能否调用Nvidia显卡\n运行以下命令来检查Nvidia显卡的状态：\n1 nvidia-smi 如果输出了正确的Nvidia显卡的状态信息，就说明容器可以调用Nvidia显卡。\n测试编译CUDA程序\n接下来我们来尝试编译在宿主机中编译失败的CUDA-Sample程序。去CUDA-Sample GitHub仓库的Release页面，选择跟你的CUDA版本号对应的版本下载，例如CUDA Samples v12.9。将下载的文件放到宿主机的~/docker/cuda-dev/workspace目录下，也可以在容器内使用wget命令直接下载：\n1 wget https://github.com/NVIDIA/cuda-samples/archive/refs/tags/v12.9.tar.gz 然后在容器内解压缩：\n1 2 tar -xzvf v12.9.tar.gz cd cuda-samples-12.9 之后就是正常的编译和运行流程了：\n1 2 3 mkdir build \u0026amp;\u0026amp; cd build cmake .. make -j4 编译完成后，你可以运行一些示例程序来测试CUDA编程环境是否正常工作，例如：\n1 2 cd 1_Utilities/deviceQuery ./deviceQuery 如果输出了Nvidia显卡的相关信息，就说明大功告成！\n测试Python能否调用GPU\n进入容器后，你可以测试Python能否调用GPU，例如运行以下Python代码：\n1 2 3 import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0)) 如果输出True和你的Nvidia显卡名称，就说明Python可以调用GPU。\n使用VS Code远程开发\n如果你想在VS Code中使用这个容器进行远程开发，可以安装Remote - Containers扩展。\n在宿主机打开VS Code后，点击左下角的远程连接图标，选择“Attach to Running Container”，然后选择cuda-dev-container容器，就可以连接到容器了。\n接下来你可以通过VS Code的终端直接进入容器命令行，也可以直接用VS Code打开容器中的工作目录进行开发。\n问题 # 在CUDA开发容器里使用YOLO训练模型时，遇到了一个报错：\n1 ERROR: Unexpected bus error encountered in worker. This might be caused by insufficient shared memory (shm). 这个问题的原因是Docker容器的共享内存（shm）默认大小只有64MB，比较小，不足以满足YOLO训练模型时的需求。解决这个问题的方法是增加Docker容器的共享内存大小，可以通过在docker-compose.yml文件中添加shm_size参数来实现，例如：\n1 2 3 services: cuda-dev: shm_size: \u0026#39;8gb\u0026#39; 这里的shm_size可以根据需要调整大小，例如8gb、16gb等。添加后重新构建和运行容器即可。\n","date":"2025年8月20日","externalUrl":null,"permalink":"/p/nvidia%E6%98%BE%E5%8D%A1%E5%9B%9B%E5%AE%B9%E5%99%A8%E5%8C%96%E9%85%8D%E7%BD%AEnvidia%E6%98%BE%E5%8D%A1%E7%9A%84cuda%E7%BC%96%E7%A8%8B%E5%92%8C%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E7%8E%AF%E5%A2%83/","section":"Posts","summary":" 缘起 # 这是“Nvidia显卡”系列文章的第四篇，本系列文章主要记录如何使用Nvidia显卡，尤其是在Linux系统上使用Nvidia显卡。在之前的文章中，我记录了在Ubuntu系统上使用Nvidia显卡的相关设置，包括游戏、CUDA编程、深度学习、Docker容器等。后来我转向了Fedora系统，本以为在Fedora系统上配置Nvidia显卡的CUDA编程和深度学习环境会和在Ubuntu系统上类似，而且由于有RPM Fusion的支持，安装和配置会更加方便。然而实际上却遇到了意想不到的困难，因此不得不再开一篇文章来记录这些问题和解决方案。\n","title":"Nvidia显卡（四）：容器化配置Nvidia显卡的CUDA编程和深度学习环境","type":"post"},{"content":"","date":"2025年8月19日","externalUrl":null,"permalink":"/tags/linux%E7%B3%BB%E7%BB%9F/","section":"Tags","summary":"","title":"Linux系统","type":"tags"},{"content":" 缘起 # Linux的桌面端近年来发展非常迅速，目前在易用性上Linux的桌面端已经做得非常不错了。然而由于其生态繁杂，各种工具和配置项五花八门，对于中文的支持一直都比较差。使用中文的新用户光折腾输入法可能都摸不着头脑，我曾经也在配置中文输入法、中文字体上花了不少精力、浪费了不少时间。而且就在最近，我又遇到了一些问题，决定把这些经验整理一下，帮助更多的用户顺利使用Linux系统中的中文环境。\n输入法配置 # Linux输入法基本概念 # 输入法框架：Linux下的输入法框架主要有IBus、fcitx和scim等，其中IBus是最常用的输入法框架。输入法框架负责管理输入法引擎和输入法前端，提供输入法的切换、配置等功能。 IBus：一个简单易用的输入法框架，支持多种输入法引擎，是很多Linux发行版的默认输入法框架。 fcitx：一个功能强大的输入法框架，支持多种输入法引擎，适合有一定经验的用户使用。fcitx目前更新到了第5版，也就是fcitx5。 scim：一个较老的输入法框架，功能较为简单，现已逐渐被IBus和fcitx所取代。 如果你没有中文输入的需求，那么系统默认的IBus一般就够用了，但是如果你需要中文输入法，那么我推荐卸载掉IBus，安装fcitx5并配置相应的输入法。\n输入法引擎：输入法引擎是实现具体输入法功能的组件，常见的中文输入法引擎有Pinyin、Wubi、Sogou等。不同的输入法引擎有不同的输入方式和词库，用户可以根据自己的习惯选择合适的输入法引擎。 这里我推荐中州韻（Rime）輸入法引擎。\noh-my-rime：Oh-My-Rime又叫“薄荷输入法”，是一款基于Rime输入法的输入法解决方案，在多个平台都适用。我个人非常推荐使用这个输入法解决方案，它提供了非常友好的配置界面和丰富的功能，能够满足大部分用户的需求。 Linux中文输入法安装 # 我一开始在Fedora系统上使用默认的IBus输入法框架，搭配Rime输入法引擎。这个方案在大部分情况下都能正常工作，但在VS Code里切换为中文时，一输入就会在跳出候选词窗口的瞬间卡住。在网上搜索一番之后发现是因为VS Code使用了Electron框架，而IBus在Wayland下与Electron的兼容性较差。解决方法是切换到fcitx5输入法框架。\n这里以Fedora 42系统为例，简单讲一下如何安装和配置IBus输入法和fcitx5输入法。\nIBus + Rime （不太推荐） # 虽然我个人不太推荐使用IBus，因为它和Wayland以及Electron的兼容性不好。但是如果你想坚持系统自带的输入法，也不怎么用基于Electron的软件，那么这套方案也未尝不可。\n安装Rime：因为系统已经自带了IBus框架，这里我们只需要安装Rime就行了：\n1 sudo dnf install ibus-rime 配置Rime：接下来就可以在设置中将Rime输入法添加进来。具体方法是通过设置-\u0026gt;Keyboard-\u0026gt;Virtual Keyboard，找到IBus，确认启用的是IBus框架。然后右击系统托盘里的输入法图标，选择Configure，就会看到下面的界面：\n点击Add添加输入法，选择Chinese里面的Rime，然后点击Add按钮。添加完成后就可以在输入法列表中看到Rime了。\n这时你就已经可以点击任务栏里的输入法图标或者使用快捷组合键，选择Rime输入法进行使用了。\n使用oh-my-rime：oh-my-rime的使用也非常简单，只要下载整个项目文件夹并放到输入法的配置里面就行了。在Fedora系统中，IBus的配置文件一般在~/.config/ibus/rime目录下。你可以通过以下命令下载oh-my-rime：\n1 git clone https://github.com/Mintimate/oh-my-rime.git 然后将整个oh-my-rime文件夹复制到~/.config/ibus/rime目录下。\n重启或者注销后重新登录，oh-my-rime就会生效。右击系统托盘里的输入法图标，就可以选择“薄荷拼音”作为输入方案了：\n额外配置 oh-my-rime还提供了很多可以配置的选项，你可以参照官方文档进行配置。我觉得默认配置就已经挺好用了，所以暂时没做额外的配置。\nfcitx5 + Rime (推荐) # 安装fcitx5：首先安装fcitx5及其相关组件：\n1 sudo dnf install fcitx5 fcitx5-chinese-addons 其中fcitx5是输入法框架，fcitx5-chinese-addons是中文输入法插件。\n安装Rime：可以通过以下命令安装Rime输入法：\n1 sudo dnf install fcitx5-rime librime-lua 其中fcitx5-rime就是Rime（中州韻）输入法的fcitx5版本；librime-lua是Rime的Lua脚本支持库，支持使用Lua进行输入法的定制。\n配置fcitx5：安装完成后将系统的输入法框架换为fcitx5，可以通过设置-\u0026gt;Keyboard-\u0026gt;Virtual Keyboard，可以看到下面的界面：\n点击fcitx5，再点击Apply。你就会发现任务栏里输入法的图标会从原来的IBus的变成一个小键盘，右击这个小键盘，选择Configure，会弹出下面的窗口：\n首先点击右上方位置的Update按钮来确保刚才安装的Rime输入法被正确识别。\n然后取消勾选右侧下方的Only Show Current Language选项，因为Rime是中文输入法，如果你的系统语言是英文，不取消勾选会找不到Rime输入法。取消勾选后你就可以找到Rime输入法了，选中后点击向左的箭头把Rime加入到输入法列表中。\n再等待一会儿等Rime自己配置完成就可以点击任务栏里的输入法图标或者使用快捷组合键，选择Rime输入法进行使用了：\noh-my-rime：oh-my-rime的使用和配置方法与IBus下的使用方法类似。不同的是fcitx的配置文件一般在~/.local/share/fcitx5目录下。你可以通过以下命令下载oh-my-rime：\n1 git clone https://github.com/Mintimate/oh-my-rime.git 然后把文件夹命名为rime放到~/.local/share/fcitx5目录下（如果里面已经有了rime文件夹，可以备份原来的再放过去）。\n重启或者注销后重新登录，oh-my-rime就会生效。右击系统托盘里的输入法图标，就可以选择“薄荷拼音”作为输入方案了：\nVS Code：完成上述配置后，大多数情况下都可以直接使用Rime输入法。但VS Code中还是无法输入中文，网上有建议将下述内容加入到环境变量中：\n1 2 3 GTK_IM_MODULE=fcitx QT_IM_MODULE=fcitx XMODIFIERS=@im=fcitx 但是这样偶尔在启动进入桌面时有警告信息。我觉得一个更好的方法是在VS Code的快捷方式中加入参数--enable-features=UseOzonePlatform,WaylandWindowDecorations '--ozone-platform=wayland %F'。具体方法是右击开始菜单的VS Code图标：\n点击Edit Application，在命令行参数里添加上面的参数：\n这样再打开VS Code就可以用Rime输入法在里面输入中文了！\n额外配置 同样地，oh-my-rime还提供了很多可以配置的选项，你可以参照官方文档进行配置。我觉得默认配置就已经挺好用了，所以暂时没做额外的配置。\n（可选）卸载IBus：首先需要卸载掉系统默认的IBus输入法框架，可以使用以下命令：\n1 sudo dnf remove ibus 通过以上步骤，就可以在Fedora 42系统中顺利安装和配置fcitx5中文输入法了。\n","date":"2025年8月19日","externalUrl":null,"permalink":"/p/linux%E7%B3%BB%E7%BB%9F%E4%B8%AD%E6%96%87%E8%BE%93%E5%85%A5%E6%B3%95%E4%B8%AD%E5%B7%9E%E9%9F%BBrime%E5%92%8C%E8%96%84%E8%8D%B7%E8%BE%93%E5%85%A5%E6%B3%95oh-my-rime/","section":"Posts","summary":" 缘起 # Linux的桌面端近年来发展非常迅速，目前在易用性上Linux的桌面端已经做得非常不错了。然而由于其生态繁杂，各种工具和配置项五花八门，对于中文的支持一直都比较差。使用中文的新用户光折腾输入法可能都摸不着头脑，我曾经也在配置中文输入法、中文字体上花了不少精力、浪费了不少时间。而且就在最近，我又遇到了一些问题，决定把这些经验整理一下，帮助更多的用户顺利使用Linux系统中的中文环境。\n","title":"Linux系统中文输入法：中州韻Rime和薄荷输入法（oh-my-rime）","type":"post"},{"content":"","date":"2025年8月19日","externalUrl":null,"permalink":"/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/","section":"Categories","summary":"","title":"操作系统","type":"categories"},{"content":"","date":"2025年8月19日","externalUrl":null,"permalink":"/tags/%E8%BE%93%E5%85%A5%E6%B3%95/","section":"Tags","summary":"","title":"输入法","type":"tags"},{"content":"","date":"2025年8月16日","externalUrl":null,"permalink":"/tags/boot/","section":"Tags","summary":"","title":"Boot","type":"tags"},{"content":"","date":"16 八月 2025","externalUrl":null,"permalink":"/en/series/boot-loader-series/","section":"Series","summary":"","title":"Boot Loader Series","type":"series"},{"content":"","date":"2025年8月16日","externalUrl":null,"permalink":"/tags/bootloader/","section":"Tags","summary":"","title":"Bootloader","type":"tags"},{"content":"","date":"2025年8月16日","externalUrl":null,"permalink":"/tags/grub/","section":"Tags","summary":"","title":"Grub","type":"tags"},{"content":"","date":"2025年8月16日","externalUrl":null,"permalink":"/tags/windows/","section":"Tags","summary":"","title":"Windows","type":"tags"},{"content":"","date":"16 八月 2025","externalUrl":null,"permalink":"/en/tags/windows/linux-dual-boot/","section":"Tags","summary":"","title":"Windows/Linux Dual Boot","type":"tags"},{"content":"","date":"2025年8月16日","externalUrl":null,"permalink":"/tags/windows/linux%E5%8F%8C%E7%B3%BB%E7%BB%9F/","section":"Tags","summary":"","title":"Windows/Linux双系统","type":"tags"},{"content":" 缘起 # 这是”启动引导“系列文章的第二篇。我也是万万没想到这么个简单的话题还能整成一个系列，因为我在写第一篇文章的时候就觉得已经把启动引导的相关概念已经讲得很明白了，而且成功搞了很多次多系统的启动引导配置。但是最近在搞双硬盘分别安装Windows和Linux双系统时踩了一个巨大的坑，所以感觉有必要再写一篇文章来记录一下。\n本系列的另一篇比较详细地讲了启动引导的基本概念和多系统启动项的配置方法，建议先看一下：\n启动引导（一）：多系统启动项配置 Windows/Linux双硬盘双系统的坑 # 我本来以为经过上面的探索，我已经对多系统启动项非常熟悉了。所以当在一个双硬盘的电脑上安装Windows、Linux双系统时我信心满满，觉得在同一个硬盘上装多系统我都搞定了，俩系统分开装岂不更简单？然而现实却是啪啪打脸，我到最后甚至都没能优雅地解决。\n背景 # 先说一下我的基本情况：电脑上装了两块固态硬盘A（1TB）和B（500GB），俩硬盘都装在主板上的M.2接口。A硬盘装的那个M.2接口离CPU更近，B硬盘离CPU稍远一点。离CPU远近有啥影响呢？我本来以为没啥影响，但最终发现这似乎是一个巨大的影响因素。\n我选择使用Fedora 42作为自己的主力系统，所以选择1T的固态硬盘A来装Fedora 42。Windows 11系统只是辅助系统，装在500G的B硬盘上。按照惯例，Windows/Linux双系统安装时选择优先安装Windows，因为后安装的话Windows会覆盖Linux的引导程序，导致Linux无法启动。但是我点亮机器时已经将Fedora 42先装到了硬盘A上，且已经用了几天了，懒得再把它清理掉了。而且我觉得我已经对启动引导了如指掌了，即使先装Linux再装Windows那自然也就是个调整启动引导的小事。然后我就有了下面这个第一次的失败经历。\n第一次失败的尝试 # 失败的过程总结起来是这样的：\n先在硬盘A上装Fedora 42，成功引导进入系统。\n再在硬盘B上装Windows 11。最保险的做法自然是将硬盘A拆下来，只留下硬盘B来安装Windows 11。这样Windows 11就会完全安装在硬盘B上，不会影响硬盘A上的Fedora 42。但是由于我懒得拆机，而且硬盘A那个M.2插槽还被显卡挡住了，拆机更麻烦。所以我就想，在安装Windows 11时能不能把硬盘A给屏蔽掉？\n屏蔽硬盘A。首先我想到的是使用BIOS设置来禁用硬盘A，但我发现坑爹的MSI主板居然没有这个功能。我的Z890也算是中高端主板了，居然连这么个功能都没有。于是只能找其他方法屏蔽。经历了一番探索，发现可以通过在Windows安装时使用Diskpart工具来隐藏硬盘A。具体做法是这样的：\n启动Windows安装程序，在进入第一个安装界面（也就是选择语言那里）时按Shift + F10，打开命令提示符。\n在命令提示符中输入以下命令，启动Diskpart工具：\n1 diskpart 然后输入以下命令，列出所有磁盘：\n1 list disk 找到硬盘A的磁盘编号（例如Disk 0），然后输入以下命令，选择硬盘A：\n1 select disk 0 最后输入以下命令，将硬盘A下线：\n1 offline disk 这样，在本次Windows安装过程中，安装程序都无法看到硬盘A。\n退出Diskpart工具：\n1 exit 关闭命令提示符，继续安装Windows 11。\n屏蔽硬盘A后继续运行安装程序。在安装程序运行到末期，需要建立启动引导，然后重启。这时问题来了，Windows 11的引导程序虽然看不到硬盘A，但由于硬盘A的M.2接口离CPU更近，Windows安装程序似乎意识到自己待的这个硬盘B不是第一块启动的硬盘，所以它仍然试图往启动顺序靠前的硬盘写入引导信息。但是硬盘A已经被下线了啊。于是Windows安装程序傲娇地拒绝把引导写在启动顺序靠后的硬盘B上，并报错说\n1 Windows could not prepare the computer to boot into the next phase of installation. To install Windows, restart the installation. 于是本次尝试以失败告终。\n第二次失败的尝试 # 有了第一次的教训，我也懒得管硬盘A上的Fedora系统了，反正才用了几天，做的配置还不多。就按照传统方案，先装Windows，把Fedora的引导项损坏了也无所谓，之后再重装Fedora就是了。于是就有了下面的操作：\n在硬盘B上安装Windows 11，安装时不再将硬盘A下线。这倒是很顺利，能正常安装且正常启动进入Windows系统。\nWindows 11安装时破坏了Fedora的启动引导，我也懒得修复了，直接重装Fedora。Fedora重装后可以正常进入系统。\n但是我在Fedora里试图用grub2-mkconfig命令重新生成GRUB配置时，发现它并没有自动检测到Windows 11的启动项。这时我才意识到，Windows 11的引导程序并没有将引导信息写入硬盘B，而是仍然试图将其写入硬盘A。而Fedora在安装时将它覆盖掉了！\n行吧，刚才为了嫌修复Fedora的引导麻烦将其重装，但这下又要修复Windows的引导。但反正这活儿我也熟，于是开始修复：\n在Fedora系统里调整硬盘B的分区，在里面重新建立一个300M的FAT32分区，并将其命名为EFI。\n重新从U盘启动Windows 11的安装程序，在进入第一个安装界面时按Shift + F10，打开命令提示符。\n在命令提示符中输入以下命令：\n1 2 diskpart list disk 找到硬盘B的磁盘编号（例如Disk 1），然后输入以下命令，选择硬盘B：\n1 2 select disk 1 list partition 找到刚才新建的EFI分区的分区编号，以及硬盘B上安装Windows 11的那个分区的编号和盘符。可以根据分区的大小来判断，大小为300M的就是EFI分区，大小为400多G的就是Windows系统分区。发现EFI分区编号为4, Windows系统分区编号为3,且没有分配盘符。于是我们给EFI分区分配盘符为S，给Windows系统分区分配盘符为C。\n1 2 3 4 5 select partition 4 assign letter=S select partition 3 assign letter=C exit 这样我们就可以将Windows的启动引导程序从系统写入到EFI分区了\n1 bcdboot C:\\Windows /s S: /f UEFI 这样就完成了Windows的启动引导程序的修复。\n修复完成后，重启电脑，进入BIOS设置，将硬盘B设置为第一启动项。这样就可以正常启动Windows 11了。但是我常用的是Fedora，所以我想把Windows的启动项加到Fedora的Grub里。然后把硬盘A设置为第一启动项，这样默认会进入Fedora，当我想启动Windows时，在Fedora的Grub界面选择Windows就行了。\n于是进入Fedora系统，运行sudo grub2-mkconfig -o /boot/grub2/grub.cfg。然而，Fedora并没有检测到刚才我们修复的Windows启动项。运行sudo os-prober也没有任何输出，表示Fedora就是看不见这个Windows启动项。我百思不得其解，这么明显一EFI分区，就在磁盘B上，我也mount了，它为啥就是视而不见。\n我发现这个EFI分区跟正常装系统时安装程序自己建的EFI分区有些不一样。具体来说，系统自己建的EFI分区和系统分区在其他系统的文件管理器里默认是显示为一整块硬盘的，但现在这个EFI分区跟同一硬盘上的系统分区却是分开的。我也不知道为啥我手动修复建立的分区就是如此不合群。当然，还有解决方法就是手动修改Grub配置，强行把Windows的启动项添加进去。但这样就不优雅了。\n于是，这次尝试又是以失败告终。\n最终解决方案 # 我意识到Windows就是如此流氓，就是非得把引导分区建立在它所监测到的最先启动的硬盘上。所以要想老老实实让它把系统和启动引导都放在硬盘B上，只能把硬盘B挪到第一个M.2接口上，或者在安装Windows时把第一个M.2接口上的硬盘拆下来。\n总之，还是逃不过拆机。\n由于Fedora系统本身没问题，所以只需要把硬盘A拆下来。然后在硬盘B上安装Windows 11就行了。装好后再把硬盘A装回主板。\n启动进入Fedora系统，这次再运行os-prober和grub2-mkconfig就可以检测到Windows 11的启动项了。\n总结 # 我是真的没想到在俩硬盘上装Windows/Linux双系统会有如此多的坑。这里的教训就是：\n如果要在双硬盘上安装双系统，最好把要装Windows的那个硬盘插到启动顺序靠前的那个M.2接口上。 如果你非要把要装Linux的那个硬盘插到启动顺序靠前的M.2接口上，那么要么在安装Windows时把第一个硬盘拆下来，要么用其他电脑在硬盘B上安装好Windows，再把硬盘B插到主板上。 ","date":"2025年8月16日","externalUrl":null,"permalink":"/p/%E5%90%AF%E5%8A%A8%E5%BC%95%E5%AF%BC%E4%BA%8C%E5%8F%8C%E7%A1%AC%E7%9B%98%E4%B8%8Bwindows/linux%E5%8F%8C%E7%B3%BB%E7%BB%9F%E5%90%AF%E5%8A%A8%E9%A1%B9%E9%85%8D%E7%BD%AE%E8%B8%A9%E5%9D%91/","section":"Posts","summary":" 缘起 # 这是”启动引导“系列文章的第二篇。我也是万万没想到这么个简单的话题还能整成一个系列，因为我在写第一篇文章的时候就觉得已经把启动引导的相关概念已经讲得很明白了，而且成功搞了很多次多系统的启动引导配置。但是最近在搞双硬盘分别安装Windows和Linux双系统时踩了一个巨大的坑，所以感觉有必要再写一篇文章来记录一下。\n","title":"启动引导（二）：双硬盘下Windows/Linux双系统启动项配置踩坑","type":"post"},{"content":"","date":"2025年8月16日","externalUrl":null,"permalink":"/series/%E5%90%AF%E5%8A%A8%E5%BC%95%E5%AF%BC%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"启动引导系列","type":"series"},{"content":"","date":"2025年8月15日","externalUrl":null,"permalink":"/tags/fedora/","section":"Tags","summary":"","title":"Fedora","type":"tags"},{"content":" 缘起 # 这是“Nvidia显卡”系列文章的第三篇，本系列文章主要记录如何使用Nvidia显卡，尤其是在Linux系统上使用Nvidia显卡。在之前的文章中，我记录了在Ubuntu系统上使用Nvidia显卡的相关设置，包括游戏、CUDA编程、深度学习、Docker容器等。现在我已经从Ubuntu迁移到了Fedora系统，而在Fedora上使用Nvidia显卡的设置与Ubuntu有所不同，所以在这里记录一下。\n本系列的其他文章参见：\nNvidia显卡（一）：Ubuntu下的游戏、CUDA、深度学习、Docker等 Nvidia显卡（二）：视频剪辑转码工具FFmpeg使用GPU加速 Nvidia显卡（四）：容器化配置Nvidia显卡的CUDA编程和深度学习环境 Fedora安装Nvidia显卡驱动 # 查看显卡信息 # 首先，我们需要查看一下我们的显卡信息，打开终端，输入以下命令：\n1 lspci | grep VGA 如果你的电脑上安装了英伟达的显卡，你会看到类似以下的输出：\n1 01:00.0 VGA compatible controller: NVIDIA Corporation Device 2803 (rev a1) 不知道为啥，我的电脑上显示的是NVIDIA Corporation Device 2803，而不是RTX 4060 Ti，不过没关系，我们只需要知道这是一块英伟达的显卡就行了。\n安装Nvidia显卡驱动 # 在Ubuntu中，我们使用了apt源里的graphics-drivers包来安装Nvidia显卡驱动，而在Fedora中，我们需要使用rpmfusion源来安装Nvidia显卡驱动。\n相比直接从Nvidia官网下载安装包，使用rpmfusion源安装Nvidia显卡驱动更为简单和方便，同时由于rpmfusion中的驱动是经历过在Fedora中测试的，所以更为稳定。主要步骤如下：\n添加rpmfusion源：\n1 2 sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm sudo dnf install https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm 也可以在RPM Fusion官网下载与你的Fedora版本对应的rpmfusion源安装包，在Fedora系统里直接双击下载的安装包，Fedora会自动打开Discover软件中心，点击标题栏的“安装”按钮即可。一般我们需要安装两个源：rpmfusion-free和rpmfusion-nonfree，前者包含了开源软件，后者包含了非开源软件。\n装好之后可以更新一下软件源：\n1 sudo dnf update 安装Nvidia显卡驱动：\n可以参考RPM Fusion官网的说明，使用以下命令安装Nvidia显卡驱动：\n1 2 sudo dnf install akmod-nvidia # rhel/centos users can use kmod-nvidia instead sudo dnf install xorg-x11-drv-nvidia-cuda #optional for cuda/nvdec/nvenc support 这里的akmod-nvidia是Fedora中Nvidia显卡驱动的包名，安装后会自动编译内核模块。\n(可选)安全启动：\n什么是安全启动 # 安全启动是一种安全标准，旨在确保计算机在启动时只加载经过验证的操作系统和软件。它可以防止恶意软件在系统启动时加载，从而提高系统的安全性。简单来说，安全启动可以确保只有可信的软件才能在系统启动时运行。\n安全启动是如何工作的 # UEFI固件中有一个安全启动数据库，用于存储所有被信任的公钥和证书。一般来说，被信任的公钥包括操作系统厂商、硬件厂商等的公钥，以及用户自己生成的公钥。 当计算机启动时，UEFI固件会验证每个加载的组件（如操作系统内核、驱动程序等）是否被信任。 只有在安全启动数据库中注册的组件才能被加载，未授权的组件将被阻止。 Linux内核模块（如Nvidia显卡驱动）需要经过签名才能在启用了安全启动的系统上加载。如果内核模块没有签名，或者签名不被信任，那么在启用了安全启动的系统上，这些模块将无法加载，从而导致显卡驱动无法工作。这就是为什么安装了Nvidia显卡驱动后，需要为Linux内核模块生成签名并注册到安全启动数据库中。\n我是否需要安全启动 # 对于普通用户（即对电脑了解有限，也不了解电脑启动过程的用户）而言，我的建议是启用安全启动；对于高级用户（知道自己在干什么，尤其是知道系统在启动时在干什么，或者知道自己让系统在启动时干什么的用户）而言，我的建议是可以禁用安全启动。\n如果你的电脑启用了安全启动 # 如果你的电脑启用了安全启动（Secure Boot），可能会导致Nvidia显卡驱动无法加载。你可以在BIOS中禁用安全启动。\n如果你不想禁用安全启动，可以参考RPM Fusion官网的说明，注册一个密钥并将其添加到你的系统中，以启用安全启动。具体步骤如下：\n安装相关工具\n1 sudo dnf install kmodtool akmods mokutil openssl 生成密钥\n1 sudo kmodgenca -a 注册密钥\n1 sudo mokutil --import /etc/pki/akmods/certs/public_key.der 这里会提示你设置一个密码，这个密码在重启后需要输入。\n注意：由于这里是给当前的内核版本生成密钥，所以之后一旦更新了内核版本，就可能需要重新生成密钥并注册（即重复生成密钥和注册密钥的步骤）。\n重启电脑\n如果你在上一步设置了安全启动，重启后进入MOK管理界面，选择“Enroll MOK”，输入你设置的密码，完成后继续启动系统。 如果你在上一步禁用了安全启动，直接重启电脑即可。 确认显卡驱动是否安装成功：\n1 nvidia-smi 如果你看到了类似以下的输出，那么恭喜你，你的Nvidia显卡驱动安装成功了：\n1 2 3 4 5 6 7 8 9 10 11 +-----------------------------------------------------------------------------+ | NVIDIA-SMI 470.57.02 Driver Version: 470.57.02 CUDA Version: 12.5 | |-------------------------------+----------------------+----------------------+ | GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |===============================+======================+======================| | 0 NVIDIA RTX 4060 Ti Off | 00000000:01:00.0 Off | N/A | | N/A 41C P8 10W / N/A | 0MiB / 7611MiB | 0% Default | | | | N/A | +-------------------------------+----------------------+----------------------+ 测试显卡 # 我们可以使用glmark2来测试显卡性能。\n安装glmark2： 1 sudo dnf install glmark2 运行glmark2： 1 glmark2 如果在终端中看到了类似以下的输出，那么说明显卡性能测试成功：\n1 2 3 4 5 6 7 8 ======================================================= glmark2 2021.02 ======================================================= OpenGL Information GL_VENDOR: NVIDIA Corporation GL_RENDERER: NVIDIA GeForce RTX 4060 Ti/PCIe/SSE2 GL_VERSION: 4.6.0 NVIDIA 555.58.02 ======================================================= 并且会有一个窗口弹出来，显示正在测试的内容。测试完成后，终端中会显示测试的分数。\n游戏 # Linux系统本来是不太适合玩游戏的，但是随着Steam的推广，越来越多的游戏可以通过Proton在Linux上运行。Proton是Valve开发的一个基于Wine的工具，可以在Linux上运行Windows游戏。\n在Ubuntu上安装Steam还需要安装一些额外的依赖包，并且手动安装Proton。而在Fedora上，安装Steam就非常简单了，而且默认就包含了Proton。\n安装Steam # 安装Steam： 1 sudo dnf install steam 登录Steam账号 在开始菜单中找到Steam就可以点击打开，登录你的Steam账号即可。\n安装游戏 # 如果你是第一次使用Steam，尚未购买任何游戏，你可以选择一些免费的游戏进行测试，例如《Dota 2》、《Counter-Strike: Global Offensive》等。\n启动游戏 # 安装好之后可以点击“Launch”来启动游戏，但是第一次启动往往不会成功，需要启动三次。\n第一次启动会卡在下载一个组件上面，点开下载管理，发现它试图下载Steam Linux Runtime 2.0。但不知为何一直无法启动下载，而且明明已经有来Steam Linux Runtime 3.0。我只能取消启动。 第一次取消启动之后再次启动游戏，Steam Linux Runtime 2.0会被成功下载，游戏进入加载界面。但会卡在一个全屏黑屏的界面。只能点击停止游戏。 第三次启动游戏，这次游戏会成功加载，进入着色器编译阶段。等待着色器编译完成就可以正常启动了。 手柄 # 如果你使用手柄，还需要一点点额外的配置。默认使用USB线来连接但话直接就能玩。\n如果使用XBox手柄，可以通过蓝牙连接。但即使连上，你可能会发现手柄依然不能控制游戏。这是因为缺少相关驱动，我们可以安装xpadneo包来解决这个问题：\n1 sudo dnf install xpadneo 然后重新连接蓝牙。连接上时手柄会轻微震动一下，而之前连接时是没有震动的，这说明手柄已经成功被识别，可以用来玩游戏了。\nCUDA编程 # CUDA是英伟达公司推出的并行计算平台和编程模型，可以利用GPU的并行计算能力，加速计算密集型应用程序。CUDA编程需要安装Nvidia显卡驱动和CUDA工具包。而且CUDA版本和Nvidia显卡驱动版本有一定的对应关系，需要根据自己的显卡驱动版本选择合适的CUDA版本。\n2025年8月更新：安装完发现这样安装还是不行，主要是Fedora 42和CUDA 12.9的兼容性问题。可直接参见最后的“问题”部分。\n安装CUDA # 查看Nvidia显卡驱动需要的CUDA版本： 1 nvidia-smi 在CUDA Version一行中，可以看到Nvidia显卡驱动需要的CUDA版本，例如CUDA Version: 12.9。即我们需要安装CUDA 12.9。\n安装CUDA： 在Ubuntu中安装CUDA时，我们直接参照了Nvidia官网上的安装指南。\n在Fedora中，由于我们刚才在安装驱动时用的时RPM Fusion仓库，直接参照Nvidia官网的话可能会遇到版本不匹配的问题。例如上一步中我们查到需要安装CUDA 12.9，但在Nvidia官网上可能只提供CUDA 13.0的安装包,如果直接按照Nvidia官网上的指示安装，就会造成版本不匹配的问题。这是因为RPM Fusion在Nvidia官方发布驱动之后还要测试，稳定之后才会推送到RPM仓库。因此我们这里还是要按照RPM Fusion上的说明来安装CUDA。\n其命令如下：\n1 2 3 4 5 sudo dnf config-manager addrepo --from-repofile=https://developer.download.nvidia.com/compute/cuda/repos/fedora41/$(uname -m)/cuda-fedora41.repo sudo dnf clean all sudo dnf module disable nvidia-driver sudo dnf config-manager setopt cuda-fedora41-$(uname -m).exclude=nvidia-driver,nvidia-modprobe,nvidia-persistenced,nvidia-settings,nvidia-libXNVCtrl,nvidia-xconfig sudo dnf -y install cuda-toolkit 注意到上面的命令中安装的CUDA版本是为Fedora 41准备的，而我们的Fedora版本是42。但是不用担心，RPM Fusion上的说明里也提到了，上述命令对于Fedora 41之后的版本都适用。\n配置环境变量： 如果安装成功，你会在/usr/local/cuda-12.9目录下看到CUDA的安装文件。同时你会在/usr/local/cuda目录下看到一个指向/usr/local/cuda-12.9的软链接。如果之后升级CUDA版本，/usr/local/cuda会指向新的CUDA版本目录。\n我们需要配置环境变量，使得CUDA可以被找到。将cuda的bin目录和lib64目录添加到PATH和LD_LIBRARY_PATH环境变量时，我们使用/usr/local/cuda目录，而不是/usr/local/cuda-12.9目录，因为这样可以在以后升级CUDA版本时不需要修改环境变量。\n如果你使用bash，可以在~/.bashrc文件中添加以下内容：\n1 2 3 echo \u0026#39;export PATH=/usr/local/cuda/bin${PATH:+:${PATH}}\u0026#39; \u0026gt;\u0026gt; ~/.bashrc echo \u0026#39;export LD_LIBRARY_PATH=/usr/local/cuda/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}\u0026#39; \u0026gt;\u0026gt; ~/.bashrc source ~/.bashrc 如果使用其他shell，可以将以上内容添加到对应的配置文件中。\n测试CUDA： 1 nvcc --version 如果你看到了类似以下的输出，那么说明CUDA安装成功：\n1 2 3 4 5 nvcc: NVIDIA (R) Cuda compiler driver Copyright (c) 2005-2024 NVIDIA Corporation Built on Thu_Jun__6_02:18:23_PDT_2024 Cuda compilation tools, release 12.9, V12.9.82 Build cuda_12.9.r12.9/compiler.34385749_0 问题 # 经历上面的步骤，看似我们已经成功安装了CUDA，但实际上上面的编译器在Fedora 42中是不能用的。如果你直接使用它来编译，会报错gcc版本不匹配。因为上面安装的CUDA版本是12.9,它只匹配GCC 14及以下的版本，而Fedora 42默认安装的是GCC 15。\n而且这个问题也无法通过安装GCC 14来解决，因为即使安装了GCC 14，Fedora 42中的库、头文件之类的仍然是GCC 15的版本，所以编译时仍会报错。\n这个问题很难解决，一个办法是不再使用RPM Fusion来安装Nvidia驱动、CUDA等，而是直接从Nvidia官网下载安装包来安装CUDA 13.0及对应的驱动。CUDA 13.0是支持GCC15的，然而这依然会有两个问题：\n无法使用Fedora的包管理系统来管理Nvidia驱动和CUDA的安装和升级。万一有跟他们相关的依赖是由Fedora管理的，那么没准哪一次更新时就会更新失败。问题严重时甚至会影响系统的稳定性。 深度学习常用的PyTorch至今（2025年8月）仍未适配CUDA 13.0，所以安装了CUDA 13.0仍然无法搞很多深度学习。 如果你非要使用Fedora系统，我认为目前（2025年8月）有两个解决方案：\n一是放弃Fedora 42，转向Fedora 41。但这估计不能通过简单的系统版本降级来实现，因为牵涉到的东西太多。估计只能重装系统。 二是使用虚拟化的容器来配置。等到之后RPM Fusion和PyTorch都支持CUDA 13.0了,或许可以再把容器丢掉。 我思考过后选择了第二个选项。我决定把安装好的Nvidia驱动和CUDA先放在这里，另起一个docker/Podman容器来配置CUDA编程和深度学习开发环境。主要想法是在容器中运行Ubuntu 24.04，安装与之兼容的CUDA和Nvidia驱动，由于PyTorch暂未支持CUDA 13.0，因此在容器中使用CUDA 12.9。具体配置过程我会另起一篇文章，请参见：\nNvidia显卡（四）：容器化配置Nvidia显卡的CUDA编程和深度学习环境 ","date":"2025年8月15日","externalUrl":null,"permalink":"/p/nvidia%E6%98%BE%E5%8D%A1%E4%B8%89fedora%E4%B8%8B%E7%9A%84%E6%B8%B8%E6%88%8Fcuda%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0docker%E7%AD%89/","section":"Posts","summary":" 缘起 # 这是“Nvidia显卡”系列文章的第三篇，本系列文章主要记录如何使用Nvidia显卡，尤其是在Linux系统上使用Nvidia显卡。在之前的文章中，我记录了在Ubuntu系统上使用Nvidia显卡的相关设置，包括游戏、CUDA编程、深度学习、Docker容器等。现在我已经从Ubuntu迁移到了Fedora系统，而在Fedora上使用Nvidia显卡的设置与Ubuntu有所不同，所以在这里记录一下。\n","title":"Nvidia显卡（三）：Fedora下的游戏、CUDA、深度学习、Docker等","type":"post"},{"content":"","date":"11 八月 2025","externalUrl":null,"permalink":"/en/tags/remote-development/","section":"Tags","summary":"","title":"Remote Development","type":"tags"},{"content":"","date":"2025年8月11日","externalUrl":null,"permalink":"/tags/ssh/","section":"Tags","summary":"","title":"Ssh","type":"tags"},{"content":" 缘起 # SSH（Secure Shell）是非常常用的远程登录协议，我本身使用得已经非常多了，设置并不难，而且很多都是一劳永逸。但大部分设置基本都是每次换电脑或重装系统时才会搞，使用频率非常低，所以每次都要重新查找相关资料。\n这里就把常用的SSH相关内容整理一下，方便以后查找。\nSSH安装 # 首先，SSH分为客户端和服务端。客户端是用来连接其他电脑的，服务端是用来允许其他电脑连接到你的。\n目前几乎所有主流操作系统（Windows/macOS/Linux）都自带SSH客户端，所以随便拿到一台电脑，你基本上都可以打开命令行工具，输入ssh命令来使用SSH。\n但SSH服务端就不一样了，大部分操作系统默认并不安装SSH服务端，或者即使安装了，也一般不会默认就开启。所以这里主要讲一下如何安装和设置SSH服务端自启动。\nWindows # 之前的Windows版本（Windows 10 1809之前）没有自带SSH服务端，但从Windows 10 1809开始，Windows就自带了OpenSSH服务端。但默认是没有安装的，需要用户主动启用。\n打开“设置”应用，点击“应用”。 在“应用和功能”页面，点击右侧的“可选功能”。 在“可选功能”页面，点击“添加功能”。 在“添加功能”页面，找到“OpenSSH Server”，点击安装。 安装完成后，打开“服务”应用（可以在开始菜单搜索“服务”），找到“OpenSSH SSH Server”，右键点击，选择“属性”。 在“属性”窗口中，将“启动类型”设置为“自动”，然后点击“启动”按钮启动服务。 点击“应用”和“确定”保存设置。 Linux # 大部分Linux发行版都自带SSH服务端，但如果没有，可以通过包管理器安装。\nUbuntu/Debian系统： 1 2 sudo apt update sudo apt install openssh-server Fedora/RHEL/CentOS系统： 1 sudo dnf install openssh-server Arch Linux： 1 sudo pacman -S openssh openSUSE： 1 sudo zypper install openssh 安装完成后可以通过以下命令启动SSH服务并设置为开机自启：\n1 2 sudo systemctl start sshd sudo systemctl enable sshd macOS # macOS自带SSH服务端，但默认是关闭的。可以通过以下步骤启用：\n打开“系统偏好设置”，点击“共享”。 在“共享”页面，勾选“远程登录”。 这时会自动启用SSH服务端，并显示SSH连接信息。 SSH免密码登录 # SSH免密码登录是通过SSH密钥对实现的，可以让你在连接到远程服务器时不需要输入密码。\n其原理是在本地电脑A生成一对密钥（公钥和私钥），然后将公钥复制到远程服务器B上。之后再从电脑A连接到服务器B时，SSH会使用密钥对进行身份验证，而不需要输入密码。\n在本地电脑A上生成SSH密钥对：\n1 ssh-keygen -t ed25519 这里的-t ed25519表示使用ed25519算法生成密钥对，你也可以使用-t rsa生成RSA密钥对。\n按照提示一路回车即可，默认会将生成的私钥和公钥分别保存在~/.ssh/id_ed25519和~/.ssh/id_ed25519.pub。\n将公钥复制到远程服务器B上：\n如果本地电脑A是Linux或macOS，可以使用以下命令将公钥复制到远程服务器B上：\n1 ssh-copy-id user@remote_server 这里的user是远程服务器B上的用户名，remote_server是远程服务器B的IP地址或域名。这里你需要输入远程服务器B的密码。\n如果本地电脑A是Windows，可以手动将公钥内容复制到远程服务器B的~/.ssh/authorized_keys文件中。\n具体来说，首先在本地电脑A上查看公钥内容：\n1 Get-Content $env:USERPROFILE\\.ssh\\id_ed25519.pub 或者直接用记事本打开C:\\Users\\\u0026lt;YourUsername\u0026gt;\\.ssh\\id_ed25519.pub文件，将内容复制。\n然后在远程服务器B上创建或编辑~/.ssh/authorized_keys文件，将公钥内容粘贴进去。\n最后为了安全起见，确保~/.ssh/authorized_keys文件的权限设置正确：\n1 chmod 600 ~/.ssh/authorized_keys 测试免密码登录： 现在你可以尝试从本地电脑A连接到远程服务器B，直接运行：\n1 ssh user@remote_server 如果一切设置正确，你应该可以直接登录到远程服务器B，而不需要输入密码。\nSSH配置文件 # SSH配置文件可以用来简化SSH连接命令，用户级配置一般位于~/.ssh/（Linux和macOS）或C:\\Users\\\u0026lt;YourUsername\u0026gt;\\.ssh\\（Windows）中，系统级配置一般位于/etc/ssh/（Linux和macOS）或C:\\ProgramData\\ssh\\（Windows）中。系统级配置文件对所有用户生效，需要管理员或者sudo权限才能修改；用户级配置文件只对当前用户生效。\nSSH配置文件分为客户端配置文件和服务端配置文件。\n客户端配置文件 # 系统级客户端配置文件是/etc/ssh/ssh_config（Linux和macOS）或C:\\ProgramData\\ssh\\ssh_config（Windows），用户级客户端配置文件是~/.ssh/config（Linux和macOS）或C:\\Users\\\u0026lt;YourUsername\u0026gt;\\.ssh\\config（Windows）。如果配置比较多，也可以将配置文件拆开成多个文件，系统级配置文件可以放在/etc/ssh/ssh_config.d/目录下，用户级配置文件可以放在~/.ssh/config.d/目录下。\n客户端配置文件的常用配置项包括：\nHost：指定主机别名，可以使用通配符*，表示匹配所有主机。 HostName：指定主机名或IP地址。 User：指定登录用户名。 Port：指定SSH连接端口，默认是22。 IdentityFile：指定私钥文件路径。 ForwardAgent：是否启用SSH代理转发，默认是no。 ServerAliveInterval：设置服务器存活检测的时间间隔，单位是秒，默认是0（不检测）。 ServerAliveCountMax：设置服务器存活检测的最大次数，默认是3。 ControlMaster：是否启用SSH连接复用，默认是no。 ControlPath：指定SSH连接复用的Socket文件路径，默认是~/.ssh/sockets/%r@%h:%p。 ControlPersist：是否启用SSH连接复用的持久化，默认是no。 下面是一个示例的用户级SSH客户端配置文件~/.ssh/config：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 默认配置 Host * User your_username Port 22 IdentityFile ~/.ssh/id_ed25519 ForwardAgent no ServerAliveInterval 60 ServerAliveCountMax 3 # 特定主机配置 Host example HostName example.com User example_user Port 2222 IdentityFile ~/.ssh/id_ed25519_example ForwardAgent yes ControlMaster auto ControlPath ~/.ssh/sockets/%r@%h:%p ControlPersist yes # 使用通配符匹配多个主机 Host *.example.com User wildcard_user IdentityFile ~/.ssh/id_ed25519_wildcard 服务端配置文件 # 服务端配置文件是/etc/ssh/sshd_config（Linux和macOS）或C:\\ProgramData\\ssh\\sshd_config（Windows）。服务端配置文件的常用配置项包括：\nPort：指定SSH服务端口，默认是22。 ListenAddress：指定SSH服务监听的IP地址，默认是所有地址。 PermitRootLogin：是否允许root用户登录，默认是prohibit-password（禁止密码登录，但允许密钥登录）。 PasswordAuthentication：是否允许密码登录，默认是yes。 PubkeyAuthentication：是否允许公钥认证，默认是yes。 ChallengeResponseAuthentication：是否启用挑战响应认证，默认是no。 UsePAM：是否启用PAM认证，默认是yes。 AllowUsers：指定允许登录的用户列表，可以使用通配符*，默认是所有用户。 DenyUsers：指定禁止登录的用户列表，可以使用通配符*，默认是没有禁止用户。 AllowGroups：指定允许登录的用户组列表，可以使用通配符*，默认是所有用户组。 DenyGroups：指定禁止登录的用户组列表，可以使用通配符*，默认是没有禁止用户组。 MaxAuthTries：设置最大认证尝试次数，默认是6。 MaxSessions：设置最大会话数，默认是10。 ClientAliveInterval：设置客户端存活检测的时间间隔，单位是秒，默认是0（不检测）。 ClientAliveCountMax：设置客户端存活检测的最大次数，默认是3。 PermitTunnel：是否允许SSH隧道，默认是no。 X11Forwarding：是否允许X11转发，默认是no。 Subsystem：指定子系统配置，默认是subsystem sftp /usr/lib/openssh/sftp-server。 下面是一个示例的SSH服务端配置文件/etc/ssh/sshd_config:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # SSH服务端配置 Port 22 ListenAddress * PermitRootLogin prohibit-password PasswordAuthentication yes PubkeyAuthentication yes ChallengeResponseAuthentication no UsePAM yes AllowUsers user1 user2 DenyUsers user3 AllowGroups group1 group2 DenyGroups group3 MaxAuthTries 3 MaxSessions 10 ClientAliveInterval 60 ClientAliveCountMax 3 PermitTunnel no X11Forwarding yes Subsystem sftp /usr/lib/openssh/sftp-server SSH一些常用的功能 # 通常我们使用SSH都是远程连接到一台电脑（服务器），然后使用服务器的命令行工具来执行命令。但SSH还有很多其他的功能，比如端口转发、X11转发等。\n端口转发 # SSH可以将本地端口转发到远程服务器的端口，或者将远程服务器的端口转发到本地端口。这对于访问防火墙后面的服务非常有用。\n本地端口转发：\n1 ssh -L local_port:remote_host:remote_port user@remote_server 通过本地端口转发，你可以在本地访问远程服务器上的服务。例如，如果远程服务器上有一个Web服务运行在8080端口，你可以使用以下命令将其转发到本地的8080端口：\n```bash ssh -L 8080:localhost:8080 user@remote_server ``` 远程端口转发：\n1 ssh -R remote_port:local_host:local_port user@remote_server 通过远程端口转发，你可以让远程服务器访问本地的服务。例如，如果你在本地运行了一个Web服务，你可以使用以下命令将其转发到远程服务器的8080端口：\n```bash ssh -R 8080:localhost:8080 user@remote_server ``` 动态端口转发（类似于SOCKS代理）：\n1 ssh -D local_port user@remote_server 通过动态端口转发，你可以在本地创建一个SOCKS代理服务器，允许其他应用程序通过SSH连接到远程服务器。例如，你可以使用以下命令创建一个SOCKS代理服务器：\n```bash ssh -D 1080 user@remote_server ``` 然后在浏览器或其他应用程序中配置SOCKS代理，使用localhost:1080作为代理服务器地址。这样，你的浏览器或者配置了SOCKS代理的其他应用程序的流量就会走远程服务器的网络。一个有用的使用场景是，比如本地电脑在A国，远程服务器在B国，你可以通过SSH的动态端口转发功能，将本地的流量通过远程服务器转发到B国，这样就可以访问B国的网络资源。\nX11转发 # SSH可以将远程服务器的图形界面应用程序通过SSH隧道转发到本地电脑上，这样你就可以在本地运行远程服务器上的图形界面应用程序。\n注意：这里有一对容易混淆的概念：X服务端和X客户端。在X11协议中，X服务端是指有图形界面可以作为显示的电脑，而X客户端是指想要运行一个图形程序的电脑。通常情况下，我们在本地电脑上运行X服务端（比如Xming或VcXsrv），而在远程服务器上运行X客户端（图形界面应用程序），这样我们可以将远程服务器上运行的程序显示到本地电脑上。这跟SSH的服务端和客户端概念是相反的。\n要启用X11转发，你需要在SSH客户端和服务端都进行配置。\n在SSH服务端上\n确保sshd_config文件中有以下配置：\n1 2 X11Forwarding yes X11DisplayOffset 10 在SSH客户端上\n前置要求：\n确保本地电脑上安装了X11服务器软件。Linux通常自带了X11服务器，但在Windows和macOS上需要安装额外的软件。比如Windows上的Xming或VcXsrv，以及macOS上的XQuartz。\n确保SSH客户端支持X11转发。大部分Linux和macOS的SSH客户端都支持X11转发，但在Windows上的OpenSSH需要是8.0或更高版本（见GitHub上的PowerShell Issue #1515）。可以运行ssh -V命令来查看当前版本。\n确保本地电脑上设置了DISPLAY环境变量。\nWindows:\n如果用PowerShell，可以运行以下命令：\n1 $env:DISPLAY = \u0026#34;localhost:0.0\u0026#34; 如果用cmd，可以运行以下命令：\n1 set DISPLAY=localhost:0.0 Linux/macOS:\n在Linux和macOS上，通常不需要手动设置DISPLAY环境变量，因为SSH客户端会自动设置。\n但是如果需要手动设置，可以在终端中运行以下命令：\n1 export DISPLAY=localhost:0.0 连接远程服务器：\n在SSH客户端连接远程服务器时，需要使用-X或-Y选项启用X11转发：\n1 ssh -X user@remote_server 或者\n1 ssh -Y user@remote_server -X选项启用安全的X11转发，而-Y选项启用不安全的X11转发（允许更高权限的操作）。\n测试X11转发：\n启用X11转发后，你可以在远程服务器上运行图形界面应用程序，它们的窗口会显示在本地电脑上。例如，你可以在远程服务器上运行xclock命令来测试X11转发：\n1 xclock 如果一切设置正确，你应该可以在本地电脑上看到一个时钟窗口。\nVSCode远程开发 # VS Code刚发布时只是众多编辑器中的一个，跟文本编辑器比，不比NotePad++、Sublime Text之流强到哪里；跟IDE比，也不如专业的Visual Studio、IntelliJ IDEA等。其流行起来主要是因为两个杀手锏：一是插件，二是远程开发。\n我在VS Code刚发布时还同时使用Sublime Text、Visual Studio Community版、PyCharm等编辑器和IDE，但在VS Code支持远程开发后，我就彻底放弃了其他编辑器和IDE，完全转向了VS Code。虽然后来在学习Java Web开发时又用了一段时间的IntelliJ IDEA，但大部分情况下VS Code已经完全够用了。\n扯远了，但总之在我看来，VS Code最具革命性和吸引力的就是其原生支持远程开发，而且十分方便易用。\nVS Code安装后就自带了远程开发插件（Remote Development Extension Pack），其使用也非常简单：\n在本地电脑上打开VS Code，点击左侧活动栏中的“远程资源管理器”图标（一个电脑和一个箭头的图标）。 点击左上角的“连接到主机\u0026hellip;”按钮，输入远程服务器的SSH连接信息（格式为user@remote_server），然后按回车键。 如果是第一次连接，会提示你选择SSH配置文件，选择默认的用户级配置文件即可（~/.ssh/config或C:\\Users\\\u0026lt;YourUsername\u0026gt;\\.ssh\\config）。这个配置文件跟前面提到的SSH客户端配置文件是一样的，前面的配置同样会适用到这里。 连接成功后，VS Code会在远程服务器上安装一个VS Code服务器端组件，然后你就可以像在本地一样使用VS Code来编辑远程服务器上的文件了。 鸣谢 # 本文封面图来自IPXO。\n","date":"2025年8月11日","externalUrl":null,"permalink":"/p/ssh%E7%9B%B8%E5%85%B3-ssh%E5%88%B0windowsssh%E9%85%8D%E7%BD%AE%E5%85%8D%E5%AF%86%E7%A0%81%E7%99%BB%E5%BD%95vscode%E8%BF%9C%E7%A8%8B%E5%BC%80%E5%8F%91%E7%AD%89/","section":"Posts","summary":" 缘起 # SSH（Secure Shell）是非常常用的远程登录协议，我本身使用得已经非常多了，设置并不难，而且很多都是一劳永逸。但大部分设置基本都是每次换电脑或重装系统时才会搞，使用频率非常低，所以每次都要重新查找相关资料。\n","title":"SSH相关: ssh到Windows、ssh配置、免密码登录、VSCode远程开发等","type":"post"},{"content":"","date":"2025年8月11日","externalUrl":null,"permalink":"/tags/vscode/","section":"Tags","summary":"","title":"VSCode","type":"tags"},{"content":"","date":"2025年8月11日","externalUrl":null,"permalink":"/tags/%E8%BF%9C%E7%A8%8B%E5%BC%80%E5%8F%91/","section":"Tags","summary":"","title":"远程开发","type":"tags"},{"content":"","date":"2025年8月7日","externalUrl":null,"permalink":"/tags/headscale/","section":"Tags","summary":"","title":"Headscale","type":"tags"},{"content":"","date":"7 八月 2025","externalUrl":null,"permalink":"/en/tags/network/","section":"Tags","summary":"","title":"Network","type":"tags"},{"content":"","date":"7 八月 2025","externalUrl":null,"permalink":"/en/series/private-virtual-network-vpn-series/","section":"Series","summary":"","title":"Private Virtual Network (VPN) Series","type":"series"},{"content":"","date":"2025年8月7日","externalUrl":null,"permalink":"/tags/tailscale/","section":"Tags","summary":"","title":"Tailscale","type":"tags"},{"content":"","date":"2025年8月7日","externalUrl":null,"permalink":"/tags/vpn/","section":"Tags","summary":"","title":"VPN","type":"tags"},{"content":"","date":"2025年8月7日","externalUrl":null,"permalink":"/series/%E7%A7%81%E6%9C%89%E8%99%9A%E6%8B%9F%E7%BD%91%E7%BB%9Cvpn%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"私有虚拟网络VPN系列","type":"series"},{"content":"","date":"2025年8月7日","externalUrl":null,"permalink":"/tags/%E7%BD%91%E7%BB%9C/","section":"Tags","summary":"","title":"网络","type":"tags"},{"content":" 缘起 # 本文是《虚拟私有网络VPN》系列的第3篇。在前两篇文章中，我介绍了VPN的基础概念和使用场景，以及如何使用Tailscale来搭建一个私有虚拟网络。本系列的其他文章参见：\n私有虚拟网络VPN（一）：在云服务器上用Shadowsocks搭建VPN 私有虚拟网络VPN（二）：基础概念和使用Tailscale搭建指南 在上一篇文章中，我介绍了如何使用Tailscale来搭建一个私有虚拟网络。Tailscale是一个基于WireGuard的VPN解决方案，它可以帮助我们快速地搭建一个私有虚拟网络。但是官方版的Tailscale使用的是Tailscale的控制服务器，这意味着我们的数据和网络流量会经过Tailscale的服务器。虽然这些数据和流量已经经过加密，但如果我们希望完全控制自己的网络环境，或者出于隐私和安全的考虑，我们可以自己来搭建一个Tailscale的控制服务器。来实现这个功能的工具就是Headscale。\n前提 # 了解容器和Docker的基本使用 了解反向代理的基本概念 有一个固定的IP地址（这个很重要，之前的文章中提到如果没有固定IP，可以使用Cloudflare Tunnel来做反向代理，但是Cloudflare Tunnel无法代理Headscale的服务。后文会说这个问题，但我并没有解决方案。） Headscale # Headscale简介 # Headscale是一个开源的Tailscale控制服务器实现，它允许用户在自己的基础设施上托管Tailscale的控制平面。通过使用Headscale，用户可以完全控制自己的VPN环境，而无需依赖Tailscale的官方服务器。其GitHub地址是：headscale。\n另外，为了方便用户管理Headscale，开发者开发了不少能够与Headscale集成的Web界面工具，详情见：headscale-web。我只尝试了headplane和headscale-ui。headscale-ui没能成功运行，而headplane直接就部署成功了。它们的功能应该都差不多，我也懒得去探索为啥headscale-ui没能成功运行了，就直接用headplane了。\nHeadscale的工作原理 # Headscale通过实现Tailscale的控制协议，充当Tailscale客户端和服务器之间的中介。用户可以将Tailscale客户端配置为使用Headscale作为控制服务器，从而实现对VPN连接的完全控制。Headscale支持Tailscale的所有主要功能，包括身份验证、设备注册和网络配置。\n使用Docker部署Headscale和Headplane # 这里我们将Headscale和Headplane两个服务部署在同一个Docker容器中。Headscale和Headplane都需要配置文件，我们分别为它们创建存放配置文件的目录，于是整个项目的目录结构如下：\n1 2 3 4 5 6 7 headscale/ ├── headplane/ │ └── config.yaml ├── headscale/ │ └── config.yaml └── docker-compose.yaml └── .env 接下来我们就来创建这些文件。\n1. Docker Compose文件 # 创建docker-compose.yaml文件，内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 version: \u0026#39;3.5\u0026#39; services: headscale: image: headscale/headscale:v0.26 container_name: headscale volumes: - ./headscale/config:/etc/headscale - ${DATA_DIR}/headscale/data:/var/lib/headscale - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro command: serve restart: unless-stopped headplane: container_name: headplane image: ghcr.io/tale/headplane:0.6.0 restart: unless-stopped volumes: - \u0026#39;./headplane/config/config.yaml:/etc/headplane/config.yaml\u0026#39; - \u0026#39;./headscale/config/config.yaml:/etc/headscale/config.yaml\u0026#39; - \u0026#39;${DATA_DIR}/headplane/data:/var/lib/headplane\u0026#39; - \u0026#39;/var/run/docker.sock:/var/run/docker.sock:ro\u0026#39; 注意：\n这里的${DATA_DIR}是一个环境变量，你可以在.env文件中定义它。这个变量用于指定数据存储的目录。下面是一个示例的.env文件内容：\n1 DATA_DIR=/path/to/your/data headscale服务只需要读取headscale的配置文件，而headplane服务需要同时读取headscale和headplane的配置文件。\n2. Headscale的配置 # Headscale的配置文件位于./headscale/config/config.yaml，你可以根据需要进行修改。以下是一个示例配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 --- # headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order: # # - `/etc/headscale` # - `~/.headscale` # - current working directory # The url clients will connect to. # Typically this will be a domain like: # # https://myheadscale.example.com:443 # server_url: https://\u0026lt;headscale.example.com\u0026gt; # Address to listen to / bind to on the server # # For production: listen_addr: 0.0.0.0:8080 #listen_addr: 127.0.0.1:8080 # Address to listen to /metrics, you may want # to keep this endpoint private to your internal # network # metrics_listen_addr: 127.0.0.1:9090 # Address to listen for gRPC. # gRPC is used for controlling a headscale server # remotely with the CLI # Note: Remote access _only_ works if you have # valid certificates. # # For production: grpc_listen_addr: 0.0.0.0:50443 #grpc_listen_addr: 127.0.0.1:50443 # Allow the gRPC admin interface to run in INSECURE # mode. This is not recommended as the traffic will # be unencrypted. Only enable if you know what you # are doing. grpc_allow_insecure: false # The Noise section includes specific configuration for the # TS2021 Noise protocol noise: # The Noise private key is used to encrypt the # traffic between headscale and Tailscale clients when # using the new Noise-based protocol. private_key_path: /var/lib/headscale/noise_private.key # List of IP prefixes to allocate tailaddresses from. # Each prefix consists of either an IPv4 or IPv6 address, # and the associated prefix length, delimited by a slash. # It must be within IP ranges supported by the Tailscale # client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48. # See below: # IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71 # IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33 # Any other range is NOT supported, and it will cause unexpected issues. prefixes: v4: 100.64.0.0/10 v6: fd7a:115c:a1e0::/48 # Strategy used for allocation of IPs to nodes, available options: # - sequential (default): assigns the next free IP from the previous given IP. # - random: assigns the next free IP from a pseudo-random IP generator (crypto/rand). allocation: sequential # DERP is a relay system that Tailscale uses when a direct # connection cannot be established. # https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp # # headscale needs a list of DERP servers that can be presented # to the clients. derp: server: # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place enabled: false # Region ID to use for the embedded DERP server. # The local DERP prevails if the region ID collides with other region ID coming from # the regular DERP config. region_id: 999 # Region code and name are displayed in the Tailscale UI to identify a DERP region region_code: \u0026#34;headscale\u0026#34; region_name: \u0026#34;Headscale Embedded DERP\u0026#34; # Listens over UDP at the configured address for STUN connections - to help with NAT traversal. # When the embedded DERP server is enabled stun_listen_addr MUST be defined. # # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/ stun_listen_addr: \u0026#34;0.0.0.0:3478\u0026#34; # Private key used to encrypt the traffic between headscale DERP # and Tailscale clients. # The private key file will be autogenerated if it\u0026#39;s missing. # private_key_path: /var/lib/headscale/derp_server_private.key # This flag can be used, so the DERP map entry for the embedded DERP server is not written automatically, # it enables the creation of your very own DERP map entry using a locally available file with the parameter DERP.paths # If you enable the DERP server and set this to false, it is required to add the DERP server to the DERP map using DERP.paths automatically_add_embedded_derp_region: true # For better connection stability (especially when using an Exit-Node and DNS is not working), # it is possible to optionally add the public IPv4 and IPv6 address to the Derp-Map using: ipv4: 1.2.3.4 ipv6: 2001:db8::1 # List of externally available DERP maps encoded in JSON urls: - https://controlplane.tailscale.com/derpmap/default # Locally available DERP map files encoded in YAML # # This option is mostly interesting for people hosting # their own DERP servers: # https://tailscale.com/kb/1118/custom-derp-servers/ # # paths: # - /etc/headscale/derp-example.yaml paths: [] # If enabled, a worker will be set up to periodically # refresh the given sources and update the derpmap # will be set up. auto_update_enabled: true # How often should we check for DERP updates? update_frequency: 24h # Disables the automatic check for headscale updates on startup disable_check_updates: false # Time before an inactive ephemeral node is deleted? ephemeral_node_inactivity_timeout: 30m database: # Database type. Available options: sqlite, postgres # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. # All new development, testing and optimisations are done with SQLite in mind. type: sqlite # Enable debug mode. This setting requires the log.level to be set to \u0026#34;debug\u0026#34; or \u0026#34;trace\u0026#34;. debug: false # GORM configuration settings. gorm: # Enable prepared statements. prepare_stmt: true # Enable parameterized queries. parameterized_queries: true # Skip logging \u0026#34;record not found\u0026#34; errors. skip_err_record_not_found: true # Threshold for slow queries in milliseconds. slow_threshold: 1000 # SQLite config sqlite: path: /var/lib/headscale/db.sqlite # Enable WAL mode for SQLite. This is recommended for production environments. # https://www.sqlite.org/wal.html write_ahead_log: true # Maximum number of WAL file frames before the WAL file is automatically checkpointed. # https://www.sqlite.org/c3ref/wal_autocheckpoint.html # Set to 0 to disable automatic checkpointing. wal_autocheckpoint: 1000 # # Postgres config # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. # See database.type for more information. # postgres: # # If using a Unix socket to connect to Postgres, set the socket path in the \u0026#39;host\u0026#39; field and leave \u0026#39;port\u0026#39; blank. # host: localhost # port: 5432 # name: headscale # user: foo # pass: bar # max_open_conns: 10 # max_idle_conns: 10 # conn_max_idle_time_secs: 3600 # # If other \u0026#39;sslmode\u0026#39; is required instead of \u0026#39;require(true)\u0026#39; and \u0026#39;disabled(false)\u0026#39;, set the \u0026#39;sslmode\u0026#39; you need # # in the \u0026#39;ssl\u0026#39; field. Refers to https://www.postgresql.org/docs/current/libpq-ssl.html Table 34.1. # ssl: false ### TLS configuration # ## Let\u0026#39;s encrypt / ACME # # headscale supports automatically requesting and setting up # TLS for a domain with Let\u0026#39;s Encrypt. # # URL to ACME directory acme_url: https://acme-v02.api.letsencrypt.org/directory # Email to register with ACME provider acme_email: \u0026#34;\u0026#34; # Domain name to request a TLS certificate for: tls_letsencrypt_hostname: \u0026#34;\u0026#34; # Path to store certificates and metadata needed by # letsencrypt # For production: tls_letsencrypt_cache_dir: /var/lib/headscale/cache # Type of ACME challenge to use, currently supported types: # HTTP-01 or TLS-ALPN-01 # See: docs/ref/tls.md for more information tls_letsencrypt_challenge_type: HTTP-01 # When HTTP-01 challenge is chosen, letsencrypt must set up a # verification endpoint, and it will be listening on: # :http = port 80 tls_letsencrypt_listen: \u0026#34;:http\u0026#34; ## Use already defined certificates: tls_cert_path: \u0026#34;\u0026#34; tls_key_path: \u0026#34;\u0026#34; log: # Output formatting for logs: text or json format: text level: info ## Policy # headscale supports Tailscale\u0026#39;s ACL policies. # Please have a look to their KB to better # understand the concepts: https://tailscale.com/kb/1018/acls/ policy: # The mode can be \u0026#34;file\u0026#34; or \u0026#34;database\u0026#34; that defines # where the ACL policies are stored and read from. mode: file # If the mode is set to \u0026#34;file\u0026#34;, the path to a # HuJSON file containing ACL policies. path: \u0026#34;\u0026#34; ## DNS # # headscale supports Tailscale\u0026#39;s DNS configuration and MagicDNS. # Please have a look to their KB to better understand the concepts: # # - https://tailscale.com/kb/1054/dns/ # - https://tailscale.com/kb/1081/magicdns/ # - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ # # Please note that for the DNS configuration to have any effect, # clients must have the `--accept-dns=true` option enabled. This is the # default for the Tailscale client. This option is enabled by default # in the Tailscale client. # # Setting _any_ of the configuration and `--accept-dns=true` on the # clients will integrate with the DNS manager on the client or # overwrite /etc/resolv.conf. # https://tailscale.com/kb/1235/resolv-conf # # If you want stop Headscale from managing the DNS configuration # all the fields under `dns` should be set to empty values. dns: # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). magic_dns: true # Defines the base domain to create the hostnames for MagicDNS. # This domain _must_ be different from the server_url domain. # `base_domain` must be a FQDN, without the trailing dot. # The FQDN of the hosts will be # `hostname.base_domain` (e.g., _myhost.example.com_). base_domain: \u0026lt;hs.example.com\u0026gt; # List of DNS servers to expose to clients. nameservers: global: - 1.1.1.1 - 1.0.0.1 - 2606:4700:4700::1111 - 2606:4700:4700::1001 # NextDNS (see https://tailscale.com/kb/1218/nextdns/). # \u0026#34;abc123\u0026#34; is example NextDNS ID, replace with yours. # - https://dns.nextdns.io/abc123 # Split DNS (see https://tailscale.com/kb/1054/dns/), # a map of domains and which DNS server to use for each. split: {} # foo.bar.com: # - 1.1.1.1 # darp.headscale.net: # - 1.1.1.1 # - 8.8.8.8 # Set custom DNS search domains. With MagicDNS enabled, # your tailnet base_domain is always the first search domain. search_domains: [] # Extra DNS records # so far only A and AAAA records are supported (on the tailscale side) # See: docs/ref/dns.md extra_records: [] # - name: \u0026#34;grafana.myvpn.example.com\u0026#34; # type: \u0026#34;A\u0026#34; # value: \u0026#34;100.64.0.3\u0026#34; # # # you can also put it in one line # - { name: \u0026#34;prometheus.myvpn.example.com\u0026#34;, type: \u0026#34;A\u0026#34;, value: \u0026#34;100.64.0.3\u0026#34; } # # Alternatively, extra DNS records can be loaded from a JSON file. # Headscale processes this file on each change. # extra_records_path: /var/lib/headscale/extra-records.json # Unix socket used for the CLI to connect without authentication # Note: for production you will want to set this to something like: unix_socket: /var/run/headscale/headscale.sock unix_socket_permission: \u0026#34;0770\u0026#34; # # headscale supports experimental OpenID connect support, # it is still being tested and might have some bugs, please # help us test it. # OpenID Connect oidc: only_start_if_oidc_is_available: true issuer: \u0026#34;https://your-oidc.issuer.com/path\u0026#34; client_id: \u0026#34;your-oidc-client-id\u0026#34; client_secret: \u0026#34;your-oidc-client-secret\u0026#34; # Alternatively, set `client_secret_path` to read the secret from the file. # It resolves environment variables, making integration to systemd\u0026#39;s # `LoadCredential` straightforward: # client_secret_path: \u0026#34;${CREDENTIALS_DIRECTORY}/oidc_client_secret\u0026#34; # # client_secret and client_secret_path are mutually exclusive. # The amount of time from a node is authenticated with OpenID until it # expires and needs to reauthenticate. # Setting the value to \u0026#34;0\u0026#34; will mean no expiry. expiry: 180d # Use the expiry from the token received from OpenID when the user logged # in, this will typically lead to frequent need to reauthenticate and should # only been enabled if you know what you are doing. # Note: enabling this will cause `oidc.expiry` to be ignored. use_expiry_from_token: false # Customize the scopes used in the OIDC flow, defaults to \u0026#34;openid\u0026#34;, \u0026#34;profile\u0026#34; and \u0026#34;email\u0026#34; and add custom query # parameters to the Authorize Endpoint request. Scopes default to \u0026#34;openid\u0026#34;, \u0026#34;profile\u0026#34; and \u0026#34;email\u0026#34;. scope: [\u0026#34;openid\u0026#34;, \u0026#34;profile\u0026#34;, \u0026#34;email\u0026#34;] # extra_params: # domain_hint: example.com # # # List allowed principal domains and/or users. If an authenticated user\u0026#39;s domain is not in this list, the # # authentication request will be rejected. # # allowed_domains: # - example.com # # Note: Groups from keycloak have a leading \u0026#39;/\u0026#39; # allowed_groups: # - /headscale # allowed_users: # - alice@example.com # # # Optional: PKCE (Proof Key for Code Exchange) configuration # # PKCE adds an additional layer of security to the OAuth 2.0 authorization code flow # # by preventing authorization code interception attacks # # See https://datatracker.ietf.org/doc/html/rfc7636 # pkce: # # Enable or disable PKCE support (default: false) # enabled: false # # PKCE method to use: # # - plain: Use plain code verifier # # - S256: Use SHA256 hashed code verifier (default, recommended) # method: S256 # # # Map legacy users from pre-0.24.0 versions of headscale to the new OIDC users # # by taking the username from the legacy user and matching it with the username # # provided by the OIDC. This is useful when migrating from legacy users to OIDC # # to force them using the unique identifier from the OIDC and to give them a # # proper display name and picture if available. # # Note that this will only work if the username from the legacy user is the same # # and there is a possibility for account takeover should a username have changed # # with the provider. # # When this feature is disabled, it will cause all new logins to be created as new users. # # Note this option will be removed in the future and should be set to false # # on all new installations, or when all users have logged in with OIDC once. # map_legacy_users: false # Logtail configuration # Logtail is Tailscales logging and auditing infrastructure, it allows the control panel # to instruct tailscale nodes to log their activity to a remote server. logtail: # Enable logtail for this headscales clients. # As there is currently no support for overriding the log server in headscale, this is # disabled by default. Enabling this will make your clients send logs to Tailscale Inc. enabled: false # Enabling this option makes devices prefer a random port for WireGuard traffic over the # default static port 41641. This option is intended as a workaround for some buggy # firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information. randomize_client_port: false 3. Headplane的配置 # Headplane的配置文件位于./headplane/config/config.yaml，你可以根据需要进行修改。以下是一个示例配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 server: host: \u0026#34;0.0.0.0\u0026#34; port: 3000 cookie_secret: \u0026#34;32-character-long-random-string\u0026#34; cookie_secure: false headscale: url: \u0026#34;https://\u0026lt;headscale.example.com\u0026gt;\u0026#34; config_path: \u0026#34;/etc/headscale/config.yaml\u0026#34; config_strict: true integration: docker: enabled: true container_name: \u0026#34;headscale\u0026#34; socket: \u0026#34;unix:///var/run/docker.sock\u0026#34; oidc: issuer: \u0026#34;https://\u0026lt;authelia.example.com\u0026gt;\u0026#34; client_id: \u0026#34;headplane\u0026#34; # The client secret for the OIDC client # Either this or `client_secret_path` must be set for OIDC to work client_secret: \u0026#34;some-random-client-secret\u0026#34; # You can alternatively set `client_secret_path` to read the secret from disk. # The path specified can resolve environment variables, making integration # with systemd\u0026#39;s `LoadCredential` straightforward: # client_secret_path: \u0026#34;${CREDENTIALS_DIRECTORY}/oidc_client_secret\u0026#34; disable_api_key_login: false token_endpoint_auth_method: \u0026#34;client_secret_post\u0026#34; # If you are using OIDC, you need to generate an API key # that can be used to authenticate other sessions when signing in. # # This can be done with `headscale apikeys create --expiration 999d` headscale_api_key: \u0026#34;your-headscale-api-key\u0026#34; # Optional, but highly recommended otherwise Headplane # will attempt to automatically guess this from the issuer # # This should point to your publicly accessibly URL # for your Headplane instance with /admin/oidc/callback redirect_uri: \u0026#39;https://\u0026lt;url-to-your-headplane\u0026gt;/admin/oidc/callback\u0026#39; # Stores the users and their permissions for Headplane # This is a path to a JSON file, default is specified below. user_storage_file: \u0026#34;/var/lib/headplane/users.json\u0026#34; 集成Authelia进行身份验证 # 上述配置中，Headplane使用了OIDC（OpenID Connect）来进行身份验证。你可以使用Authelia作为OIDC提供者。Authelia的配置文件通常位于/etc/authelia/configuration.yml，你需要在其中添加一个OIDC客户端配置。以下是一个示例配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 identity_providers: oidc: clients: - client_id: \u0026#39;headplane\u0026#39; client_name: \u0026#39;Headplane OIDC Client\u0026#39; client_secret: \u0026#39;client-secret\u0026#39; public: false authorization_policy: one_factor redirect_uris: - \u0026#39;https://example.com/oidc/callback\u0026#39; scopes: - openid - profile - email grant_types: - authorization_code response_types: - code token_endpoint_auth_method: client_secret_post 确保这里的client_id、client_secret和redirect_uris与你在Headplane配置中的设置一致。\n启动服务 # 在创建好上述文件后，你可以使用以下命令来启动Headscale和Headplane服务：\n1 docker-compose up -d 如果一切正常，你应该能够通过浏览器访问Headplane的Web界面，地址为http://\u0026lt;your-server-ip\u0026gt;:3000/admin/（注意，在地址后的/admin/非常重要，如果不带这个路径，你会看到一个404错误页面）。你会看到如下界面：\n如果你没有配置OIDC身份验证，那么你在上述页面中不会看到Single Sign On选项。你可以直接使用Headscale的API密钥进行登录。你可以通过Headscale容器的命令行界面创建API密钥，命令如下：\n1 docker exec -it headscale headscale apikeys create --expiration 999d 这将生成一个API密钥，你可以在Headplane的登录界面中使用这个密钥进行登录。\n如果你配置了OIDC身份验证，你可以在登录界面中选择Single Sign On选项，会跳转到OIDC提供者的登录页面（例如Authelia）：\n然后点击确认授权登录：\n你就可以看到Headplane的管理界面了：\n这个界面跟Tailscale的Web界面非常相似，你可以在这里管理你的Headscale服务器、查看连接的设备、创建预授权密钥等。\n使用Tailscale连接到Headscale # Tailscale的客户端默认会连接到Tailscale的官方控制服务器，但我们可以将其配置为连接到我们自己的Headscale服务器。在macOS或者安卓手机上，你可以在Tailscale的设置中找到“控制服务器”选项，将其设置为你的Headscale服务器地址，例如https://\u0026lt;headscale.example.com\u0026gt;。\n但在Windows上，我似乎没有找到这个选项。我们可以通过命令行来设置Tailscale的控制服务器。打开命令提示符或者PowerShell，运行以下命令：\n1 tailscale up --login-server https://\u0026lt;headscale.example.com\u0026gt; 如果你的Headscale服务运行正常且部署了身份验证，运行上述命令后，你会看到输出了一个URL链接地址，在浏览器中打开这个链接，你就会看到身份验证的登录界面。登录后，你会看到一个授权页面，点击“授权”按钮，Tailscale客户端就会连接到你的Headscale服务器。\n如果你没有配置OIDC身份验证，你可以通过提供preauthkeys来进行登录。首先，你需要在Headscale容器中创建一个预授权密钥，命令如下：\n1 docker exec -it headscale headscale preauthkeys create --reusable --user your_user --expiration=72h 这将生成一个预授权密钥，你可以在Tailscale客户端中使用这个密钥进行登录。运行以下命令：\n1 tailscale up --authkey \u0026lt;your_preauth_key\u0026gt; 问题解决 # 1. 使用Nginx作为反向代理时的问题 # 使用如下命令查看Tailscale日志：\n1 journalctl -u tailscaled -f 得到如下输出：\n1 2 3 4 5 6 7 Apr 30 20:34:36 fedora tailscaled[1058]: control: LoginInteractive -\u0026gt; regen=true Apr 30 20:34:36 fedora tailscaled[1058]: control: doLogin(regen=true, hasUrl=false) Apr 30 20:34:36 fedora tailscaled[1058]: control: control server key from https://\u0026lt;headscale domain name\u0026gt;: ts2021=[lx/4O], legacy= Apr 30 20:34:36 fedora tailscaled[1058]: control: Generating a new nodekey. Apr 30 20:34:36 fedora tailscaled[1058]: control: RegisterReq: onode= node=[W5uIA] fup=false nks=false Apr 30 20:34:36 fedora tailscaled[1058]: control: controlhttp: forcing port 443 dial due to recent noise dial Apr 30 20:34:46 fedora tailscaled[1058]: Received error: register request: Post \u0026#34;https://\u0026lt;headscale domain name\u0026gt;/machine/register\u0026#34;: connection attempts aborted by context: context deadline exceeded 这说明Tailscale在向Headscale发送注册请求POST时，连接超时了。\n然后我们检查Headscale docker容器的日志。可以在Portainer中查看，也可以使用以下命令：\n1 docker logs \u0026lt;headscale_container_name\u0026gt; 发现里面一直在输出下述信息：\n1 2025-04-30T21:45:45-04:00 WRN home/runner/work/headscale/headscale/hscontrol/noise.go:66 \u0026gt; No Upgrade header in TS2021 request. If headscale is behind a reverse proxy, make sure it is configured to pass WebSockets through. 我最开始以为这只是一个警告，众所周知，程序员是看不到警告的，所以我一直忽略了这条信息。直到在我排查了所有可能的错误之后，我才想着在网上搜一下这个警告。不搜不知道，一搜才发现这条警告正是导致Tailscale无法连接到Headscale的元凶。我真想问候这个程序员的家人，这么重要的信息怎么能归到警告里去呢？！\n总之，这个警告的意思是说，如果Headscale部署在反向代理后面，需要确保反向代理配置正确，以便通过WebSockets进行通信。这里的配置是指在反向代理里要添加Upgrade头。如果你使用的是Nginx作为反向代理，可以在Nginx的配置文件中添加以下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 location / { proxy_pass http://\u0026lt;headscale_ip\u0026gt;:\u0026lt;headscale_port\u0026gt;; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \u0026#34;upgrade\u0026#34;; 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 Upgrade $http_upgrade;和proxy_set_header Connection \u0026quot;upgrade\u0026quot;;这两行，它们确保了WebSockets的连接可以正确地升级。\n2. 使用Cloudflare Tunnel作为反向代理时的问题 # 之前考虑到固定IP不太好获取，所以我一直在尝试将之前的docker服务通过Cloudflare Tunnel来进行反向代理。详见下列文章：\n“动态域名解析\u0026ndash;Cloudflare”。 “从公网访问个人网站——Nginx反向代理配置”。 “从公网访问个人网站（二）——Traefik反向代理配置”。 “从公网访问个人网站（三）——使用Cloudflare Tunnel反向代理Docker容器”。 因此在部署Headscale时，我也尝试使用Cloudflare Tunnel来进行反向代理。按照之前的文章配置好Cloudflare Tunnel之后，我发现Headscale的Web界面Headplane可以正常访问，但是Tailscale客户端却无法连接到Headscale。运行Tailscale登录命令时会卡住，查看Tailscale日志时发现如下错误：\n1 2 3 4 5 6 7 Jun 24 15:29:02 fedora.attlocal.net tailscaled[885490]: Received error: register request: Post \u0026#34;https://headscale.example.com/machine/register\u0026#34;: connection attempts aborted by context: context deadline exceeded Jun 24 15:29:02 fedora.attlocal.net tailscaled[885490]: control: LoginInteractive -\u0026gt; regen=true Jun 24 15:29:02 fedora.attlocal.net tailscaled[885490]: control: doLogin(regen=true, hasUrl=false) Jun 24 15:29:02 fedora.attlocal.net tailscaled[885490]: control: control server key from https://headscale.example.com: ts2021=[lx/4O], legacy= Jun 24 15:29:02 fedora.attlocal.net tailscaled[885490]: control: Generating a new nodekey. Jun 24 15:29:02 fedora.attlocal.net tailscaled[885490]: control: RegisterReq: onode= node=[yFWNe] fup=false nks=false Jun 24 15:29:02 fedora.attlocal.net tailscaled[885490]: control: controlhttp: forcing port 443 dial due to recent noise dial Headscale容器的日志中有如下错误：\n1 2025-06-24T17:53:20Z WRN home/runner/work/headscale/headscale/hscontrol/noise.go:66 \u0026gt; No Upgrade header in TS2021 request. If headscale is behind a reverse proxy, make sure it is configured to pass WebSockets through. 我在这个问题上得耗了有一两周的时间，尝试了各种方法，最终还是没能解决。\n最后ChatGPT帮我分析了产生这个问题的原因，它认为这是遇到了免费版的Cloudflare Tunnel的限制。免费版的Cloudflare Tunnel只支持代理HTTP/1.1(+WebSocket upgrades)和HTTP/2+WebSocket流量。而Headscale报错说的TS2021 Noise handshake是通过HTTP2的纯gRPC流量进行的，因此无法通过Cloudflare Tunnel的免费版进行代理。\n不过ChatGPT也提到，Cloudflare的付费版Tunnel支持HTTP/3和gRPC流量，因此如果你有Cloudflare的付费版账号，可以尝试使用付费版的Tunnel来进行代理。但我是放弃了。\n这个问题在Cloudflare Tunnel GitHub仓库里也有人提Issue讨论，地址是：General Upgrade header support #990. 总之，我目前的结论就是Cloudflare Tunnel无法代理Headscale的服务。\nUpdate 2025-11-17：在上面那个Issue里，最近有个叫[Sharpie]的用户(https://github.com/Sharpie)提到可以使用Ngrok，我暂时还没试这个方案，不知道是不是可以解决这个问题。具体可参见：https://github.com/cloudflare/cloudflared/issues/990#issuecomment-3448903844。\n","date":"2025年8月7日","externalUrl":null,"permalink":"/p/%E8%99%9A%E6%8B%9F%E7%A7%81%E6%9C%89%E7%BD%91%E7%BB%9C%E4%B8%89%E4%BD%BF%E7%94%A8headscale%E5%92%8Ctailscale%E6%90%AD%E5%BB%BAvpn/","section":"Posts","summary":" 缘起 # 本文是《虚拟私有网络VPN》系列的第3篇。在前两篇文章中，我介绍了VPN的基础概念和使用场景，以及如何使用Tailscale来搭建一个私有虚拟网络。本系列的其他文章参见：\n","title":"虚拟私有网络（三）：使用Headscale和Tailscale搭建VPN","type":"post"},{"content":"","date":"21 七月 2025","externalUrl":null,"permalink":"/en/tags/notes/","section":"Tags","summary":"","title":"Notes","type":"tags"},{"content":"","date":"2025年7月21日","externalUrl":null,"permalink":"/tags/obsidian/","section":"Tags","summary":"","title":"Obsidian","type":"tags"},{"content":"","date":"21 七月 2025","externalUrl":null,"permalink":"/en/series/obsidian-series/","section":"Series","summary":"","title":"Obsidian Series","type":"series"},{"content":" 缘起 # 在上一篇文章中，我介绍了如何使用Obsidian和Nextcloud来管理笔记。上一篇文章的标题中使用了“全平台”这个词，是说Obsidian支持Windows、Linux、Mac、Android等多个平台。一般来说，这已经够用了，但是在某些场景下却依然不太方便。\n比如某些单位的电脑不允许随意安装软件，Obsidian就无法在单位电脑上使用了。要想真正做到全平台，我觉得还是需要有个网页版。于是我在网上搜索了一番，还真发现有人已经实现了网页版的Obsidian，于是我就尝试了一下。客观地说，网页版的Obsidian还不太成熟，略显粗糙，但基本功能已经可以了。因此我就把它部署在了自己的服务器上，以供不时之需。\n本系列介绍Obsidian的其他文章参见：\n全平台笔记管理软件Obsidian 前置条件 # 基本和部署各种容器服务所需的前置条件一样：\n一台可作为服务器的电脑 docker和docker compose Obsidian Docker版 # Obsidian Docker版简介 # Obsidian的网页版是一个由linuxserver.io开发的开源项目，基于Obsidian的核心功能，提供了一个Web界面。它可以通过Docker容器运行，非常适合在服务器上部署。其GitHub地址是：docker-obsidian.\ndocker-obsidian使用KasmVNC作为Web界面，KasmVNC是一个基于VNC的Web桌面解决方案。它允许用户通过浏览器访问Obsidian的桌面环境。\ndocker-obsidian包括了一些可定制化的选项，比如支持HTTP基本认证，即用户在部署容器时可以设置一个用户名和密码来保护Obsidian的访问。\n部署Obsidian Docker # 项目结构\n跟之前部署容器类似，我们使用docker compose来部署Obsidian。首先，创建一个目录来存放配置文件和数据：\n1 2 mkdir -p ~/docker/obsidian cd ~/docker/obsidian 在此目录下我们创建两个文件：docker-compose.yml和.env。\ndocker-compose.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 services: obsidian: image: lscr.io/linuxserver/obsidian:latest container_name: obsidian security_opt: - seccomp:unconfined #optional environment: - PUID=1000 - PGID=1000 - TZ=America/New_York - DOCKER_MODS=linuxserver/mods:universal-package-install - CUSTOM_USER=${CUSTOM_USER} - PASSWORD=${PASSWORD} volumes: - ${APP_DIR}/config:/config shm_size: \u0026#34;1gb\u0026#34; restart: unless-stopped networks: traefik-net: aliases: - obsidian .env\n1 2 3 APP_DIR=/media/user/docker_data/obsidian CUSTOM_USER=username PASSWORD=\u0026#39;your_password_here\u0026#39; 其中，APP_DIR是Obsidian的配置文件和数据存储目录，CUSTOM_USER是你希望使用的用户名，PASSWORD是访问Obsidian的密码。\n启动容器\n1 docker compose up -d 运行上述命令后，Docker会自动下载Obsidian镜像并启动容器。docker-obsidian默认使用的端口是3000，你可以通过浏览器访问http://your_server_ip:3000来访问Obsidian。\n反向代理\n为容器做反向代理以便通过域名访问有多种方式，我之前写过使用Nginx、Traefik以及Cloudflare Tunnel做反向代理的文章，具体可以参考：\n\u0026ldquo;从公网访问个人网站——Nginx反向代理配置\u0026rdquo; \u0026ldquo;从公网访问个人网站（二）——Traefik反向代理配置\u0026rdquo; \u0026ldquo;从公网访问个人网站（三）——使用Cloudflare Tunnel反向代理Docker容器\u0026rdquo; docker-obsidian的初始配置 # 如果上述步骤都没问题，你应该可以通过浏览器访问Obsidian的Web界面了。\n首次访问，你会看到一个登录验证弹窗\n输入你在.env文件中设置的用户名和密码。\n登录后，你会看到Obsidian的Web界面：\n这个界面和Obsidian的桌面版一样，只不过你还会看到KasmVNC的工具栏在顶部和侧边。\n同时浏览器还会出现一个弹窗，提示你授权访问剪切板：\n点击“允许”以便在Obsidian中使用剪切板功能。否则你在其他地方复制的内容将无法粘贴到Obsidian网页版中，反之亦然。\n然后初始化Obsidian就行了，注意笔记仓库的名字要和你之前在其他Obsidian客户端中使用的名字一致，这样才能同步。其他的设置如开启插件、设置云同步等都和桌面版Obsidian一样，可参加我之前的文章：全平台笔记管理软件Obsidian。\n设置完成后就可以开始使用Obsidian了：\n你可以在浏览器中使用Obsidian的所有功能，包括编辑笔记、使用插件等。\n粘贴在别处复制的内容似乎不能直接粘贴进Obsidian中，需要通过KasmVNC的剪切板功能来实现：\n小问题及解决 # 我发现Obsidian的网页版无法显示汉字：\n解决方案是让容器在启动时安装可以显示汉字的字体及设置LC_ALL环境变量。\n在docker-compose.yml中添加以下环境变量即可：\n1 2 3 environment: - INSTALL_PACKAGES=fonts-noto-cjk - LC_ALL=zh_CN.UTF-8 然后重新启动容器：\n1 2 docker compose down docker compose up -d 现在Obsidian的网页版应该可以正常显示汉字了：\n总结 # Obsidian的网页版虽然还不够成熟，但基本功能已经可以满足日常使用。通过Docker容器部署Obsidian的Web版，可以方便地在服务器上运行，并通过浏览器访问。这样才算真正实现了全平台的笔记管理。\n","date":"2025年7月21日","externalUrl":null,"permalink":"/p/obsidian%E4%BA%8C%E7%BD%91%E9%A1%B5%E7%89%88obsidian%E7%9A%84%E9%83%A8%E7%BD%B2%E4%B8%8E%E4%BD%BF%E7%94%A8/","section":"Posts","summary":" 缘起 # 在上一篇文章中，我介绍了如何使用Obsidian和Nextcloud来管理笔记。上一篇文章的标题中使用了“全平台”这个词，是说Obsidian支持Windows、Linux、Mac、Android等多个平台。一般来说，这已经够用了，但是在某些场景下却依然不太方便。\n","title":"Obsidian（二）：网页版Obsidian的部署与使用","type":"post"},{"content":"","date":"2025年7月21日","externalUrl":null,"permalink":"/series/obsidian%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"Obsidian系列","type":"series"},{"content":"","date":"2025年7月21日","externalUrl":null,"permalink":"/tags/web/","section":"Tags","summary":"","title":"Web","type":"tags"},{"content":"","date":"2025年7月21日","externalUrl":null,"permalink":"/tags/%E7%AC%94%E8%AE%B0/","section":"Tags","summary":"","title":"笔记","type":"tags"},{"content":"","date":"2025年6月2日","externalUrl":null,"permalink":"/tags/cloudflare/","section":"Tags","summary":"","title":"Cloudflare","type":"tags"},{"content":"","date":"2025年6月2日","externalUrl":null,"permalink":"/tags/cloudflare-tunnel/","section":"Tags","summary":"","title":"Cloudflare Tunnel","type":"tags"},{"content":"","date":"2 六月 2025","externalUrl":null,"permalink":"/en/tags/domain/","section":"Tags","summary":"","title":"Domain","type":"tags"},{"content":"","date":"2 六月 2025","externalUrl":null,"permalink":"/en/tags/reverse-proxy/","section":"Tags","summary":"","title":"Reverse Proxy","type":"tags"},{"content":" 缘起 # 为了能够从公网访问我在自己服务器上搭建的网站和部署的服务，我之前的做法是：\n使用动态域名解析服务（DDNS）来将我的公网IP地址和域名绑定起来。 然后使用Nginx配置反向代理，将域名指向我服务器上的服务。 最后用acme来获取SSL证书，实现HTTPS访问。 后来，为了将反向代理也容器化，我转向了Traefik，将第2步和第3步合并了起来。算是一个更加方便的解决方案。以上这些可以参考我之前发布的文章：\n“动态域名解析\u0026ndash;Cloudflare”。 “从公网访问个人网站——Nginx反向代理配置”。 “从公网访问个人网站（二）——Traefik反向代理配置”。 以上的解决方案已经算是比较好用了，但依然依赖于公网IP地址的稳定性，以及端口的开放情况。如果在公司或学校等网络环境下部署，可能会遇到IP地址不稳定或端口被封禁的问题。很长一段时间内，我都认为这个问题很难解决。\n最近，我终于发现了一个比较好的解决方案：使用Cloudflare Tunnel。这个方案可以让你在没有公网IP地址的情况下，或者在端口被封禁的情况下，以及网络被防火墙限制的情况下，依然从公网访问你的服务。\n前置条件 # 运行Linux系统的电脑 (作为私人服务器) 私有域名（详细操作请查看之前的帖子\u0026ldquo;个人网站的建立过程（一）：购买个人域名并配置动态域名解析\u0026rdquo;） 了解容器化的基本概念，会用Docker或者Kubernetes（K8s或K3s）部署服务。 了解反向代理的基本概念和基本用法。可以参考\u0026ldquo;从公网访问个人网站——Nginx反向代理配置\u0026rdquo;或者\u0026ldquo;从公网访问个人网站（二）——Traefik反向代理配置\u0026rdquo;，里面有较为详细的介绍。 有一个Cloudflare账号，并且已经将你的域名添加到Cloudflare中。如果你的域名之前是由其他域名商提供的DNS服务，你需要将域名的DNS服务器切换到Cloudflare提供的DNS服务器。可以参考\u0026ldquo;将域名服务商从Hostinger迁移到Cloudflare\u0026rdquo;来了解如何将域名迁移到Cloudflare。 Cloudflare Tunnel基本概念 # 简介 # Cloudflare Tunnel（之前称为Argo Tunnel）是Cloudflare提供的一项服务，它可以让你在没有公网IP地址的情况下，将你的服务暴露到公网。它通过在你的服务器上运行一个轻量级的代理程序，将你的服务通过Cloudflare的网络进行转发，从而实现从公网访问你的服务。\n工作原理 # Cloudflare Tunnel的工作原理可以参考官方文档，简单来讲是这样的，只要你的电脑能访问Cloudflare的DNS服务器，就可以建立一条从Cloudflare到你电脑的隧道。在声明将你的域名解析到这个隧道上之后，Cloudflare会将所有关于你的域名的请求通过这个隧道转发到和这个隧道连接的电脑上。同样地，你电脑上的服务也可以通过这个隧道将响应发送出去。这样就实现了从公网访问你的服务。\n当用户在公网访问某个服务时，具体过程可参见下图：\n客户在浏览器中输入你的域名，域名解析到Cloudflare的DNS服务器。 Cloudflare的服务器将请求转发到你建立的Cloudflare Tunnel。这个Tunnel靠你电脑上的cloudflared程序来维持。 cloudflared将请求转发给你设置的反向代理服务（如Traefik或Nginx）。 反向代理服务将请求转发到你电脑上的具体服务（如NextCloud、Jellyfin等）。 具体服务处理请求后，将响应通过上述反向的过程发送回给做出请求的客户。 可见，上述过程只要求你的电脑能够访问Cloudflare的服务器，而不需要公网IP地址或开放端口，也不管你的网络是否被防火墙限制。\nCloudflare Tunnel的使用 # 开启Cloudflare的Zero Trust服务 # Cloudflare Tunnel是Cloudflare Zero Trust服务的一部分，因此你需要先开启Cloudflare的Zero Trust服务。\n登录Cloudflare账号后，左侧菜单栏中找到“Zero Trust”，点击进入。\n首次开启会让你选择一个套餐，对于我这个个人用户而言，免费套餐就够用了。填入信用卡的付款信息开启即可。\n创建一个Tunnel # 在Zero Trust页面，左侧边栏有一个Networks选项，展开后里面有个Tunnels选项，点击即可打开Tunnels管理页面：\n你可以点击Create a tunnel按钮来创建一个新的Tunnel，但这里不推荐这么创建，因为这样创建后还需要将配置挪到服务器上。我们选择在服务器上直接创建，步骤如下：\n1. 在服务器上安装cloudflared # 首先我们在服务器上安装cloudflared程序。cloudflared是Cloudflare提供的一个命令行工具，用于创建和管理Cloudflare Tunnel。具体安装方法参见官方文档。这里简单列一下：\n在Debian系Linux系统（Ubuntu等）上：\n1 2 3 4 5 6 7 8 9 # Add cloudflare gpg key sudo mkdir -p --mode=0755 /usr/share/keyrings curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg \u0026gt;/dev/null # Add this repo to your apt repositories echo \u0026#39;deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main\u0026#39; | sudo tee /etc/apt/sources.list.d/cloudflared.list # install cloudflared sudo apt-get update \u0026amp;\u0026amp; sudo apt-get install cloudflared 在RedHat系Linux系统（fedora等）上：\n1 2 sudo dnf config-manager --add-repo https://pkg.cloudflare.com/cloudflared-ascii.repo sudo dnf install cloudflared 2. 在服务器上创建Tunnel # 在安装好cloudflared后，我们首先需要登录一下Cloudflare账号。在服务器上运行\n1 cloudflared tunnel login 会给你输出一个链接，复制这个链接到浏览器即可登录Cloudflare账号。登录后会让你选择你想要绑定到Tunnel上的域名，选择域名即可。\n然后在服务器上运行\n1 cloudflared tunnel create my-tunnel 其中my-tunnel是你要创建的Tunnel的名称，你可以根据需要修改。运行后会输出一个Tunnel的ID，记下来。\n同时，cloudflared会在你的服务器上创建一个记录这个Tunnel的JSON文件，位置一般在~/.cloudflared/目录下，文件名是Tunnel ID。这个文件很重要，里面存储了连接这个Tunnel所需的凭证。\n这时你可以登录Cloudflare网页端查看Tunnel是否创建成功。在Zero Trust -\u0026gt; Networks -\u0026gt; Tunnels页面，你应该能看到刚才创建的Tunnel：\n注意，这时我们只创建了这个Tunnel，还没有连接它，因此它处于inactive的状态。\n3. 连接Tunnel # 这里分别以Docker和k3s为例，说明如何将部署的服务连接到Tunnel。\nDocker # 前置条件：\n这里假设\n你已经安装了docker和docker compose， 使用Traefik来做反向代理， 并创建了一个所有容器共享的网络traefik-net。 具体过程可参考“从公网访问个人网站（二）——Traefik反向代理配置”。\n创建cloudflared容器\n在你的Docker项目目录下（假设是~/docker），创建一个新的项目文件夹cloudflared，项目结构如下：\n1 2 3 4 cloudflared/ ├── docker-compose.yml ├── .env └── config.yml docker-compose.yml文件内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 version: \u0026#39;3.8\u0026#39; services: cloudflared: image: cloudflare/cloudflared:2025.5.0 container_name: cloudflared command: tunnel --no-autoupdate run my-tunnel volumes: - ./config.yaml:/etc/cloudflared/config.yaml:ro - ${CERT_FILE}:/etc/cloudflared/cert.pem - ${CRED_FILE}:/etc/cloudflared/creds/credentials.json restart: unless-stopped networks: - traefik-net networks: traefik-net: external: true 这里的my-tunnel是你之前创建的Tunnel的名称。CERT_FILE是你之前创建的Tunnel时生成的证书，CRED_FILE是你之前创建的Tunnel时生成的凭证文件，通常是~/.cloudflared/\u0026lt;Tunnel ID\u0026gt;.json。你可以在.env文件中定义这个变量：\n.env文件内容如下：\n1 2 CERT_FILE=/home/user/.cloudflared/cert.pem CRED_FILE=/home/user/.cloudflared/\u0026lt;TUNNEL ID\u0026gt;.json 注意：在某些情况下，cert.pem文件和\u0026lt;TUNNEL ID\u0026gt;.json文件可能不包含读写权限，你可能需要手动修改权限以让docker能够读取它们：\n1 2 chmod +r /home/user/.cloudflared/cert.pem chmod +r /home/user/.cloudflared/TUNNEL-ID.json 除此之外，我们还需要一个配置文件config.yml：\n1 2 3 4 5 6 7 8 tunnel: li-tunnel credentials-file: /etc/cloudflared/creds/credentials.json metrics: 0.0.0.0:2000 no-autoupdate: true ingress: - hostname: \u0026#34;*.jinli.li\u0026#34; service: http://traefik:80 - service: http_status:404 这里的hostname是你要通过Tunnel访问的域名，这里我让所有以jinli.li结尾的域名都通过这个li-tunnel访问。但并不推荐这么干，最好还是一个域名一个域名地设置。 service是指向Traefik容器的服务地址。注意这里的traefik:80是指Traefik容器的名称和端口。 启动cloudflared容器 在cloudflared目录下运行以下命令来启动cloudflared容器：\n1 docker-compose up -d 这时你可以在Zero Trust -\u0026gt; Networks -\u0026gt; Tunnels页面看到Tunnel的状态变为active，表示Tunnel已经连接成功：\n配置DNS记录\n在Cloudflare的DNS管理页面，删除掉之前关于*.jinli.li的DNS记录。\n然后回到服务器命令行，运行\n1 cloudflared tunnel route dns li-tunnel \u0026#34;*.jinli.li\u0026#34; 这条命令创建了一个wildcard DNS记录，将所有以jinli.li结尾的域名都指向这个Tunnel。\n当然，你也可以只指定某个具体的域名，例如：\n1 cloudflared tunnel route dns li-tunnel \u0026#34;nextcloud.jinli.li\u0026#34; 或者在Cloudflare的网页端DNS管理界面来创建DNS记录，选择CNAME类型，指向\u0026lt;Tunnel ID\u0026gt;.cfargotunnel.com，其中\u0026lt;Tunnel ID\u0026gt;是你之前创建的Tunnel的ID。具体可参见官方文档。但不知为啥，我这么创建后依然无法访问域名，上面用命令行创建的就没问题。\n注意：如果你之后不需要再用命令行来创建DNS记录了，那么在服务器用包管理工具安装的这个cloudflared程序就没啥用了，你可以将其卸载。之前创建的Tunnel将由Docker里容器化的cloudflared程序维护。\n测试访问\n现在你就可以通过浏览器访问你的域名了，例如https://nextcloud.jinli.li。如果你之前已经部署了NextCloud服务，并在Traefik中配置反向代理，那么就可以直接访问了。\nK3s # 前置条件：\n这里假设你已经安装了K3s，并且使用Traefik作为Ingress Controller。具体过程可参考“Homelab（1）：使用Kubernetes（K8s）或K3s自建家庭集群”。\n将Tunnel凭证secret化\n之前创建Tunnel时cloudflared生成的凭证文件保存在~/.cloudflared/\u0026lt;Tunnel ID\u0026gt;.json，我们需要将这个文件转换为Kubernetes的Secret。\n在服务器上运行以下命令：\n1 kubectl create secret generic cloudflared-credentials --from-file=credentials.json=\u0026lt;User Home Path\u0026gt;/.cloudflared/\u0026lt;Tunnel ID\u0026gt;.json 其中\u0026lt;User Home Path\u0026gt;是你的用户主目录路径，\u0026lt;Tunnel ID\u0026gt;是你之前创建的Tunnel的ID。\n创建cloudflared Deployment和Service\n所需的manifest文件如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 apiVersion: apps/v1 kind: Deployment metadata: name: cloudflared spec: selector: matchLabels: app: cloudflared replicas: 1 template: metadata: labels: app: cloudflared spec: containers: - name: cloudflared image: cloudflare/cloudflared:2025.5.0 args: - tunnel - --config - /etc/cloudflared/config/config.yaml - run livenessProbe: httpGet: path: /ready port: 2000 failureThreshold: 1 initialDelaySeconds: 10 periodSeconds: 10 volumeMounts: - name: config mountPath: /etc/cloudflared/config readOnly: true - name: creds mountPath: /etc/cloudflared/creds readOnly: true volumes: - name: creds secret: secretName: li-tunnel-credentials - name: config configMap: name: cloudflared items: - key: config.yaml path: config.yaml --- apiVersion: v1 kind: ConfigMap metadata: name: cloudflared data: config.yaml: | tunnel: li-tunnel credentials-file: /etc/cloudflared/creds/credentials.json metrics: 0.0.0.0:2000 no-autoupdate: true ingress: - hostname: \u0026#34;*.jinli.li\u0026#34; service: http://traefik.kube-system.svc.cluster.local:80 - service: http_status:404 配置的含义跟Docker里的类似，这里不再赘述。\n部署cloudflared\n将上述manifest文件保存为cloudflared.yaml，然后在服务器上运行以下命令来部署cloudflared：\n1 kubectl apply -f cloudflared.yaml 这时你可以在Zero Trust -\u0026gt; Networks -\u0026gt; Tunnels页面看到Tunnel的状态变为active，表示Tunnel已经连接成功。\n如果没有，那么你需要检查cloudflared是否部署成功。\n配置DNS记录与测试访问\n这两步跟上述在Docker中使用cloudflared的步骤（第3步和第4步）一样。\n","date":"2025年6月2日","externalUrl":null,"permalink":"/p/%E4%BB%8E%E5%85%AC%E7%BD%91%E8%AE%BF%E9%97%AE%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99%E4%B8%89%E4%BD%BF%E7%94%A8cloudflare-tunnel%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86docker%E5%AE%B9%E5%99%A8/","section":"Posts","summary":" 缘起 # 为了能够从公网访问我在自己服务器上搭建的网站和部署的服务，我之前的做法是：\n","title":"从公网访问个人网站（三）——使用Cloudflare Tunnel反向代理Docker容器","type":"post"},{"content":"","date":"2025年6月2日","externalUrl":null,"permalink":"/tags/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86/","section":"Tags","summary":"","title":"反向代理","type":"tags"},{"content":"","date":"2025年6月2日","externalUrl":null,"permalink":"/tags/%E5%9F%9F%E5%90%8D/","section":"Tags","summary":"","title":"域名","type":"tags"},{"content":"","date":"2025年6月1日","externalUrl":null,"permalink":"/tags/homelab/","section":"Tags","summary":"","title":"Homelab","type":"tags"},{"content":" 缘起 # 在开始自建家庭服务器和用容器技术来部署各种私有服务约5年后，我已经熟悉了一套用docker compose管理所有容器的方案，这套方案很优雅也很有用，但这仅限单台电脑。最近我突然有了管理使用多台电脑的需求，我终于开始觉得有必要从单个服务器转向服务器集群，或者说Homelab了。\n具体来说，我之前作为服务器的电脑有着比较大的内存和硬盘，但没安装显卡，这对于我之前部署的许多服务已经够了。现在我有了一台有显卡的电脑，想要跑一些大语言模型。当然我可以单独在这台电脑上部署，但这样与我之前的方案就比较割裂了，我希望能有个方案能同时管理这两台电脑。将来再添加其他电脑时也更容易扩展。\n当然，转向Kubernetes之前，你最好已经对容器的概念比较熟悉了。我之前写过一系列的关于容器（主要是Docker）的文章可供参考：\n容器（2）：docker最佳实践指南——docker-compose和Portainer 容器（3）：docker最佳实践指南——数据卷volume的管理 容器（4）：docker最佳实践指南——容器更新、升级和迁移 容器（5）：docker最佳实践指南——容器更新监测工具WUD（What\u0026rsquo;s Up Docker） 容器（6）：使用Docker时的一些误区、坏习惯和问题 Homelab # 什么是Homelab # Homelab表面上的意思是指在家中搭建一个实验室，用于学习、实验和开发。Homelab通常包括一台或多台服务器、网络设备、存储设备等，可以用来运行各种服务和应用程序。\nHomelab的规模可大可小，如果你财力充足，可以购买多台高性能服务器，搭建一个大型的家庭实验室；如果你财力有限，也可以只用一台普通的电脑，甚至一个树莓派（Raspberry Pi）就可以搭建一个小型的家庭实验室。\n为什么需要Homelab # 服务器集群或者Homelab听起来离个人用户比较遥远，但实际上并非如此，我甚至觉得很多玩私有服务器的人可能都会慢慢走向Homelab。即使你没有多台电脑或服务器，依然可以尝试使用Kubernetes（K8s）或K3s来管理你的容器化应用，之后如果你有了多台电脑或服务器，你可以很容易地将现有的Kubernetes集群扩展到多台机器上。\n总之，Homelab具有很高的灵活性和可扩展性，可以满足个人用户的各种需求。即使你只有简单的需求和简单的硬件，也可以通过搭建Homelab来学习很多新技术和新知识。\nKubernetes（K8s）和K3s # Kubernetes（K8s）是一个开源的容器编排平台，用于自动化容器化应用的部署、扩展和管理。它可能是应用最广泛的搭建Homelab的方案。\n当然，Kubernetes里的知识相当复杂，涉及到很多概念和组件，例如Pod、Service、Deployment、Ingress等。对于初学者来说，可能会觉得过于复杂而有些难以理解。K3s是Kubernetes的一个轻量级版本，专为资源受限的环境设计。它去掉了一些不必要的组件和功能，使得K3s更容易安装和管理。K3s非常适合在家庭实验室或小型集群中使用。\n在这个系列的文章中，我们将从非常简单的应用出发，使用K3s从零开始搭建一个Kubernetes集群，并逐步扩展到更复杂的应用和多节点。\nK3s基础知识 # Kubernetes（K8s）是一个很复杂的系统，里面包含了很多组件。K3s对其做了简化，只留下了最核心的组件。我们这里也先只介绍最核心、最基本的概念，来简单了解一下K3s是怎样运作的，其他的组件和概念以后用到时再做介绍。\n下图是一个单节点的服务器上K3s运行的基本框架示例：\n当一个用户对某个服务做出一个请求时，运行过程是这样的：\n当一个外部用户的请求到达K3s集群时，首先会通过Ingress Controller（通常是Traefik）来处理请求。 Ingress Controller会根据Ingress资源定义的路由规则，将请求转发到相应的Service。 Service会将请求转发到对应的Pod，Pod中运行着实际的应用程序容器。 另外，管理员可以通过命令行工具kubectl来管理K3s集群内的组件和资源。\n接下来我们来简单介绍一下这几个概念。\nPod # Pod是Kubernetes的最小部署单元，它可以包含一个或多个容器。Pod中的容器共享网络和存储资源。Pod通常用于运行一个应用程序或服务。\nService # Service是Kubernetes中的一个抽象概念，用于定义一组Pod的访问策略。Service可以通过一个固定的IP地址和端口来访问Pod，无论Pod的实际IP地址如何变化。Service可以分为ClusterIP、NodePort、LoadBalancer等类型。\nClusterIP：默认类型，Service只能在集群内部访问。 NodePort：Service可以在集群外部通过指定的端口访问。 LoadBalancer：Service可以通过云提供商的负载均衡器来访问。 Ingress和Ingress Controller # Ingress是Kubernetes中的一个资源，用于管理外部访问集群内部服务的路由规则。Ingress可以通过域名或路径来路由请求到不同的Service。Ingress通常与Ingress Controller一起使用，Ingress Controller负责实现Ingress资源定义的路由规则。\nDeployment # Deployment是Kubernetes中的一个控制器，用于管理Pod的部署和更新。Deployment可以定义Pod的副本数、更新策略等。通过Deployment，我们可以轻松地扩展或缩减Pod的数量，并且可以在不影响服务可用性的情况下进行滚动更新。\nK3s的安装和配置 # 接下来我们以一个单节点集群为例，来说明如何使用K3s。之后会介绍如果有更多节点时如何扩展集群。\n这里我们选择部署一个最简单的whoami服务。\n1. 安装K3s # 在Linux服务器上安装K3s非常简单，只需要运行以下命令：\n1 curl -sfL https://get.k3s.io | sudo sh - 运行完成后，K3s会自动创建一个默认的单节点集群。当安装结束时，你会在命令行看到如下输出：\n1 2 3 4 5 6 7 8 9 10 [INFO] Creating /usr/local/bin/kubectl symlink to k3s [INFO] Creating /usr/local/bin/crictl symlink to k3s [INFO] Creating /usr/local/bin/ctr symlink to k3s [INFO] Creating killall script /usr/local/bin/k3s-killall.sh [INFO] Creating uninstall script /usr/local/bin/k3s-uninstall.sh [INFO] env: Creating environment file /etc/systemd/system/k3s.service.env [INFO] systemd: Creating service file /etc/systemd/system/k3s.service [INFO] systemd: Enabling k3s unit Created symlink \u0026#39;/etc/systemd/system/multi-user.target.wants/k3s.service\u0026#39; → \u0026#39;/etc/systemd/system/k3s.service\u0026#39;. [INFO] systemd: Starting k3s 这说明这条命令做了以下工作：\n下载并安装K3s。 创建了kubectl、crictl和ctr的符号链接，这些工具可以用来管理K3s集群。其中 kubectl是Kubernetes的命令行工具，用于管理Kubernetes集群。 crictl是容器运行时接口（CRI）的命令行工具，用于管理容器。 ctr是容器运行时的命令行工具，用于直接与容器运行时交互。 创建了k3s-killall.sh和k3s-uninstall.sh脚本，用于停止和卸载K3s。 运行命令sudo /usr/local/bin/k3s-killall.sh可以停止K3s服务。 运行命令sudo /usr/local/bin/k3s-uninstall.sh可以卸载K3s。 创建了环境变量文件/etc/systemd/system/k3s.service.env，用于配置K3s服务。 创建了K3s的systemd服务文件/etc/systemd/system/k3s.service，并启用了该服务。之后每次电脑开机时，K3s服务都会自动启动。 启动了K3s服务。 2. 验证K3s安装 # 安装完成后，K3s会在/etc/rancher/k3s目录下创建一个名为k3s.yaml的配置文件。这个文件包含了K3s集群的配置信息，包括API服务器的地址、认证信息等。\n上面提到，K3s还安装了一个kubectl作为管理K3s的命令行工具，kubectl运行时需要根据API服务器的地址、认证信息知道它要管理的是哪个集群。kubectl运行时默认读取/etc/rancher/k3s/k3s.yaml文件中的配置信息，如果你不是以root来运行kubectl，会因为权限问题无法读取/etc/rancher/k3s/k3s.yaml文件。这时我们可以将/etc/rancher/k3s/k3s.yaml复制到~/.kube目录下，并更改文件权限，让kubectl可以读取集群信息：\n1 2 3 mkdir -p ~/.kube sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config sudo chown $(id -u):$(id -g) ~/.kube/config 现在你可以使用kubectl来管理K3s集群了。可以运行以下命令来验证K3s是否安装成功：\n1 kubectl get nodes 如果安装成功，你应该能看到类似下面的输出：\n1 2 NAME STATUS ROLES AGE VERSION fedora.attlocal.net Ready control-plane,master 15s v1.32.5+k3s1 然后查看正在运行的pods：\n1 kubectl get pods -A 如果一切正常，你应该能看到类似下面的输出：\n1 2 3 4 5 6 NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES coredns-697968c856-scmft 0/1 ContainerCreating 0 15s \u0026lt;none\u0026gt; fedora.attlocal.net \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; helm-install-traefik-crd-7dkch 0/1 ContainerCreating 0 15s \u0026lt;none\u0026gt; fedora.attlocal.net \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; helm-install-traefik-qkl97 0/1 ContainerCreating 0 15s \u0026lt;none\u0026gt; fedora.attlocal.net \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; local-path-provisioner-774c6665dc-jrbrj 0/1 ContainerCreating 0 15s \u0026lt;none\u0026gt; fedora.attlocal.net \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; metrics-server-6f4c6675d5-v97zv 0/1 ContainerCreating 0 15s \u0026lt;none\u0026gt; fedora.attlocal.net \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; 其中coredns是Kubernetes的DNS服务，helm-install-traefik-crd和helm-install-traefik-qkl97是指安装了Traefik，local-path-provisioner是K3s的默认存储类，metrics-server是Kubernetes的监控服务。\n3. 部署whoami服务 # 现在我们可以部署一个简单的whoami服务来测试K3s集群。whoami是一个非常简单的HTTP服务，它会返回请求的IP地址、请求头等信息，非常适合用来测试K3s集群。\n创建whoami应用的manifest # K3s使用一个被称作manifest的YAML文件来定义应用程序的部署。我们可以创建一个名为whoami.yaml的文件，内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 apiVersion: apps/v1 kind: Deployment metadata: name: whoami labels: app: whoami spec: replicas: 1 selector: matchLabels: app: whoami template: metadata: labels: app: whoami spec: containers: - name: whoami image: traefik/whoami ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: whoami labels: app: whoami spec: type: ClusterIP ports: - port: 80 targetPort: 80 selector: app: whoami --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: whoami-ingress spec: ingressClassName: traefik rules: - host: whoami.example.com http: paths: - path: / pathType: Prefix backend: service: name: whoami port: number: 80 虽然这是一个YAML文件，但实际上有三部分。这里为了简单起见，我们把这三部分放在了同一个文件里，后续的文章会说明，为了更好地组织和管理K3s里的应用，我们应该怎样组织文件结构。\n这三部分用---分隔开来，分别定义了：\nDeployment：定义了一个名为whoami的Deployment，表示我们要部署一个名为whoami的应用。这个应用有1个副本（replica），使用Traefik官方提供的whoami镜像，并监听80端口。 Service：定义了一个名为whoami的Service，表示我们要创建一个名为whoami的服务。这个服务的类型是ClusterIP，表示只能在集群内部访问。它监听80端口，并将请求转发到Pod中的whoami容器的80端口。 Ingress：定义了一个名为whoami-ingress的Ingress，表示我们要创建一个名为whoami-ingress的Ingress资源。这个Ingress使用Traefik作为Ingress Controller，并将请求路由到名为whoami的Service。 应用whoami服务 # 现在我们可以使用kubectl来应用这个manifest文件了。运行以下命令：\n1 kubectl apply -f whoami.yaml 如果一切正常，你应该能看到类似下面的输出：\n1 2 3 deployment.apps/whoami created service/whoami created ingress.networking.k8s.io/whoami-ingress created 查看whoami服务状态 # 首先，我们来确认whoami服务是否已经成功部署。运行以下命令：\n1 kubectl get deployments 如果一切正常，你应该能看到类似下面的输出：\n1 2 NAME READY UP-TO-DATE AVAILABLE AGE whoami 1/1 1 1 2m 接下来，我们来确认whoami服务的Pod是否已经成功运行。运行以下命令：\n1 kubectl get pods 如果一切正常，你应该能看到类似下面的输出：\n1 2 NAME READY STATUS RESTARTS AGE whoami-5b6c7f8d9f-2j4k5 1/1 Running 0 2m 接下来，我们来确认whoami服务的Service是否已经成功创建。运行以下命令：\n1 kubectl get svc 如果一切正常，你应该能看到类似下面的输出：\n1 2 3 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE whoami ClusterIP 10.43.42.148 \u0026lt;none\u0026gt; 8080/TCP 20s kubernetes ClusterIP 10.43.0.1 \u0026lt;none\u0026gt; 443/TCP 3m15s 访问whoami服务 # 现在我们可以通过Ingress来访问whoami服务了。首先，我们在部署whoami的电脑上，或者在同一局域网的电脑上，使用本机地址或者局域网内的地址来访问whoami服务。 如果你在本机上访问，可以使用以下命令：\n1 curl \u0026#34;Host: whoami.example.com\u0026#34; http://localhost 如果你在局域网内的其他电脑上访问，例如你的K3s服务器的IP地址是192.168.1.233，可以使用以下命令：\n1 curl \u0026#34;Host: whoami.example.com\u0026#34; http://192.168.1.233 这里添加了Host头部信息，因为Ingress需要根据这个头部信息来路由请求到对应的Service，否则Ingress怎么知道你要访问的是哪个Service呢？\n如果一切正常，你应该能看到类似下面的输出：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Hostname: whoami-64f6cf779d-zxsm4 IP: 127.0.0.1 IP: ::1 IP: 10.42.0.9 IP: fe80::b43d:c0ff:fe52:80bd RemoteAddr: 10.42.0.8:39990 GET / HTTP/1.1 Host: whoami.example.com User-Agent: curl/8.12.0 Accept: */* Accept-Encoding: gzip X-Forwarded-For: 10.42.0.1 X-Forwarded-Host: whoami.jinli.li X-Forwarded-Port: 80 X-Forwarded-Proto: http X-Forwarded-Server: traefik-c98fdf6fb-5q6m6 X-Real-Ip: 10.42.0.1 如果你能看到类似上面的输出，说明whoami服务已经成功运行了。但是注意，这里的X-Forwarded-For和X-Real-Ip的值10.42.0.1是K3s集群内部的IP地址，而不是你访问的电脑的IP地址。这是因为Ingress Controller（Traefik）会将请求转发到whoami服务时，使用了K3s集群内部的IP地址。\nTODO: 如何解决这一问题？\n如果你想在外部访问whoami服务，你需要将域名whoami.example.com解析到你的K3s服务器的公网IP地址上。然后你就可以通过浏览器或其他HTTP客户端来访问whoami服务了。\n如果你在浏览器中访问http://whoami.example.com，你应该能看到类似下面的页面：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Hostname: whoami-64f6cf779d-ktwm9 IP: 127.0.0.1 IP: ::1 IP: 10.42.0.18 IP: fe80::1453:3cff:fe59:5835 RemoteAddr: 10.42.0.8:50132 GET / HTTP/1.1 Host: whoami.jinli.li User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, br Accept-Language: en Cache-Control: max-age=0 Priority: u=0, i Sec-Ch-Ua: \u0026#34;Chromium\u0026#34;;v=\u0026#34;136\u0026#34;, \u0026#34;Google Chrome\u0026#34;;v=\u0026#34;136\u0026#34;, \u0026#34;Not.A/Brand\u0026#34;;v=\u0026#34;99\u0026#34; Sec-Ch-Ua-Mobile: ?0 Sec-Ch-Ua-Platform: \u0026#34;macOS\u0026#34; Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: none Sec-Fetch-User: ?1 Upgrade-Insecure-Requests: 1 X-Forwarded-For: 10.42.0.11 X-Forwarded-Host: whoami.jinli.li X-Forwarded-Port: 80 X-Forwarded-Proto: http X-Forwarded-Server: traefik-c98fdf6fb-5q6m6 X-Real-Ip: 10.42.0.11 总结 # 在本篇文章中，我们介绍了K3s的基础知识，并演示了如何在单节点K3s集群上部署一个简单的whoami服务。\n在接下来的文章中，我们将继续扩展K3s集群，介绍如何部署更复杂的应用，如何管理多节点集群，以及如何使用K3s的其他功能来满足不同的需求。\n鸣谢 # 我在学习如何使用K3s时参考了Youtube博主LinuxCloudHacks的视频From Zero to Hero: K3s, Traefik \u0026amp; Cloudflare Your Home Lab Powerhouse。\n","date":"2025年6月1日","externalUrl":null,"permalink":"/p/homelab1%E4%BD%BF%E7%94%A8kubernetesk8s%E6%88%96k3s%E8%87%AA%E5%BB%BA%E5%AE%B6%E5%BA%AD%E9%9B%86%E7%BE%A4/","section":"Posts","summary":" 缘起 # 在开始自建家庭服务器和用容器技术来部署各种私有服务约5年后，我已经熟悉了一套用docker compose管理所有容器的方案，这套方案很优雅也很有用，但这仅限单台电脑。最近我突然有了管理使用多台电脑的需求，我终于开始觉得有必要从单个服务器转向服务器集群，或者说Homelab了。\n","title":"Homelab（1）：使用Kubernetes（K8s）或K3s自建家庭集群","type":"post"},{"content":"","date":"2025年6月1日","externalUrl":null,"permalink":"/tags/k3s/","section":"Tags","summary":"","title":"K3s","type":"tags"},{"content":"","date":"2025年6月1日","externalUrl":null,"permalink":"/tags/kubernetes/","section":"Tags","summary":"","title":"Kubernetes","type":"tags"},{"content":" 缘起 # 之前我一直使用Nginx作为反向代理来访问我的私人服务器上的网站。当时Nginx是直接安装在电脑上的，没有容器化，这导致我之后想把这些服务迁移到其他电脑上时还得重新配置Nginx、重新配置SSL证书等。\n所以我决定将反向代理服务也容器化，以方便以后的管理和迁移。我在网上看到了许多在Docker中使用Traefik的例子，因此决定从Nginx迁移到Traefik。\n前置条件 # 运行Linux系统的电脑 (作为私人服务器) 私有域名 （详细操作请查看之前的帖子\u0026ldquo;个人网站的建立过程（一）：购买个人域名并配置动态域名解析\u0026rdquo;） 已安装Docker和docker-compose，了解Docker的基本概念和基本用法。如果你还没有安装Docker和docker-compose，或者没有了解Docker和容器化的基本概念和基本用法，可以参考\u0026ldquo;容器（1）：容器相关知识简介——容器化、docker、docker-compose、Kubernetes / K8s等\u0026rdquo;，里面有较为详细的介绍。 已了解Nginx的基本概念和基本用法。如果你没用过或还不了解Nginx，可以参考\u0026ldquo;从公网访问个人网站——Nginx反向代理配置\u0026rdquo;，里面有较为详细的介绍。 Traefik简介 # Traefik是一个现代化的反向代理和负载均衡器，它可以自动发现和配置后端服务。Traefik支持Docker、Kubernetes、Consul等多种后端服务，并且可以自动获取SSL证书。Traefik的配置非常灵活，可以通过标签、文件或API等方式进行配置。\n安装并配置Traefik # 整体架构 # Traefik可以使用标签配置，可以使用API配置，也可以使用文件配置。由于我之前在使用Nginx时是用文件配置的，每个服务一个配置文件，我觉得这样非常清晰，也便于管理，因此在使用Traefik时也使用文件来配置。\nTraefik容器本身是独立的，跟其他Docker容器一样，使用docker-compose.yml文件来管理。我们在Docker里新建一个网络traefik-net，包括Traefik在内的所有容器都使用这个网络，在这个网络内，我们使用Traefik进行反向代理。\n这篇文章中，我们将以部署一个Whoami服务为例来介绍Traefik的使用。Whoami是一个简单的HTTP服务器，它会返回请求的IP地址、请求头等信息，非常适合用来测试反向代理。\n项目目录结构 # traefik项目 # 基于上述架构设计，我们在之前存放所有docker项目的目录下新建一个traefik目录，用于存放Traefik项目的相关文件。目录结构如下：\n1 2 3 4 5 6 7 8 9 traefik/ ├── docker-compose.yml ├── .env ├── traefik.yml ├── acme.json └── dynamic/ ├── app1.yml ├── app2.yml └── app3.yml 其中：\ndocker-compose.yml：Traefik的Docker Compose配置文件。 .env：环境变量文件，用于存放Traefik项目使用的环境变量。 traefik.yml：Traefik的主配置文件。 acme.json：用于存储SSL证书的文件。 dynamic：存放动态配置文件的目录，每个服务一个配置文件。 whoami项目 # 由于我们以一个简单的Whoami服务为例，所以这里我们除了创建一个Traefik项目，还需要创建一个Whoami项目。Whoami项目就很简单，同样放在之前存放所有docker项目的目录下，里面只含有一个docker-compose.yml文件。\n1 2 whoami/ └── docker-compose.yml Traefik安装配置及测试流程 # 1. 在Docker中创建traefik-net网络 # 首先我们在Docker中创建一个名为traefik-net（也可以用其他名字，但之后的配置中要与这个名字一致）的公用网络：\n1 docker network create traefik-net 2. 创建上述traefik项目目录 # 在之前存放所有docker项目的目录下创建一个traefik目录，并在该目录下创建上述的文件和子目录。\ndocker-compose.yml # docker-compose.yml如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 version: \u0026#34;3.8\u0026#34; services: traefik: image: traefik:v3.4.0 container_name: traefik command: - \u0026#34;--configFile=/etc/traefik/traefik.yml\u0026#34; - \u0026#34;--entryPoints.web.address=:80\u0026#34; - \u0026#34;--entryPoints.websecure.address=:443\u0026#34; - \u0026#34;--providers.docker=true\u0026#34; - \u0026#34;--providers.docker.network=traefik-net\u0026#34; - \u0026#34;--providers.docker.exposedByDefault=false\u0026#34; - \u0026#34;--providers.file.directory=/etc/traefik/dynamic\u0026#34; - \u0026#34;--providers.file.watch=true\u0026#34; - \u0026#34;--certificatesResolvers.le.acme.email=1067848738@qq.com\u0026#34; - \u0026#34;--certificatesResolvers.le.acme.storage=/letsencrypt/acme.json\u0026#34; - \u0026#34;--certificatesResolvers.le.acme.httpChallenge.entryPoint=web\u0026#34; - \u0026#34;--api.dashboard=true\u0026#34; - \u0026#34;--log.level=INFO\u0026#34; ports: - \u0026#34;80:80\u0026#34; - \u0026#34;443:443\u0026#34; - \u0026#34;8080:8080\u0026#34; # comment out if you don’t need the dashboard volumes: - \u0026#34;/var/run/docker.sock:/var/run/docker.sock:ro\u0026#34; - \u0026#34;./traefik.yml:/etc/traefik/traefik.yml:ro\u0026#34; - \u0026#34;./dynamic:/etc/traefik/dynamic:ro\u0026#34; - \u0026#34;./acme.json:/letsencrypt/acme.json\u0026#34; environment: - CF_API_KEY=${CF_API_KEY} - CF_API_EMAIL=${CF_API_EMAIL} networks: - traefik-net restart: unless-stopped networks: traefik-net: external: true 其中指定了使用的网络是之前创建的traefik-net，并且将Traefik的配置文件traefik.yml、动态配置文件夹dynamic/和存储SSL证书的文件acme.json挂载到容器中。\n.env # .env中存储了CloudFlare的API密钥和电子邮件地址，用于自动获取SSL证书。你需要将以下内容替换为你自己的Cloudflare API密钥和电子邮件地址。理论上只提供密钥应该就够了，但我没试。如果你使用的DNS服务商不是Cloudflare（比如你使用阿里云），则需要将相关的API密钥和电子邮件地址替换为你使用的DNS服务商的API密钥和电子邮件地址，具体可参见Traefik的文档。\n1 2 CF_API_KEY=your_cloudflare_api_key CF_API_EMAIL=your_cloudflare_email traefik.yml # traefik.yml是Traefik的主配置文件，内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 entryPoints: web: address: \u0026#34;:80\u0026#34; websecure: address: \u0026#34;:443\u0026#34; providers: file: directory: /etc/traefik/dynamic watch: true api: dashboard: true insecure: false certificatesResolvers: le: acme: email: your-email@example.com storage: /letsencrypt/acme.json dnsChallenge: provider: cloudflare resolvers: - \u0026#34;1.1.1.1:53\u0026#34; - \u0026#34;1.0.0.1:53\u0026#34; #log: # level: \u0026#34;DEBUG\u0026#34; 其中我们指定了两个入口点：web（HTTP）和websecure（HTTPS），并且指定了动态配置文件的目录为/etc/traefik/dynamic。我们还启用了Traefik的API仪表盘，并配置了ACME证书解析器为Cloudflare DNS Challenge。如果你使用的是其他DNS服务商，可以将provider: cloudflare替换为你使用的DNS服务商的名称，并根据其文档配置相应的API密钥和电子邮件地址。\nacme.json # acme.json是用于存储SSL证书的文件。你需要先创建这个文件（空文件即可），并设置其权限为600，以确保Traefik可以写入证书信息：\n1 2 touch acme.json chmod 600 acme.json dynamic目录 # 在dynamic目录下，我们可以为每个服务创建一个配置文件。下面是一个非常简单的Whoami服务对应的配置文件，在这篇文章中就以部署一个Whoami服务为例来介绍Traefik的使用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 http: routers: whoami-router: entryPoints: - websecure rule: \u0026#34;Host(`whoami.example.com`)\u0026#34; tls: certResolver: le service: whoami-service services: whoami-service: loadBalancer: servers: - url: \u0026#34;http://whoami:80\u0026#34; 3. 创建上述whoami项目目录 # 在之前存放所有docker项目的目录下创建一个whoami目录，并在该目录下创建docker-compose.yml文件，内容也很简单：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 version: \u0026#39;3.8\u0026#39; services: whoami: image: traefik/whoami:v1.11.0 container_name: whoami restart: always networks: traefik-net: aliases: - whoami networks: traefik-net: external: true 这个docker-compose.yml文件定义了一个Whoami服务，使用Traefik官方提供的Whoami镜像，并将其连接到之前创建的traefik-net网络。\n4. 启动Whoami和Traefik服务 # 如果你之前在这台电脑上部署了Nginx服务，需要首先停止Nginx服务，以免与Traefik的端口冲突：\n1 sudo systemctl stop nginx 为了避免电脑重启之后Nginx再次启动，你可以禁用Nginx服务：\n1 sudo systemctl disable nginx 进入whoami目录，使用以下命令启动Whoami服务：\n1 docker-compose up -d 进入traefik目录，使用以下命令启动Traefik服务：\n1 docker-compose up -d 检查Traefik和Whoami服务是否正常运行：\n1 docker ps 你应该能看到Traefik和Whoami服务的容器正在运行。\n5. 测试Traefik反向代理 # 首先确保你在DNS服务商处已经将你的域名（例如whoami.example.com）解析到你的服务器IP地址上。\n然后，你可以通过访问https://whoami.example.com来测试Traefik的反向代理功能。你需要将whoami.example.com替换为你自己的域名，并确保DNS解析已经正确配置。\n如果一切正常，你就可以访问whoami服务了，你应该能看到一个包含请求信息的页面，类似于下面的内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Hostname: 5ded297e3bf3 IP: 127.0.0.1 IP: ::1 IP: 192.168.144.2 RemoteAddr: 192.168.144.4:33354 GET / HTTP/1.1 Host: whoami.example.com User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, br Accept-Language: en Cdn-Loop: cloudflare; loops=1 Cf-Connecting-Ip: 2600:1700:37a0:70e0:b8c8:f187:aaa1:1d07 Cf-Ipcountry: US Cf-Ray: 948a2cd22a1958ee-ORD Cf-Visitor: {\u0026#34;scheme\u0026#34;:\u0026#34;https\u0026#34;} Priority: u=0, i Sec-Ch-Ua: \u0026#34;Chromium\u0026#34;;v=\u0026#34;136\u0026#34;, \u0026#34;Google Chrome\u0026#34;;v=\u0026#34;136\u0026#34;, \u0026#34;Not.A/Brand\u0026#34;;v=\u0026#34;99\u0026#34; Sec-Ch-Ua-Mobile: ?0 Sec-Ch-Ua-Platform: \u0026#34;macOS\u0026#34; Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: none Sec-Fetch-User: ?1 Upgrade-Insecure-Requests: 1 X-Forwarded-For: 172.69.17.115 X-Forwarded-Host: whoami.example.com X-Forwarded-Port: 443 X-Forwarded-Proto: https X-Forwarded-Server: bca3e42c8951 X-Real-Ip: 172.69.17.115 如果你无法访问https:////whoami.example.com，那么意味着上述配置出了问题。你可以讲traefik.yml注释掉的log.level: \u0026quot;DEBUG\u0026quot;取消注释，这样Traefik会输出更详细的日志信息，帮助你排查问题。\n如果你在访问时遇到SSL证书错误，可能是因为Traefik还没有获取到SSL证书。你可以等待一段时间，Traefik会自动获取SSL证书并配置HTTPS。\n","date":"2025年5月31日","externalUrl":null,"permalink":"/p/%E4%BB%8E%E5%85%AC%E7%BD%91%E8%AE%BF%E9%97%AE%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99%E4%BA%8Ctraefik%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E9%85%8D%E7%BD%AE/","section":"Posts","summary":" 缘起 # 之前我一直使用Nginx作为反向代理来访问我的私人服务器上的网站。当时Nginx是直接安装在电脑上的，没有容器化，这导致我之后想把这些服务迁移到其他电脑上时还得重新配置Nginx、重新配置SSL证书等。\n","title":"从公网访问个人网站（二）——Traefik反向代理配置","type":"post"},{"content":" 缘起 # 这是关于docker容器的第六篇文章，这个系列的其他文章链接如下：\n容器（1）：容器相关知识简介——容器化、docker、docker-compose、Kubernetes / K8s等 容器（2）：docker最佳实践指南——docker-compose和Portainer 容器（3）：docker最佳实践指南——数据卷volume的管理 容器（4）：docker最佳实践指南——容器更新、升级和迁移 容器（5）：docker最佳实践指南——容器更新监测工具WUD（What\u0026rsquo;s Up Docker） 在用了这么长时间docker之后，通过跟ChatGPT的交流，我从最开始几乎不懂docker的运行逻辑，到现在比较熟练地使用docker，渐渐发现了之前的一些误区和坏习惯。为了避免其他人也犯同样的错误，我决定把这些误区和坏习惯记录下来，供大家参考。之后再遇到什么问题也将继续在这里更新。\n误区 # 1. Docker与容器的关系 # 最开始时我以为Docker和容器是同一个概念，但实际上容器是一个更广泛的概念，Docker只是实现容器化的一种工具。正是Docker的出现，才让容器化变得如此简单和流行，可以说Docker是容器化的代表。\n然而，Docker并不是唯一的容器化工具，还有其他一些工具和平台也可以实现容器化，比如Podman、LXC、rkt等。它们各自有自己的优缺点和适用场景，但Docker无疑是目前最流行和最成熟的容器化工具。我之前在从源码编译Proton时曾简单用过Podman，但没有深入了解过它的使用方法和原理，所以这里就不展开了。以后如果深入使用Podman或其他容器化工具时，我会发布相关的文章。\n2. Docker的安装 # 我之前在安装Docker时，使用的是apt install docker命令，这种方式安装的Docker是Ubuntu官方源中的Docker版本，可能不是最新的版本。而且这种方式安装的Docker可能会缺少一些功能或特性，因为它是由Ubuntu官方维护的，而不是Docker官方维护的。\n为了使用最新的Docker版本和功能，建议的安装方式是使用Docker官方社区源提供的Docker，具体可以参见官方文档。一般来说，安装流程如下：\nUbuntu 22.04 # 卸载旧版本的Docker\n1 sudo apt-get remove docker docker-engine docker.io containerd runc 安装依赖项\n1 2 3 4 5 6 sudo apt-get update sudo apt-get install \\ ca-certificates \\ curl \\ gnupg \\ lsb-release 添加Docker官方社区源\n1 2 3 echo \u0026#34;deb [arch=$(dpkg --print-architecture) ] https://download.docker.com/linux/ubuntu \\ $(lsb_release -cs) \\ stable\u0026#34; | sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null 安装Docker\n1 2 sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io Fedora 42 # 我先按照fedora开发者文档中docker的安装方式进行安装，但安装后无法启动docker服务，后来发现是iptables的问题。所以这里记录一下整个流程。\n安装dnf-plugins-core，以便管理dnf仓库：\n1 sudo dnf install dnf-plugins-core 添加docker-ce仓库\n1 sudo dnf config-manager addrepo --from-repofile=\u0026#34;https://download.docker.com/linux/fedora/docker-ce.repo\u0026#34; 注意上述命令只适用于Fedora 41之后的系统，Fedora 40及更老的系统使用下面的命令：\n1 sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo 安装docker\n1 sudo dnf install docker-ce docker-ce-cli containerd.io 可以试试能不能启动docker服务：\n1 sudo systemctl start docker 如果一切正常，接下来就可以设置开机自启：\n1 sudo systemctl enable docker 如果遇到报错说启动失败，可以查看Docker的日志以获取更多信息：\n1 sudo journalctl -u docker -n 50 --no-pager 我遇到的错误是failed to find iptables error=\u0026quot;exec: \\\u0026quot;iptables\\\u0026quot;: executable file not found in $PATH\u0026quot;，即docker找不到iptables命令。iptables一般是跟随系统安装了的，只不过近期的Fedora系统中使用的是iptables-nft作为替代。可以使用下述命令确认：\n1 sudo dnf install iptables 它会告诉你Package \u0026quot;iptables-nft-1.8.11-8.fc42.x86_64\u0026quot; is already installed.，说明iptables-nft已经安装了。然后我们可以查看它安装到哪了：\n1 which iptables-nft 一般会告诉你安装到了/usr/bin/iptables-nft。然后我们需要将iptables命令链接到iptables-nft，以便Docker可以找到它：\n1 sudo ln -s /usr/bin/iptables-nft /usr/bin/iptables 同时，docker还需要ip6tables，同理我们也需要创建ip6tables的链接：\n1 sudo ln -s /usr/bin/iptables-nft /usr/bin/ip6tables 现在应该就可以启动docker服务了：\n1 sudo systemctl start docker 并设置开机自启：\n1 sudo systemctl enable docker 3. docker-compose 和 docker compose # 我在第一篇文章容器（1）：容器相关知识简介——容器化、docker、docker-compose、Kubernetes / K8s等中就提到了docker-compose和docker compose的区别。当时我的建议是使用docker-compose，它是一个独立的命令行工具。这是因为我之前在使用docker compose创建某个容器时，无论如何都不成功，最后还是使用docker-compose创建的容器成功了。忘了具体原因是什么了，但我当时的确是遇到了问题。\n后来发现docker-compose并非由Docker官方维护，Docker官方维护的是作为docker插件的docker compose。而且docker compose的更新速度更快，支持的功能也更多。所以现在我建议使用docker compose，而不是docker-compose。\n坏习惯 # 1. Docker数据 # 之前我没有搞懂Docker中几种不同数据卷的区别，导致数据管理比较混乱。为了避免容器出问题时不会相互相应，或者方便地清理不用的容器，我的做法是为每一个容器创建一个单独的分区，这个容器的所有数据都放在这个分区里。这样做的好处是可以很方便地清理不用的容器，只需要删除这个分区即可。但是这样做的坏处是会浪费很多空间，因为每个分区都需要一定的空间来存放数据，而实际上这些分区中的数据可能并不多。例如给某个容器分配了20G的空间，但实际上这个容器只用了1G的空间，这样就浪费了19G的空间。\n后来在理解了Docker中各种不同的数据卷之后（具体可参见本系列第三篇文章容器（3）：docker最佳实践指南——数据卷volume的管理），我开始使用新的方案：\n将所有容器的docker配置文件（如docker-compose.yml和各种config.yml文件）放在一个目录下，便于使用git管理。 将所有容器需要持久化的数据放在一个大容量的硬盘或者分区中，这样既防止占用过多系统空间，又不会浪费空间。 2. Docker镜像管理 # 以前我在使用Docker时，虽然养成了使用docker-compose.yml文件来管理容器的好习惯，但习惯性地使用latest标签来拉取最新的镜像。这样做的坏处就是时间长了就忘记了之前拉取的镜像是什么版本了，如果过的时间比较长，镜像跨过了较多的版本，再更新时很可能更新失败。而且用latest标签也很难意识到镜像的版本更没更新。\n所以后来我开始在docker-compose.yml文件中指定镜像的版本号，例如：\n1 2 3 4 version: \u0026#39;3\u0026#39; services: nginx: image: nginx:1.23.3 这样做的好处是可以清晰地知道当前使用的镜像版本号，对比官方新发布的镜像版本号，就知道差了多少个版本了。这样在更新时就可以有针对性地更新了。\n另外，我最近还开始使用WUD（What\u0026rsquo;s Up Docker）来监测容器的更新情况，具体可参见本系列第五篇文章容器（5）：docker最佳实践指南——容器更新监测工具WUD（What\u0026rsquo;s Up Docker）。\n3. Docker的权限 # 我最开始使用Docker时，使用的是Ubuntu社区源中的Docker版本，安装时没有添加当前用户到docker组中，所以每次使用Docker时都需要加上sudo命令。后来开始使用docker-compose，依然需要加上sudo命令才能正确运行，否则会提示没有读写权限。\n后来我意识到了这个问题，新建了一个docker组，并将当前用户添加到docker组中。这样就可以在不使用sudo命令的情况下运行Docker了。\n1 2 sudo groupadd docker sudo usermod -aG docker $USER 记得登出后再重新登录，或者运行\n1 newgrp docker 以后再使用docker或者docker compose命令时，就不需要加上sudo命令了。\n","date":"2025年5月21日","externalUrl":null,"permalink":"/p/%E5%AE%B9%E5%99%A86%E4%BD%BF%E7%94%A8docker%E6%97%B6%E7%9A%84%E4%B8%80%E4%BA%9B%E8%AF%AF%E5%8C%BA%E5%9D%8F%E4%B9%A0%E6%83%AF%E5%92%8C%E9%97%AE%E9%A2%98/","section":"Posts","summary":" 缘起 # 这是关于docker容器的第六篇文章，这个系列的其他文章链接如下：\n","title":"容器（6）：使用Docker时的一些误区、坏习惯和问题","type":"post"},{"content":"","date":"2025年5月17日","externalUrl":null,"permalink":"/tags/authelia/","section":"Tags","summary":"","title":"Authelia","type":"tags"},{"content":"","date":"17 五月 2025","externalUrl":null,"permalink":"/en/tags/authentication/","section":"Tags","summary":"","title":"Authentication","type":"tags"},{"content":"","date":"17 五月 2025","externalUrl":null,"permalink":"/en/tags/identity-service/","section":"Tags","summary":"","title":"Identity Service","type":"tags"},{"content":"","date":"17 五月 2025","externalUrl":null,"permalink":"/en/tags/openid-connect/","section":"Tags","summary":"","title":"OpenID Connect","type":"tags"},{"content":"","date":"2025年5月17日","externalUrl":null,"permalink":"/tags/%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81/","section":"Tags","summary":"","title":"身份验证","type":"tags"},{"content":" 缘起 # 之前一直觉得没必要自己搞一个身份验证服务，毕竟在自己部署的服务中，直接创建用户和密码就行了。但是最近准备搭建一个HeadScale（Tailscale的自托管版本）服务器，有几种方式搭建HeadScale的UI界面，但它们似乎都不支持直接创建用户和密码。\n如果不使用身份验证的话，每次都必须输入API密钥，而密钥是一长串不规则的字符串，基本是没法记住的。如果使用身份验证服务，当然可以使用很多现成的身份验证服务，例如谷歌、苹果、微软、GitHub等都提供了身份验证服务。但既然都自建各种服务了，不妨也自建一个身份验证服务，将所有数据都掌握在自己手中。\n身份验证服务的搭建方式有很多，例如使用Authentik、Keycloak、Authelia等。其中Authentik和Keycloak都是比较复杂的身份验证服务，功能也比较强大，但消耗的资源也比较多。我需要的身份验证服务比较简单，只需要能创建少量几个用户就行，所以我选择了Authelia。\nAuthelia简介 # Authelia的特点 # Authelia是一个开源的身份验证和单点登录（SSO）解决方案，支持多种身份验证方式，包括用户名和密码、双因素身份验证（2FA）、WebAuthn等。Authelia可以与多种身份验证后端集成，例如LDAP、Active Directory、MySQL、PostgreSQL等。它还支持多种身份验证协议，例如OAuth2、OpenID Connect、SAML等。\n我在使用HeadScale时，主要需要的是使用OpenID Connect（OIDC）协议来进行身份验证。Authelia支持OpenID Connect协议，可以与HeadScale进行集成。\nAuthelia的主要特点包括：\n开源：Authelia是一个开源项目，可以自由使用和修改。 多种身份验证方式：支持用户名和密码、双因素身份验证（2FA）、WebAuthn等多种身份验证方式。 多种身份验证后端：支持LDAP、Active Directory、MySQL、PostgreSQL等多种身份验证后端。 多种身份验证协议：支持OAuth2、OpenID Connect、SAML等多种身份验证协议。 单点登录（SSO）：支持单点登录，可以在多个应用之间共享身份验证信息。 多种身份验证策略：支持多种身份验证策略，例如基于IP地址、基于时间等。 Authelia的不足之处是，它暂时没有提供图形化的管理界面，所有的配置都需要通过YAML文件进行配置。不过Authelia官方计划在未来的版本中提供一个管理界面（包括图形界面和命令行界面），详见Roadmap，期待一下。\nAuthelia的工作原理 # Authelia只是一个身份验证服务，它本身并不提供任何应用程序或服务。它的工作原理是通过与其他应用程序或服务进行集成来实现身份验证。我在使用Authelia时，主要是将Authelia与Nginx进行集成。Nginx作为反向代理服务器，将用户的请求转发到Authelia进行身份验证，然后再将请求转发到实际的应用程序或服务。\n通常，当用户访问一个网络应用程序时，其过程如下图所示：\n即：\n用户在浏览器上输入网络应用程序的URL，并通过HTTP或HTTPS请求访问到该应用程序的服务器。 服务器由Nginx处理请求，并将请求转发给相应的应用程序。 应用程序处理请求，并将响应返回给Nginx。 Nginx将响应返回给用户的浏览器。 如果这个网络应用程序自带身份验证功能，那么在用户访问应用程序时，可能会弹出一个登录窗口，要求用户输入用户名和密码。如果验证成功，应用程序会将用户的身份信息存储在会话中，并允许用户访问应用程序的其他部分。\n然而，有些网络应用程序并不自带身份验证功能，这时如果想要限制用户的访问权限，就可以通过添加一层身份验证服务来实现。Authelia就是这样一个身份验证服务。如果我们给某个网络应用程序添加了Authelia身份验证服务，那么用户访问该应用程序的过程如下图所示：\n即：\n用户在浏览器上输入网络应用程序的URL，并通过HTTP或HTTPS请求访问到该应用程序的服务器。 服务器由Nginx处理请求，并将请求转发给Authelia进行身份验证。 Authelia验证用户的身份信息， 如果验证成功，则将请求转发给Nginx。Nginx再将请求转发给相应的应用程序。应用程序处理请求，并将响应返回给Nginx。Nginx将响应返回给用户的浏览器。 如果验证失败，则返回一个401 Unauthorized的响应，不会将请求转发给应用程序。用户无法访问应用程序的服务。 使用Docker部署Authelia # 我们可以使用docker compose来部署authelia。我们需要准备一个docker-compose.yml文件和两个配置文件，结构如下：\n1 2 3 4 5 6 authelia/ ├── config │ ├── configuration.yml │ └── users.yml ├── .env └── docker-compose.yml 其中：\ndocker-compose.yml是docker compose的配置文件，定义了Authelia的服务、网络和卷等信息。 config目录下的configuration.yml是Authelia的配置文件，users.yml是用户的配置文件。 .env是环境变量文件，用于定义Authelia的环境变量。 docker-compose.yml # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 version: \u0026#39;3.8\u0026#39; services: authelia: image: authelia/authelia:4.39.3 container_name: authelia restart: always volumes: - ./config:/config:ro - ${DATA_DIR}/data:/data - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro ports: - \u0026#34;9091:9091\u0026#34; networks: - authnet labels: - wud.tag.include=^(\\d+\\.\\d+)\\.\\d+$$ =\u0026gt; $$1 networks: authnet: driver: bridge 其中${DATA_DIR}是一个环境变量，表示数据存储的目录。我们可以在.env文件中定义这个变量，例如：\n1 DATA_DIR=/path/to/data 配置文件 # Authelia的配置文件configuration.yml和用户配置文件users.yml可以参考官方文档进行配置。这里给出一个简单的示例：\nconfiguration.yml # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 ############################################################### # Authelia configuration # ############################################################### server: address: tcp://0.0.0.0:9091/ buffers: read: 4096 write: 4096 endpoints: enable_pprof: false enable_expvars: false disable_healthcheck: false # https://www.authelia.com/configuration/miscellaneous/logging/ log: level: info format: text file_path: /data/authelia.log keep_stdout: true # https://www.authelia.com/configuration/second-factor/time-based-one-time-password/ totp: issuer: authelia.com period: 30 skew: 1 # AUTHELIA_DUO_PLACEHOLDER # https://www.authelia.com/reference/guides/passwords/ authentication_backend: password_reset: disable: false refresh_interval: 5m file: path: /config/users.yml password: algorithm: argon2id iterations: 1 salt_length: 16 parallelism: 8 memory: 256 # blocks this much of the RAM # https://www.authelia.com/overview/authorization/access-control/ access_control: default_policy: deny rules: # - domain: # - \u0026#34;*.example.com\u0026#34; # - \u0026#34;example.com\u0026#34; # policy: bypass # networks: # bypass authentication for local networks # - 10.0.0.0/8 # - 192.168.0.0/16 # - 172.16.0.0/12 - domain: - \u0026#34;*.example.com\u0026#34; - \u0026#34;example.com\u0026#34; policy: one_factor # https://www.authelia.com/configuration/session/introduction/ session: name: authelia_session same_site: lax expiration: 7h inactivity: 5m remember_me: 1M cookies: - domain: \u0026#39;example.com\u0026#39; authelia_url: \u0026#39;https://authelia.example.com\u0026#39; default_redirection_url: \u0026#39;https://example.com\u0026#39; # AUTHELIA_REDIS_PLACEHOLDER # https://www.authelia.com/configuration/security/regulation/ regulation: max_retries: 3 find_time: 10m ban_time: 12h # https://www.authelia.com/configuration/storage/introduction/ storage: # For local storage, uncomment lines below and comment out mysql. https://docs.authelia.com/configuration/storage/sqlite.html # This is good for the beginning. If you have a busy site then switch to other databases. encryption_key: \u0026#39;some-secret-key-for-storage\u0026#39; local: path: /data/db.sqlite3 # https://www.authelia.com/configuration/notifications/introduction/ notifier: disable_startup_check: false # For testing purposes, notifications can be sent in a file. Be sure to map the volume in docker-compose. filesystem: filename: /data/notifications.txt # https://www.authelia.com/configuration/identity-validation/introduction/ identity_validation: reset_password: jwt_secret: \u0026#39;some-secret-key-for-reset-password\u0026#39; # https://www.authelia.com/configuration/identity-providers/openid-connect/provider/ identity_providers: oidc: hmac_secret: \u0026#39;hmac-secret-key\u0026#39; jwks: - key_id: \u0026#39;app-name\u0026#39; algorithm: \u0026#39;RS256\u0026#39; use: sig key: | -----BEGIN PRIVATE KEY----- Your private key here -----END PRIVATE KEY----- clients: - client_id: \u0026#39;client-id\u0026#39; client_name: \u0026#39;Description of the client\u0026#39; client_secret: \u0026#39;client-secret\u0026#39; public: false authorization_policy: one_factor redirect_uris: - \u0026#39;https://example.com/oidc/callback\u0026#39; scopes: - openid - profile - email grant_types: - authorization_code response_types: - code token_endpoint_auth_method: client_secret_post 这里在identity_providers中定义了一个OIDC身份验证提供者，如果有其他应用程序需要使用OIDC身份验证，就可以使用这个提供者。我们可以在clients中定义多个OIDC客户端，每个客户端都有一个client_id和client_secret，用于身份验证。\n具体每个配置项的含义可以参见配置文件中注释的链接，这里不再赘述。\nusers.yml # 1 2 3 4 5 6 7 users: user1: password: \u0026#34;hash-of-user1-password\u0026#34; displayname: \u0026#34;User One\u0026#34; email: user@example.com groups: - admins 这里定义了一个authelia用户，其用户名是user1，注意密码是经过哈希处理的密码，而不是明文密码。我们可以使用authelia提供的命令行工具来生成哈希密码，例如：\n1 docker run authelia/authelia:4.39.3 authelia crypto hash generate bcrypt --password \u0026#39;your-user1-password\u0026#39; 将生成的哈希密码替换到users.yml文件中即可。\n其中的邮箱地址不重要，是不是真实的邮箱地址都可以。groups表示用户所属的组，Authelia支持多种组，例如admins、users等。我们可以在配置文件中定义这些组，并为每个组分配不同的权限。\n启动Authelia # 做好以上准备工作后，我们就可以启动Authelia了。在authelia目录下执行以下命令：\n1 docker compose up -d 这条命令会在后台启动Authelia服务，并将其绑定到9091端口。我们可以在浏览器中访问http://localhost:9091来查看Authelia的Web界面。\n如果一切正常，我们应该能看到Authelia的登录界面。\n输入上面配置的用户名和密码，即user1和your-user1-password，就可以登录了。注意，这里的密码是明文密码，而不是哈希密码。\n登录成功后，我们会被重定向到上面配置的默认重定向URL，即https://example.com。\nAuthelia与其他服务集成 # 上面提到了其他应用程序或服务可以与Authelia进行集成，以实现身份验证。这里以一个简单的Whoami应用程序为例，演示如何与Authelia进行集成。\nWhoami应用程序 # Whoami是一个简单的Web应用程序，它会返回请求的IP地址、请求头、请求方法等信息。我们可以使用Whoami来测试Authelia的身份验证功能。\n部署Whoami应用程序\n我们可以使用docker compose来部署Whoami应用程序。我们需要准备一个docker-compose.yml文件，内容如下：\n1 2 3 4 5 6 7 8 version: \u0026#39;3.8\u0026#39; services: whoami: image: traefik/whoami:v1.11.0 container_name: whoami restart: always ports: - \u0026#34;2001:80\u0026#34; 启动Whoami应用程序\n在whoami目录下执行以下命令：\n1 docker compose up -d 这条命令会在后台启动Whoami服务，并将其绑定到8080端口。我们可以在浏览器中访问http://localhost:2001来查看Whoami的Web界面。\n如果一切正常，我们应该能看到Whoami的界面。\n配置Nginx反向代理\n我们需要配置Nginx作为反向代理，将用户的请求转发到Authelia进行身份验证，然后再将请求转发到Whoami应用程序。\n我们需要准备一个Nginx的配置文件，内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 upstream \u0026lt;whoami.example.com\u0026gt; { server 127.0.0.1:2001; } server { listen 80; server_name whoami.example.com; return 301 https://whoami.example.com$request_uri; } server { listen 443 ssl; server_name whoami.example.com; # ssl 配置 ssl_certificate \u0026lt;/path/to/fullchain.cer\u0026gt;; ssl_certificate_key \u0026lt;/path/to/whoami.example.com.key\u0026gt;; # Step 1: internal location that calls Authelia for authentication location /authelia { internal; proxy_pass http://localhost:9091/api/verify; proxy_set_header Content-Length \u0026#34;\u0026#34;; proxy_pass_request_body off; proxy_set_header X-Original-URL $scheme://$host$request_uri; 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; } # Step 2: protect Whoami via Authelia location / { # This sends an internal subrequest to Authelia for verification auth_request /authelia; # Redirect to Authelia portal if unauthorized error_page 401 =302 https://\u0026lt;authelia.example.com\u0026gt;/?rd=https://$host$request_uri; proxy_pass http://whoami.example.com; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Ssl on; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Frame-Options SAMEORIGIN; } } 这里的server_name需要替换为你自己的域名或IP地址。ssl_certificate和ssl_certificate_key需要替换为你自己的SSL证书和私钥的路径。\n然后重新加载Nginx配置：\n1 sudo systemctl reload nginx 根据上面Nginx的配置可以发现，用Authelia为Whoami验证用户身份的过程如下：\n用户访问https://whoami.example.com，Nginx会根据auth_request指令将请求转发到/authelia，即Authelia进行身份验证。 接着这个验证请求会被转发到Authelia的/api/verify接口。 Authelia会验证用户的身份，如果验证成功，则返回200 OK的响应，Nginx会将请求转发到Whoami应用程序。 如果验证失败，则返回401 Unauthorized的响应，Nginx会将请求重定向到Authelia的登录页面。 测试身份验证 # 现在我们可以在浏览器中访问http://example.com（替换为你自己的域名或IP地址）来测试身份验证功能了。\n如果一切正常，我们应该能看到Authelia的登录界面： 输入用户名和密码后，登录成功后会被重定向到Whoami应用程序的界面。\n如果输入错误的用户名或密码，则会提示“用户名或密码错误”，重新回到Authelia的登录界面。\n总结 # 我们使用Authelia搭建了一个简单的身份验证服务，并以一个简单的Whoami应用程序为例，演示了如何与Authelia进行集成。如果想与其他已经部署的应用程序进行集成，只需要在Nginx中添加相应的配置即可。\n","date":"2025年5月17日","externalUrl":null,"permalink":"/p/%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81%E4%BD%BF%E7%94%A8authelia%E6%90%AD%E5%BB%BA%E7%A7%81%E6%9C%89%E7%9A%84%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81%E6%9C%8D%E5%8A%A1/","section":"Posts","summary":" 缘起 # 之前一直觉得没必要自己搞一个身份验证服务，毕竟在自己部署的服务中，直接创建用户和密码就行了。但是最近准备搭建一个HeadScale（Tailscale的自托管版本）服务器，有几种方式搭建HeadScale的UI界面，但它们似乎都不支持直接创建用户和密码。\n","title":"身份验证：使用Authelia搭建私有的身份验证服务","type":"post"},{"content":" 缘起 # 本文是《私有虚拟网络VPN》系列的第2篇。在第1篇文章中，我基于自己需要在国外访问国内资源的需求，介绍了如何在云服务器上搭建Shadowsocks服务，实现科学上网。本文将介绍VPN的基础概念，并重点介绍如何使用Tailscale来搭建一个私有虚拟网络。本系列的其他文章参见：\n私有虚拟网络VPN（一）：在云服务器上用Shadowsocks搭建VPN VPN的基础概念 # 通常我们提到VPN时，想到的都是通过VPN访问某些被墙的网站，或者是通过VPN访问公司内网、校园网等。实际上，VPN的使用场景远不止于此。VPN可以帮助我们在不同的网络之间建立一个安全的隧道，从而实现数据的加密和隐私保护。它可以用于远程办公、跨国访问、数据传输等场景。\n例如我日常会使用多个电脑，包括家里的，办公室的，还有云服务器上的。这些电脑在不同地区和网络中，例如办公室的电脑在公司的局域网中，云服务器在阿里云和华为云中，这些电脑甚至分布在不同的国家和地区。为了方便地在这些电脑之间进行文件传输和远程访问，VPN就派上用场了。\n使用场景 # 对于我来讲，VPN的使用场景主要有以下几个：\nssh远程登录：虽然也可以通过公网IP直接ssh登录，但是公网IP不稳定，当然可以使用动态域名解析，但动态域名解析更新往往不那么及时，而且动态IP解析服务也可能会挂掉。使用VPN后，所有的电脑都在同一个虚拟网络中，可以直接通过内网IP进行ssh登录。 远程桌面：同样的道理，使用VPN后，所有的电脑都在同一个虚拟网络中，可以直接通过内网IP进行远程桌面登录。 文件传输：使用VPN后，所有的电脑都在同一个虚拟网络中，可以直接通过内网IP进行文件传输。 访问其他电脑的网络服务，这个又分多种场景： 例如我在国外时，网易云音乐会根据我的IP地址判断我在国外，从而无法听某些有地区限制的音乐。使用VPN后，可以通过代理的方式将网易云音乐的请求转发到位于国内的某台电脑上，假装自己在国内，就可以正常听歌了。 例如我在国内时，无法访问谷歌和GitHub等网站。使用VPN后，可以通过代理的方式将谷歌和GitHub的请求转发到位于国外的某台电脑上，假装自己在国外，就可以正常访问了。 VPN的原理 # VPN的原理是通过在公网上建立一个虚拟的专用网络，将不同的网络连接起来。VPN使用加密技术对数据进行加密，从而保证数据的安全性和隐私性。VPN的工作原理可以分为以下几个步骤：\n建立连接：客户端通过VPN客户端软件与VPN服务器建立连接。这个过程通常使用SSL/TLS等加密协议进行加密。 身份验证：客户端向VPN服务器发送身份验证信息，例如用户名和密码。VPN服务器对身份进行验证，确保客户端是合法用户。 建立隧道：身份验证通过后，VPN服务器和客户端之间建立一个加密的隧道。这个隧道可以是基于IPsec、L2TP、PPTP等协议的。 数据传输：客户端和VPN服务器之间的数据通过加密的隧道进行传输。数据在传输过程中被加密，确保数据的安全性和隐私性。 断开连接：当客户端不再需要使用VPN时，可以通过VPN客户端软件断开与VPN服务器的连接。此时，VPN隧道也会被关闭。 Tailscale # Tailscale简介 # Tailscale是一个基于WireGuard的VPN解决方案，它可以帮助我们快速地搭建一个私有虚拟网络。Tailscale的特点是简单易用、跨平台支持、自动穿透NAT等。它可以在Windows、macOS、Linux、iOS和Android等多个平台上使用。\nWireGuard是一个现代化的VPN协议，它使用最新的加密技术，具有高性能和低延迟的特点。WireGuard的设计目标是简单、安全和高效。它的代码量非常小，只有几千行代码，相比于其他VPN协议（例如OpenVPN、IPsec等）要小得多。这使得WireGuard更容易审计和维护。\nTailscale的工作原理 # Tailscale的工作原理是通过一个中心化的控制服务器来管理所有的客户端。每个客户端在连接到Tailscale时，会向控制服务器注册自己的IP地址和公钥。控制服务器会将这些信息分发给其他客户端，从而实现点对点的连接。\nTailscale使用WireGuard协议来建立加密的隧道。每个客户端都有一个WireGuard的私钥和公钥。通过控制服务器，客户端可以获取其他客户端的公钥，从而建立点对点的连接。\nTailscale还支持自动穿透NAT和防火墙，这使得它可以在各种网络环境中使用。它使用了STUN和TURN等技术来实现NAT穿透。\nTailscale还支持访问控制和身份验证。它可以与Google、Microsoft、GitHub等身份提供商集成，从而实现单点登录（SSO）。这使得Tailscale可以与现有的身份管理系统无缝集成。\nTailscale的优缺点 # Tailscale的优点：\n简单易用：Tailscale的安装和配置非常简单，只需要几分钟就可以搭建一个私有虚拟网络。 跨平台支持：Tailscale支持多个平台，包括Windows、macOS、Linux、iOS和Android等。 自动穿透NAT：Tailscale可以自动穿透NAT和防火墙，无需手动配置端口转发。 高性能：Tailscale使用WireGuard协议，具有高性能和低延迟的特点。 安全性：Tailscale使用最新的加密技术，确保数据的安全性和隐私性。 访问控制：Tailscale支持访问控制和身份验证，可以与现有的身份管理系统集成。 低成本：Tailscale的免费版可以支持最多20个设备，适合个人和小型团队使用。 开源：Tailscale的核心组件是开源的，可以在GitHub上找到。 社区支持：Tailscale有一个活跃的社区，可以提供技术支持和帮助。 文档齐全：Tailscale的文档非常详细，提供了丰富的使用案例和教程。 监控和日志：Tailscale提供了监控和日志功能，可以帮助我们排查问题和优化性能。 Tailscale的缺点：\n中心化：Tailscale使用中心化的控制服务器来管理客户端，这可能会导致单点故障和隐私问题。 依赖网络：Tailscale需要依赖网络连接，如果网络不稳定，可能会影响VPN的性能。 Tailscale的使用方法 # Tailscale有两种使用方式，一种是利用Tailscale官方的控制服务器，另一种是自建控制服务器。对于大多数用户来说，使用官方的控制服务器就足够了。但是如果你对隐私和安全性有更高的要求，或者有一些特殊的需求，需要更高的灵活性，就需要自建控制服务器了。\n这里简单介绍一下第一种使用方式，即使用Tailscale官方的控制服务器。对于第二种使用方式，即自建控制服务器，这里暂不做详细介绍，之后会单独写一篇文章介绍。\n注册账号：访问Tailscale官网注册一个账号。可以使用谷歌、微软、GitHub等账号登录。\n安装Tailscale：在需要连接的设备上安装Tailscale。可以使用以下命令安装：\nWindows：下载Windows安装包并安装。\nmacOS：下载macOS安装包并安装。\nLinux：使用以下命令安装：\n1 curl -fsSL https://tailscale.com/install.sh | sh iOS和Android：在App Store或Google Play中搜索\u0026quot;Tailscale\u0026quot;并安装。\n登录：安装完成后，Tailscale会打开一个浏览器窗口，要求登录，使用注册的账号，或者通过谷歌、微软、GitHub等第三方账号登录。\n连接：登录后，Tailscale会自动为设备分配一个IP地址。可以在Tailscale应用中查看设备列表和IP地址：\n访问其他设备：在同一个Tailscale网络中的设备可以直接通过IP地址进行访问。例如，可以使用ssh命令登录到上面的fedora设备：\n1 ssh user@100.65.0.100 你也可以通过Tailscale提供的DNS名称访问其他设备，例如：\n1 ssh user@fedora.abcdefgh.ts.net 设置访问控制：可以在Tailscale应用中设置访问控制规则，例如限制某些设备之间的访问权限。\n断开连接：当不再需要使用Tailscale时，可以在Tailscale应用中断开连接。\n","date":"2025年5月4日","externalUrl":null,"permalink":"/p/%E7%A7%81%E6%9C%89%E8%99%9A%E6%8B%9F%E7%BD%91%E7%BB%9Cvpn%E4%BA%8C%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5%E5%92%8C%E4%BD%BF%E7%94%A8tailscale%E6%90%AD%E5%BB%BA%E6%8C%87%E5%8D%97/","section":"Posts","summary":" 缘起 # 本文是《私有虚拟网络VPN》系列的第2篇。在第1篇文章中，我基于自己需要在国外访问国内资源的需求，介绍了如何在云服务器上搭建Shadowsocks服务，实现科学上网。本文将介绍VPN的基础概念，并重点介绍如何使用Tailscale来搭建一个私有虚拟网络。本系列的其他文章参见：\n","title":"私有虚拟网络VPN（二）：基础概念和使用Tailscale搭建指南","type":"post"},{"content":" 缘起 # 之前一直使用微软的OneNote来管理笔记，OneNote功能强大，可以跟踪笔记的修改历史，可以使用OneDrive来同步笔记。但是OneNote的缺点也不少，最令我无法忍受的就是其默认字体是Calibri，而且无法修改（或者说即使修改了也永远不会生效），我真的不明白，为什么用户修改个默认字体这么简单的事，这么多年来一直就无法解决。我每次都得不厌其烦地把字体改成Times New Roman。\n在开始自建各种服务之后，我就开始寻找一个可以自建的笔记管理软件。在尝试过Joplin、Trilium、思源笔记、Logseq等软件之后，我最终选择了Obsidian。Obsidian的优点是：\n界面简洁，功能强大，支持Markdown语法 支持插件，可以扩展功能 支持云端同步，可以使用Nextcloud来同步笔记 支持多平台，可以在Windows、Linux、Mac、Android等平台上使用 笔记文件以纯文本文件存储，方便备份和迁移 支持自定义主题和样式，可以根据自己的喜好来修改界面 这些基本就满足了我对笔记管理软件的所有需求了。\n本文是Obsidian系列的第一篇，主要介绍Obsidian的安装和配置，以及如何使用Nextcloud来同步Obsidian的笔记。本系列的其他文章包括：\nObsidian（二）：网页版Obsidian的部署与使用 前置条件 # docker和docker compose Nextcloud 关于上述软件和工具的安装和准备，详情请参见\u0026ldquo;使用Nextcloud和docker自建个人云网盘\u0026rdquo;\nObsidian # Obsidian简介 # Obsidian官方的说法是它是一个Markdown编辑器，也是一个知识库软件。我不太需要Markdown编辑器的功能，基本只把它当作一个笔记软件来用。Obsidian的笔记文件是以Markdown格式存储的，所有的笔记文件都存储在一个文件夹中，文件名就是笔记的标题。Obsidian支持双向链接，可以很方便地在不同的笔记之间建立链接。\nObsidian支持云端同步，你可以注册Obsidian账号，订阅Obsidian同步服务，起步价是每月$4。我之前已经自建了Nextcloud，所以就不需要花这个钱了，直接使用Nextcloud来同步Obsidian的笔记。\nObsidian安装 # Obsidian支持Windows、Linux、Mac、Android全平台，而且提供了浏览器插件，可以在浏览器中使用Obsidian。直接去Obsidian官网下载对应平台的安装包即可。\nObsidian配置 # 创建笔记库 # 安装完成后，打开Obsidian，会让我们选择笔记存放的位置和名称。我们选择一个文件夹作为笔记存放的位置，比如/home/user/Documents/Obsidian，然后点击“创建新库”，就可以创建一个新的笔记库了。这里我的笔记库名字是\u0026quot;Jin Li Obsidian\u0026quot;：\n创建完成后，Obsidian会将我们创建的笔记都放到这个文件夹中。我们可以在这个文件夹中创建新的笔记，或者将已有的笔记复制到这个文件夹中。\n安装并配置插件 # 由于我们想使用Nextcloud来同步Obsidian的笔记，所以我们需要安装一个插件来实现这个功能。Obsidian的插件分为两类：\n核心插件（Core Plugins）：Obsidian自带的插件，可以直接启用 社区插件（Community Plugins）：第三方开发的插件，需要手动安装 我们需要安装的插件是“Remotely Save”，它是一个社区插件。Obsidian中社区插件默认是不启用的，我们需要先手动启用它。\n点击左下角的设置按钮，打开设置界面 点击“社区插件”，然后启用它，就可以搜索和安装社区插件了。这里我们搜索“Remotely Save”插件： 点击进去会有安装选项，安装完成后，点击启用即可。\n安装成功后你会在“已安装插件”中看到“Remotely Save”插件： 点击齿轮图标进入设置界面：\n主要需要设置的选项有：\nChoose A Remote Service：选择\u0026quot;Webdav\u0026quot;\nServer Address：填写Nextcloud的WebDAV地址，比如https://\u0026lt;your-nextcloud-domain\u0026gt;/remote.php/dav/files/\u0026lt;username\u0026gt;/Obsidian/。你可以登录Nextcloud，打开你想要存放Obsidian笔记的文件夹，点击左下角的齿轮图标打开“文件设置”，然后在“WebDAV”中可以看到WebDAV地址：\n复制这个地址，粘贴到Obsidian的“Server Address”中即可。\nUsername：填写Nextcloud的用户名\nPassword：填写Nextcloud的密码\n设置完成后就可以将在Obsidian中创建的笔记同步到nextcloud中了。我们可以在Obsidian中创建一个新的笔记，然后点击“Remotely Save”插件的图标，就可以将笔记保存到Nextcloud中了： 点击后右上角会弹出同步成功的提示，右下角也会显示同步成功的状态。\n登录Nextcloud，打开你存放Obsidian笔记的文件夹，就可以看到你在Obsidian中创建的笔记了： 多端同步 # Obsidian是一个全平台的笔记管理软件，你也可以在其他电脑或手机上使用它。在其他设备上安装时需要注意以下几点：\n在其他设备上安装完Obsidian，第一次打开会让你选择创建笔记库，这里填入的文件库名字需要与之前的相同，例如\u0026quot;Jin Li Obsidian\u0026quot;。\n在其他设备上同样安装并启用“Remotely Save”插件。\n在其他设备上配置“Remotely Save”插件时，填写的Nextcloud的WebDAV地址、用户名和密码需要与之前的相同。如果你不想一个个输入，\u0026ldquo;Remotely Save\u0026quot;插件提供了导出和导入配置的功能，你可以在一个设备上配置好后，导出配置文件：\n导出的配置是一串以\u0026quot;obsidian://remotely-save\u0026quot;开头的字符串，你把它复制下来，然后在其他设备上打开Obsidian，点击“Remotely Save”插件的设置按钮，点击“Import Settings”，粘贴刚才复制的字符串并点击确认，就可以导入配置了：\n最后关掉插件的设置界面，回到Obsidian，再点击左侧边栏的“Remotely Save”插件图标，Obsidian就会把nextcloud中的笔记同步到新设备上了。\n","date":"2025年5月1日","externalUrl":null,"permalink":"/p/obsidian%E4%B8%80%E5%85%A8%E5%B9%B3%E5%8F%B0%E7%AC%94%E8%AE%B0%E7%AE%A1%E7%90%86%E8%BD%AF%E4%BB%B6/","section":"Posts","summary":" 缘起 # 之前一直使用微软的OneNote来管理笔记，OneNote功能强大，可以跟踪笔记的修改历史，可以使用OneDrive来同步笔记。但是OneNote的缺点也不少，最令我无法忍受的就是其默认字体是Calibri，而且无法修改（或者说即使修改了也永远不会生效），我真的不明白，为什么用户修改个默认字体这么简单的事，这么多年来一直就无法解决。我每次都得不厌其烦地把字体改成Times New Roman。\n","title":"Obsidian（一）：全平台笔记管理软件","type":"post"},{"content":"","date":"26 四月 2025","externalUrl":null,"permalink":"/en/tags/container-upgrade/","section":"Tags","summary":"","title":"Container Upgrade","type":"tags"},{"content":"","date":"2025年4月26日","externalUrl":null,"permalink":"/tags/docker-compose/","section":"Tags","summary":"","title":"Docker-Compose","type":"tags"},{"content":"","date":"2025年4月26日","externalUrl":null,"permalink":"/tags/wud/","section":"Tags","summary":"","title":"WUD","type":"tags"},{"content":"","date":"2025年4月26日","externalUrl":null,"permalink":"/tags/%E5%AE%B9%E5%99%A8/","section":"Tags","summary":"","title":"容器","type":"tags"},{"content":" 缘起 # 这是介绍docker容器的第五篇文章。本系列其他文章链接如下：\n容器（1）：容器相关知识简介——容器化、docker、docker-compose、Kubernetes / K8s等 容器（2）：docker最佳实践指南——docker-compose和Portainer 容器（3）：docker最佳实践指南——数据卷volume的管理 容器（4）：docker最佳实践指南——容器更新、升级和迁移 容器（6）：使用Docker时的一些误区、坏习惯和问题 上一篇文章提到了如何更新或升级容器。我在更新docker容器时发现，其中很多容器都已经两三年没更新了，版本跨越太多，这就导致有些容器没法直接更新。而有些容器则更新不多，可以直接更新。由于各个容器更新频率不同，而且创建的容器优比较多，我不想隔几天就手动去检查一下容器有没有更新可用。于是我就想到了一个问题：有没有工具可以自动监测容器的更新呢？\n当然是有的，比如WatchTower，但WatchTower是自动更新容器的工具，我并不想让它自动更新，我只想知道容器有没有更新可用，所以就一直没用。最近，我发现有个叫WUD（What\u0026rsquo;s Up Docker）的工具可以监测容器的更新，并且简单易用，就打算尝试一下。\n前提 # 已安装Docker和docker-compose 了解Docker的基本概念和基本用法 使用docker-compose管理容器（如果你没有使用docker-compose管理容器，也不想使用docker-compose管理容器，那么本篇文章仅供参考） WUD（What\u0026rsquo;s Up Docker）简介 # WUD（What\u0026rsquo;s Up Docker）是一个开源的Docker容器更新监测工具，它可以监测Docker Hub上的容器镜像是否有更新，并且可以通过邮件、Slack等方式通知用户。\nWUD安装与使用 # 安装WUD # WUD的安装十分简单，按照官方文档的说明即可。这里我们使用docker-compose来安装WUD，docker-compose.yml文件如下：\n1 2 3 4 5 6 7 8 services: whatsupdocker: image: getwud/wud container_name: wud volumes: - /var/run/docker.sock:/var/run/docker.sock ports: - 3000:3000 启动后在浏览器中访问http://localhost:3000即可。\n创建管理账户 # 但上述方法安装的WUD是没有管理账户的，在本地访问尚可，但如果想把服务暴露到公网，就需要创建一个管理账户。WUD提供了两种方法来创建管理账户：\n使用环境变量将用户名和密码传递给WUD容器 使用OpenidConnect（OIDC）来创建管理账户 详见[官方文档](https://getwud.github.io/wud/#/configuration/authentications/）。\n第一种方法非常简单便捷，我就直接使用了。我们只需要在docker-compose.yml文件中添加以下环境变量即可：\n1 2 3 4 5 6 7 8 9 10 11 services: whatsupdocker: image: getwud/wud container_name: wud volumes: - /var/run/docker.sock:/var/run/docker.sock ports: - 3001:3000 environment: - WUD_AUTH_BASIC_ADMIN_USER=your_admin_name - WUD_AUTH_BASIC_ADMIN_HASH=your_admin_password_hash 其中密码是以hash的形式存储的，我们可以使用hrpasswd命令来生成密码的hash：\n1 htpasswd -nib admin doe 如果你没有安装htpasswd命令，可以在这个网站上生成密码的hash。\n需要注意的是，生成的hash值以用户名开头，我们只需要取出冒号后面的部分即可。冒号后面的部分可能含有$符号，所以我们需要用$$来替换掉$符号。比如原hash值为$apr1$2c4a3d5e$e0f8b7c6d7f8b7c6d7f8b7c6，那么我们需要将其替换为$$apr1$$2c4a3d5e$$e0f8b7c6d7f8b7c6d7f8b7c6。\n再重新启动WUD容器，进入浏览器，可以看到如下登录界面： 输入你的用户名和密码就可以登录了，注意输入的是密码，不需要输入hash值。\nWUD监测容器更新 # 通过网页界面可以监测容器的更新状态，登录后可以看到如下界面：\n可见WUD检测到了37个容器，其中10个有更新的版本可用。点击进去可用查看详情：\n可以看到容器的当前版本号以及最新的版本号，我们可以根据需求来决定是否更新容器。\n设置WUD监测规则 # 在WUD中，我们看到的容器状态可能是这样的：\n这里面有两个问题：\n我们的容器使用的标签(tag)是数字形式的，但WUD会监测所有标签，包括一些不规则的标签，例如图中authelia最新的标签是feat-i18n-lang-attr。如果我们只希望监测数字形式的标签，就需要设置监测规则。 当我们部署一个服务时，可能会使用多个容器来部署这个服务，例如chevereto的服务就需要两个容器来部署，一个是chevereto-free，一个是chevereto-free-database。我们不需要监测所有的容器，只需要监测chevereto-free这个容器就可以了。否则如果监测到chevereto-free-database有更新了，而chevereto-free暂无更新可用，我们只更新chevereto-free-database，可能导致兼容性的问题。我们也需要设置监测规则来避免这种情况。 包含某些标签 # 对于第一种情况，我们可以在对应容器的docker-compose.yml文件中添加wud.tag.include来告诉WUD我们只希望监测哪些标签。比如我们只希望监测数字形式如xx.yy.zz形式的标签，那么我们可以添加如下配置：\n1 2 3 4 5 services: some_service: image: some_image labels: - wud.tag.include=^\\d+\\.\\d+\\.\\d+$$ 这种方式是正则表达式的方式来匹配标签的，^表示开头，\\d表示数字，+表示一个或多个，\\.表示点号，$$表示结尾。这样就可以匹配到数字形式的标签了。\nxx.yy.zz形式的标签中，一般xx表示主版本号，yy表示次版本号，zz表示修订号。我们可以根据需求来设置监测规则，比如只监测主版本号和次版本号的更新，忽略掉修订号，那么我们可以将正则表达式改为：\n1 2 3 4 5 services: some_service: image: some_image labels: - wud.tag.include=^(\\d+\\.\\d+)\\.\\d+$$ =\u0026gt; $$1 这样我们把xx.yy用括号括起来表示为一个分组，然后在=\u0026gt;后面指定了我们只希望监测xx.yy的更新。这样就可以只监测主版本号和次版本号的更新了。\n不监测某些容器 # 对于第二种情况，我们可以在对应容器的docker-compose.yml文件中添加wud.watch来告诉WUD我们是否监测这个容器。比如我们不希望监测chevereto-free-database这个容器，那么我们可以添加如下配置：\n1 2 3 4 5 6 7 services: chevereto-free-app: image: chevereto-free chevereto-free-database: image: chevereto-free-database labels: - wud.watch=false 这样就可以不监测chevereto-free-database这个容器了。\n","date":"2025年4月26日","externalUrl":null,"permalink":"/p/%E5%AE%B9%E5%99%A85docker%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E6%8C%87%E5%8D%97%E5%AE%B9%E5%99%A8%E6%9B%B4%E6%96%B0%E7%9B%91%E6%B5%8B%E5%B7%A5%E5%85%B7wudwhats-up-docker/","section":"Posts","summary":" 缘起 # 这是介绍docker容器的第五篇文章。本系列其他文章链接如下：\n","title":"容器（5）：docker最佳实践指南——容器更新监测工具WUD（What's Up Docker）","type":"post"},{"content":"","date":"2025年4月26日","externalUrl":null,"permalink":"/tags/%E5%AE%B9%E5%99%A8%E5%8D%87%E7%BA%A7/","section":"Tags","summary":"","title":"容器升级","type":"tags"},{"content":"","date":"24 四月 2025","externalUrl":null,"permalink":"/en/tags/container-migration/","section":"Tags","summary":"","title":"Container Migration","type":"tags"},{"content":" 缘起 # 这是介绍docker容器的第四篇文章。本系列其他文章链接如下：\n容器（1）：容器相关知识简介——容器化、docker、docker-compose、Kubernetes / K8s等 容器（2）：docker最佳实践指南——docker-compose和Portainer 容器（3）：docker最佳实践指南——数据卷volume的管理 容器（5）：docker最佳实践指南——容器更新监测工具WUD（What\u0026rsquo;s Up Docker） 容器（6）：使用Docker时的一些误区、坏习惯和问题 在创建了容器之后，我们可能需要对容器进行更新或者升级，以便使用新的功能或者修复bug。如果我们更换了服务器，或者升级了电脑的硬件，则可能需要将容器迁移到新服务器上，或者迁移到新硬盘上。\n事先声明：如果容器的官方文档中有关于更新、升级或者迁移的说明，请优先参考官方文档。本文仅针对一般性的容器更新、升级和迁移进行介绍。\n前提 # 已安装Docker和docker-compose 了解Docker的基本概念和基本用法 使用docker-compose管理容器（如果你没有使用docker-compose管理容器，也不想使用docker-compose管理容器，那么本篇文章仅供参考） 更改容器 # 无论是更新、升级还是迁移容器，其本质都是要对原容器做一些改动，这里我们需要明确是对容器的哪一部分做了改动。一般来讲，对容器的改动涉及到以下几个部分：\n镜像：容器是由镜像创建的，如果要用到容器的新功能、修复bug，或者想要还原回容器的旧功能，都需要对镜像进行更新或者替换。这个通常是最常见，也是最方便的改动。因为普通用户通常都是镜像的使用者，而不是镜像的创建者。我们只需要从仓库拉取新的镜像，或者删除旧的镜像，不需要关心太多其他的。 容器配置：容器的配置是指容器的运行参数，例如环境变量、端口映射、数据卷挂载等。一般来讲： 在更新或升级容器时，我们需要查阅官方文档或者更新说明，看是否新增了新的配置项，或者修改了旧的配置项。我们需要根据官方文档或者更新说明来修改容器的配置。否则，更新或升级容器时不需要修改容器的配置。 在迁移容器时，我们需要根据新的服务器的环境来修改容器的配置，例如修改数据卷的挂载路径、端口映射等。 数据：这里的数据分为两类 软件配置数据：这里的软件配置是指容器中运行的软件的配置，例如数据库的配置、web服务的配置等。一般来讲，软件的配置是存放在容器专门创建的数据卷中的， 在更新或升级容器时，我们往往不需要做什么操作，升级后的容器回继承之前的软件配置。 在迁移容器时，我们需要将储存软件配置的数据卷迁移到新的服务器上，或者新的硬盘上。 用户数据：用户的数据是指用户上传到容器中的数据，例如用户上传的图片、视频等。以及软件用户本身的帐户、密码之类的数据。一般来讲，用户的数据是以数据库或者以文件夹的形式存放在数据卷中的，我们在更新或者迁移容器时，最关注的就是这些数据。 更新、升级容器的基本流程 # 更新、升级容器的基本流程如下：\n确定版本 # 在更新或者升级容器之前，我们需要确定要更新或者升级到哪个版本。一般来讲，容器的版本号是由镜像的标签（tag）来决定的。我们可以通过以下命令来查看当前容器的版本号：\n1 docker inspect \u0026lt;container_name\u0026gt; | grep Image 或者\n1 docker inspect \u0026lt;container_name\u0026gt; | grep RepoTags 或者在Portainer中查看容器的详细信息，找到镜像的标签（tag）信息。\n需要注意的是，很多容器在创建时往往会使用latest标签，这个标签是指最新的版本，而且网上很多教程中也会给镜像加上latest标签。我之前创建容器时也一直这样用，但后来发现这其实并不是一个好习惯，因为如果长时间没有更新容器，那么我们的容器和实际上仓库里的最新版本可能会有很大的差距。我们在更新或者升级容器时，就可能会遇到一些问题。而且我们也不能直观地看到我们的容器和最新的版本之间的差距。在备份和迁移容器时也不好标记。\n因此，我建议在创建容器时，尽量使用具体的版本号，而不是latest标签。这样我们想更新容器时，去官方仓库查一下最新的版本号是多少，如果跟旧容器的版本号差距不大，就可以直接更新了。如果差距很大，我们可能需要一个版本一个版本地更新。\n更新升级容器 # 在使用docker-compose管理容器时，更新或者升级容器一般分3步：\n停止容器 1 sudo docker-compose stop 或者\n1 sudo docker-compose down 修改docker-compose.yml文件 修改镜像的标签（tag），将版本号更改为新的版本号 修改其他配置项（如果需要） 重新创建容器 1 sudo docker-compose up -d 这个命令在检测到我们更新了镜像版本号时，会自动拉取新的镜像，并重新创建容器。\n如果当前容器的版本号和最新的版本号差距很大，我们可能需要一个版本一个版本地更新。例如我之前在创建了GitLab的容器后，将近两年都没更新过，结果和最新版差了2个大版本，十几个小版本。这时如果直接更新到最新版本，就会因为兼容性等问题导致容器无法启动。GitLab在这方面其实做得还不错，如果你查看log，会发现它会提示你：\n1 2 3 4 5 It seems you are upgrading from 16.0.4-ee to 16.7.0. It is required to upgrade to the latest 16.3.x version first before proceeding. Please follow the upgrade documentation at https://docs.gitlab.com/ee/update/#upgrade-paths Thank you for using GitLab Docker Image! Current version: gitlab-ee=16.7.0-ee.0 这说明我们不能直接从16.0.4升级到16.7.0，而是需要先升级到16.3.x版本。但我在实际测试中发现，从16.0.4似乎也不能直接升级到16.3.0，只能先升级到16.1.0。总之，最后花了我近两个小时到时间，逐步更新了十几个版本，才将GitLab更新到最新版本。\n迁移容器 # 迁移容器更复杂一些，主要是因为我们需要把数据卷迁移到新电脑或新硬盘上，还要保证容器能正确地读取数据卷中的数据。\n确定容器镜像版本、数据卷位置 # 在迁移容器前，我们需要确定容器的镜像版本，以免出现不兼容的问题。镜像版本号的问题在上文已经说过了，我们在创建容器时尽量使用具体的版本号，而不是latest标签，以便在迁移时能清楚地知道容器的版本号。如果不知道，可以用上面提到的命令查看。\n数据卷的位置可以在docker-compose.yml文件中查看，有以下几种情况：\n命名卷：命名卷的名称是唯一的，可以通过名称来访问数据卷。命名卷的数据存储在宿主机上的/var/lib/docker/volumes目录下。\n绑定挂载：绑定挂载的数据卷，其位置就是定义中冒号前的那个路径。如果是只读的绑定挂载，我们在迁移时一般不需要管它。\n临时卷：临时卷的数据存储在宿主机上的/var/lib/docker/containers目录下。临时卷的数据在容器停止或删除时会被删除，因此我们在迁移时一般不需要管它。\n匿名卷：我们应该避免使用匿名卷。如果非要使用，其数据在容器停止或删除时会被删除，一般也不需要管它。\n迁移数据卷 # 迁移数据卷和普通的迁移文件有所不同，主要是需要注意数据卷的权限问题。如果迁移后某些文件的权限变了，可能会导致容器无法访问数据卷中的文件。我们可以使用rsync命令来迁移数据卷，具体命令如下：\n1 rsync -aHAX --progress /path/to/source/ /path/to/destination/ rsync命令是一个用于文件同步和备份的工具，可以在本地或远程计算机之间高效地复制和同步文件。它支持增量备份、压缩传输、保留文件权限等功能，非常适合用于迁移数据卷。 其中，-a表示以归档模式复制文件，保留文件的权限、时间戳等信息；-H表示保留硬链接；-A表示保留ACL权限；-X表示保留扩展属性；--progress表示显示复制进度。\n另外：\n如果使用上述命令时遇到某些文件无法复制的问题，可以sudo来提升权限，例如：\n1 sudo rsync -aHAX --progress /path/to/source/ /path/to/destination/ 如果目的地是本地的另一个目录，其中已经有一些文件了，rsync会自动跳过这些文件，只复制源目录中不存在的文件。如果目标目录中存在一些源目录中不存在的文件，你想把它们删除，可以加上--delete选项。\n1 rsync -aHAX --progress --delete /path/to/source/ /path/to/destination/ 如果源文件和目标文件不在同一台机器上，可以使用远程电脑的地址来复制，例如：\n1 rsync -aHAX --progress /path/to/source/ user@remote:/path/to/destination/ 如果远程电脑使用的不是22端口，可以使用-e选项指定ssh的端口，例如：\n1 rsync -aHAX --progress -e \u0026#34;ssh -p 2222\u0026#34; /path/to/source/ user@remote:/path/to/destination/ 如果远程电脑使用的是ssh密钥登录，可以使用-e选项指定ssh的密钥，例如：\n1 rsync -aHAX --progress -e \u0026#34;ssh -i /path/to/key\u0026#34; /path/to/source/ user@remote:/path/to/destination/ 迁移docker-compose.yml文件 # 之所以在迁移完数据之后再迁移docker-compose.yml文件，是因为在新电脑或新硬盘上的数据卷路径可能会发生变化。我们需要在迁移完数据之后，再修改docker-compose.yml文件中的数据卷路径。\n这一步其实很简单，主要是别忘了修改数据卷的路径。\n重新创建容器 # 在迁移完数据和docker-compose.yml文件之后，我们可以使用以下命令重新创建容器了：\n1 sudo docker-compose up -d 如果是第一次启动容器，或者需要重新构建容器，可以使用：\n1 sudo docker-compose up -d --build 如果一切顺利，容器就迁移成功了！\n","date":"2025年4月24日","externalUrl":null,"permalink":"/p/%E5%AE%B9%E5%99%A84docker%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E6%8C%87%E5%8D%97%E5%AE%B9%E5%99%A8%E6%9B%B4%E6%96%B0%E5%8D%87%E7%BA%A7%E5%92%8C%E8%BF%81%E7%A7%BB/","section":"Posts","summary":" 缘起 # 这是介绍docker容器的第四篇文章。本系列其他文章链接如下：\n","title":"容器（4）：docker最佳实践指南——容器更新、升级和迁移","type":"post"},{"content":"","date":"2025年4月24日","externalUrl":null,"permalink":"/tags/%E5%AE%B9%E5%99%A8%E8%BF%81%E7%A7%BB/","section":"Tags","summary":"","title":"容器迁移","type":"tags"},{"content":"","date":"23 四月 2025","externalUrl":null,"permalink":"/en/tags/data-volume/","section":"Tags","summary":"","title":"Data Volume","type":"tags"},{"content":" 缘起 # 这是介绍docker容器的第三篇文章。本系列文章链接如下：\n容器（1）：容器相关知识简介——容器化、docker、docker-compose、Kubernetes / K8s等 容器（2）：docker最佳实践指南——docker-compose和Portainer 容器（4）：docker最佳实践指南——容器更新、升级和迁移 容器（5）：docker最佳实践指南——容器更新监测工具WUD（What\u0026rsquo;s Up Docker） 容器（6）：使用Docker时的一些误区、坏习惯和问题 前两篇文章分别介绍了容器的基本概念和使用方法，以及使用容器的最佳实践方式。\n之前的两篇文章中没有着重介绍数据卷（volume）的相关问题，但数据卷的管理其实是容器使用和管理中非常重要的一部分。通常来说，容器中的数据是临时的，当容器停止或删除时，容器中的数据也会被删除。这一机制使得容器非常轻量级，用户不用担心容器中的数据会占用过多的存储空间。\n但在实际使用容器来部署某些服务时，我们可能需要将容器中的某些数据持久化，即使容器停止或删除，这些数据也不会丢失。例如之前的文章《使用Chevereto-free搭建私人图床》中，我们介绍了如何搭建私有图床，这时我们就需要将Chevereto中的图片数据持久化到宿主机上，以便在容器停止或删除后，仍然可以访问这些图片。或者将来我们将docker迁移到其他机器上时，也可以将这些数据迁移过去。\n数据卷是Docker中用于持久化数据的机制，可以将容器中的数据保存到宿主机上，或者在多个容器之间共享数据。\n前提 # 已安装Docker和docker-compose 了解Docker的基本概念和基本用法 使用docker-compose管理容器（如果你没有使用docker-compose管理容器，也不想使用docker-compose管理容器，那么本篇文章仅供参考） 数据卷（volume）简介 # 数据卷（volume）是Docker中用于持久化数据的机制，可以将容器中的数据保存到宿主机上，或者在多个容器之间共享数据。数据卷可以在容器之间共享，也可以在宿主机和容器之间共享。 数据卷的使用可以避免容器停止或删除时，容器中的数据被删除的问题。数据卷的使用可以提高容器的性能，因为数据卷是直接挂载到宿主机上的，而不是通过网络访问宿主机上的数据。\n当使用docker-compose来管理容器时，我们可以在docker-compose.yml文件中使用volumes字段来定义数据卷及其使用方式。\n一般来说，docker中的数据卷有以下几种类型：\n命名卷（named volume）：命名卷是Docker中最常用的数据卷类型，可以在多个容器之间共享数据。命名卷的名称是唯一的，可以通过名称来访问数据卷。命名卷的数据存储在宿主机上的/var/lib/docker/volumes目录下。 在docker-compose中使用命名卷时，可以在docker-compose.yml文件中使用volumes字段来定义命名卷，例如：\n1 2 3 4 5 6 7 8 version: \u0026#39;3\u0026#39; services: app: image: nginx volumes: - my_volume:/usr/share/nginx/html volumes: my_volume: 注意，命名卷在services中定义之后，还必须在volumes字段中定义。\n绑定挂载（bind mount）：绑定挂载是将宿主机上的目录挂载到容器中，可以在宿主机和容器之间共享数据。绑定挂载的数据存储在宿主机上的指定目录下。 在docker-compose中使用绑定挂载时，可以在docker-compose.yml文件中使用volumes字段来定义绑定挂载，例如：\n1 2 3 4 5 6 version: \u0026#39;3\u0026#39; services: app: image: nginx volumes: - ./data:/usr/share/nginx/html 上述配置将宿主机上的./data目录挂载到容器中的/usr/share/nginx/html目录下。\n绑定挂载还有一类特殊的用法，叫做只读挂载（read-only mount），只读挂载是将宿主机上的目录以只读的方式挂载到容器中，可以在容器中读取数据，但不能修改数据。只读挂载通常用于读取宿主机上一些特定的数据，例如系统时区文件、SSL证书等。 在docker-compose中使用只读挂载时，可以在docker-compose.yml文件中使用volumes字段来定义只读挂载，例如：\n1 2 3 4 5 6 version: \u0026#39;3\u0026#39; services: app: image: nginx volumes: - /etc/localtime:/etc/localtime:ro 上述配置将宿主机上的/etc/localtime文件以只读的方式挂载到容器中的/etc/localtime文件下。这样容器中的时区就和宿主机一致了。\ntmpfs挂载：tmpfs挂载是将宿主机上的内存挂载到容器中，可以在容器中使用内存作为数据存储。tmpfs挂载的数据存储在宿主机的内存中，容器停止或删除时，数据会丢失。 在docker-compose中使用tmpfs挂载时，可以在docker-compose.yml文件中使用volumes字段来定义tmpfs挂载，例如：\n1 2 3 4 5 6 version: \u0026#39;3\u0026#39; services: app: image: nginx volumes: - /tmp:/usr/share/nginx/html:tmpfs 注意，tmpfs挂载的语法和绑定挂载的语法类似，但在路径后面加上:tmpfs来表示使用tmpfs挂载。\n匿名卷（anonymous volume）：匿名卷是Docker中不常用的数据卷类型，匿名卷没有名称，可以在多个容器之间共享数据。匿名卷的数据存储在宿主机上的/var/lib/docker/volumes目录下。 在docker-compose中使用匿名卷时，可以在docker-compose.yml文件中使用volumes字段来定义匿名卷，例如：\n1 2 3 4 5 6 version: \u0026#39;3\u0026#39; services: app: image: nginx volumes: - /usr/share/nginx/html 上述配置将容器中的/usr/share/nginx/html目录挂载到宿主机上的匿名卷中。\ndocker容器中数据卷使用的最佳实践 # 在docker中使用数据卷可以很灵活，但也容易出现一些问题，例如数据卷的权限问题、数据卷的备份和恢复等。因此在使用数据卷时，我们最好遵循一些原则：\n理解数据卷用途 # 当创建一个数据卷时，我们应该明白这个数据卷的用途是什么。我们可以将数据卷分为以下几类：\n应用数据卷：存储应用程序的数据，例如数据库、缓存等。 配置数据卷：存储应用程序的配置文件，例如nginx、apache等。 日志数据卷：存储应用程序的日志文件，例如nginx、apache等。 临时数据卷：存储临时文件，例如上传的文件、缓存文件等。 共享数据卷：存储多个容器之间共享的数据，例如nginx和php-fpm之间共享的数据。 将数据卷分类 # 根据数据卷的用途，确定数据卷的挂载类型。一般来说，我们应该这样分类：\n应用数据卷：使用命名卷或绑定挂载。 配置数据卷：使用绑定挂载。 日志数据卷：使用绑定挂载。 临时数据卷：使用tmpfs挂载。 共享数据卷：使用命名卷或绑定挂载。 统一管理数据卷 # 这里的统一管理包含几个不同的方面：\n同一个应用程序的数据卷，需要使用绑定挂载的，应该统一挂载到同一个目录下，或者跟数据卷在docker中的路径保持一致。例如 1 2 3 4 5 6 7 8 9 version: \u0026#39;3\u0026#39; services: app: image: nextcloud volumes: - /media/user/docker_data/nextcloud/app/config:/var/www/html/config - /media/user/docker_data/nextcloud/app/custom_apps:/var/www/html/custom_apps - /media/user/docker_data/nextcloud/app/data:/var/www/html/data - /media/user/docker_data/nextcloud/app/themes:/var/www/html/themes 我们可以将所有的nextcloud的数据卷都挂载到/media/user/docker_data/nextcloud/app目录下，这样我们就可以很方便地管理这些数据卷了。 对于不同的应用程序，我们也应该尽量将它们的数据卷统一放到同一个目录下，例如/media/user/docker_data，这样我们就可以很方便地管理这些数据卷了。 对于某些重要的数据卷，可以将其与普通的数据卷分开，并存放到稳定性更高的存储介质上，例如RAID阵列等。例如上面的nextcloud数据卷，其中最重要的数据卷是/var/www/html/data，我们可以将其放到RAID阵列上，例如RAID阵列挂载在/media/user/raid，就可以这样定义数据卷： 1 2 3 4 5 6 version: \u0026#39;3\u0026#39; services: app: image: nextcloud volumes: - /media/user/raid/nextcloud/app/data:/var/www/html/data 这样我们就可以将数据存放在RAID阵列上了。 备份重要数据卷 # 对于重要的数据卷，我们应该定期备份数据卷中的数据，以防止数据丢失。我们可以使用docker cp命令将数据卷中的数据复制到宿主机上，或者使用docker volume export命令将数据卷导出为tar文件。在更新、迁移或者升级容器前，我们也应该备份数据卷中的数据，以防止数据丢失。\n1 docker cp \u0026lt;container_id\u0026gt;:/path/to/data /path/to/backup 或者\n1 docker volume export \u0026lt;volume_name\u0026gt; \u0026gt; backup.tar 定期清理无用数据卷 # 在使用docker时，我们可能会创建一些临时的数据卷，这些数据卷在使用完后就不再需要了。我们应该定期清理这些无用的数据卷，以释放存储空间。我们可以使用docker volume prune命令来删除所有未使用的数据卷。\n1 2 docker volume prune ``` ","date":"2025年4月23日","externalUrl":null,"permalink":"/p/%E5%AE%B9%E5%99%A83docker%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E6%8C%87%E5%8D%97%E6%95%B0%E6%8D%AE%E5%8D%B7volume%E7%9A%84%E7%AE%A1%E7%90%86/","section":"Posts","summary":" 缘起 # 这是介绍docker容器的第三篇文章。本系列文章链接如下：\n容器（1）：容器相关知识简介——容器化、docker、docker-compose、Kubernetes / K8s等 容器（2）：docker最佳实践指南——docker-compose和Portainer 容器（4）：docker最佳实践指南——容器更新、升级和迁移 容器（5）：docker最佳实践指南——容器更新监测工具WUD（What’s Up Docker） 容器（6）：使用Docker时的一些误区、坏习惯和问题 前两篇文章分别介绍了容器的基本概念和使用方法，以及使用容器的最佳实践方式。\n","title":"容器（3）：docker最佳实践指南——数据卷volume的管理","type":"post"},{"content":"","date":"2025年4月23日","externalUrl":null,"permalink":"/tags/%E6%95%B0%E6%8D%AE%E5%8D%B7/","section":"Tags","summary":"","title":"数据卷","type":"tags"},{"content":"","date":"10 四月 2025","externalUrl":null,"permalink":"/en/categories/computer/","section":"Categories","summary":"","title":"Computer","type":"categories"},{"content":"","date":"2025年4月10日","externalUrl":null,"permalink":"/tags/ddns/","section":"Tags","summary":"","title":"DDNS","type":"tags"},{"content":"","date":"10 四月 2025","externalUrl":null,"permalink":"/en/tags/dynamic-domain-name-system/","section":"Tags","summary":"","title":"Dynamic Domain Name System","type":"tags"},{"content":"","date":"2025年4月10日","externalUrl":null,"permalink":"/tags/%E5%8A%A8%E6%80%81%E5%9F%9F%E5%90%8D%E8%A7%A3%E6%9E%90/","section":"Tags","summary":"","title":"动态域名解析","type":"tags"},{"content":"之前就写过关于动态域名解析的文章，使用的是阿里云的API。最近将域名转移到了Cloudflare上，顺便记录一下如何使用Cloudflare的API进行动态域名解析。\n缘起 # 之前的文章里提到过，由于工作原因需要经常使用ssh登录家里的和办公室的个人电脑，但是这些电脑基本都没有固定的IP地址，所以当IP地址发生变动之后，往往不能及时连接到电脑上。就考虑使用域名代替IP地址来访问这些电脑，并且购买了一两个便宜的域名。很长一段时间都在用阿里云的域名解析服务。之前关于在阿里云上进行动态域名解析的文章可以参考“个人网站的建立过程（一）：购买个人域名并配置动态域名解析”。\n后来我把域名转移到了Cloudflare上，主要是因为Cloudflare的CDN服务和DNS解析速度都比阿里云快。现在记录一下如何使用Cloudflare的API进行动态域名解析。\n前提 # 拥有自己的域名\n购买的域名可以在Cloudflare上进行解析。 如果没有自己的域名，可以在Namecheap上购买一个便宜的域名。 拥有Cloudflare账号\n如果没有Cloudflare账号，可以在Cloudflare上注册一个账号。 注册完成后，登录Cloudflare账号，添加自己的域名，并将域名的DNS服务器指向Cloudflare提供的DNS服务器。 域名解析到服务器IP地址 # 购买的域名需要解析到服务器的IP地址才能直接通过域名访问服务器。 如果服务器具有固定IP，只需要在Cloudflare上将域名和服务器绑定即可。 如果服务器没有固定IP，则需要将域名动态解析到服务器的IP地址。 有固定IP——直接绑定 # 获取IP地址\n可以前往IPv6测试网站查看网络是否支持IPv6： 如果是类Unix系统，可以通过如下命令获取IPv4地址：\n1 curl ipinfo.io/ip 通过如下命令获取IPv6地址：\n1 curl ipv6.icanhazip.com 将域名与IP地址绑定\n登录Cloudflare账号，点击左上角的菜单，在菜单里找到DNS选项： 点击Add Record按钮，添加一条A记录（IPv4）或AAAA记录（IPv6），将域名解析到服务器的IP地址： 填入需要解析的域名和对应的IP地址（IP地址正确与否不重要，如果错误的话，待会儿运行本脚本后会把它改正；但域名一定要是你在之前“有固定IP——直接绑定”第2步中创建的域名）。 如果你有多个域名需要解析，可以仿照上面的例子自行添加域名，IPv4和IPv6都支持。再次强调，本文件中需要解析的域名需要是之前在“有固定IP——直接绑定”第2步中在Cloudflare上已经创建的域名。 测试是否绑定成功 绑定完成后可能需要10分钟的时间等待Cloudflare DNS服务器完成更新（一般提交之后立刻就会更新）。然后可以查看是否绑定成功。\n建议在另一个网络环境通过“ping”命令测试 1 ping surface.jinli.cyou 如果可以在公网ping通，则绑定成功。 完成！ 绑定成功之后就可以通过域名来访问服务器了，例如如果需要使用ssh远程登录服务器，就可以直接ssh域名：\n1 ssh lijin@surface.jinli.cyou -X 也可以用服务器搭建NAS（Network Attached Storage）服务等，然后通过域名访问。\n无固定IP——绑定之后再动态解析 # 如果电脑或服务器没有固定IP地址，则需要先按照上面的步骤在Cloudflare上绑定当前的IP地址。然后监控本机IP地址，一旦发生改变，就通过Cloudflare提供的API上传到Cloudflare DNS服务器，修改原来绑定的IP地址。\nDDNS脚本\n我使用的DDNS(Dynamic Domain Name System)脚本是一个让ChatGPT生成的bash脚本： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #!/bin/bash # Config API_TOKEN=\u0026#34;your_cloudflare_api_token\u0026#34; ZONE_ID=\u0026#34;your_cloudflare_zone_id\u0026#34; RECORDS=(\u0026#34;host1.example.com:A\u0026#34; \u0026#34;host1.example.com:AAAA\u0026#34; \u0026#34;host2.example.com:A\u0026#34; \u0026#34;host2.example.com:AAAA\u0026#34;) # Add as needed # Get current IPv4 and IPv6 addresses IPV4=$(curl -s https://ipv4.icanhazip.com) IPV6=$(ip -6 addr show scope global | grep inet6 | awk \u0026#39;{print $2}\u0026#39; | cut -d/ -f1 | head -n 1) if [ -z \u0026#34;$IPV4\u0026#34; ]; then echo \u0026#34;No IPv4 address found.\u0026#34; else echo \u0026#34;Detected IPv4: $IPV4\u0026#34; fi if [ -z \u0026#34;$IPV6\u0026#34; ]; then echo \u0026#34;No IPv6 address found.\u0026#34; else echo \u0026#34;Detected IPv6: $IPV6\u0026#34; fi # Function to update DNS record update_record() { local name=\u0026#34;$1\u0026#34; local type=\u0026#34;$2\u0026#34; local ip=\u0026#34;$3\u0026#34; echo \u0026#34;Processing $type record for $name\u0026#34; # Get record info RECORD=$(curl -s -X GET \u0026#34;https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=$type\u0026amp;name=$name\u0026#34; \\ -H \u0026#34;Authorization: Bearer $API_TOKEN\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34;) RECORD_ID=$(echo \u0026#34;$RECORD\u0026#34; | jq -r \u0026#39;.result[0].id\u0026#39;) CURRENT_IP=$(echo \u0026#34;$RECORD\u0026#34; | jq -r \u0026#39;.result[0].content\u0026#39;) if [ \u0026#34;$CURRENT_IP\u0026#34; == \u0026#34;$ip\u0026#34; ]; then echo \u0026#34; - $type is already up to date: $ip\u0026#34; return fi # Update the DNS record UPDATE=$(curl -s -X PUT \u0026#34;https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID\u0026#34; \\ -H \u0026#34;Authorization: Bearer $API_TOKEN\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ --data \u0026#34;{\\\u0026#34;type\\\u0026#34;:\\\u0026#34;$type\\\u0026#34;,\\\u0026#34;name\\\u0026#34;:\\\u0026#34;$name\\\u0026#34;,\\\u0026#34;content\\\u0026#34;:\\\u0026#34;$ip\\\u0026#34;,\\\u0026#34;ttl\\\u0026#34;:120,\\\u0026#34;proxied\\\u0026#34;:false}\u0026#34;) SUCCESS=$(echo \u0026#34;$UPDATE\u0026#34; | jq -r \u0026#39;.success\u0026#39;) if [ \u0026#34;$SUCCESS\u0026#34; == \u0026#34;true\u0026#34; ]; then echo \u0026#34; - Updated $type record to $ip\u0026#34; else echo \u0026#34; - Failed to update $type record: $UPDATE\u0026#34; fi } # Loop through all record names and update the specified type (A or AAAA) for record in \u0026#34;${RECORDS[@]}\u0026#34;; do # Split the record into name and type IFS=\u0026#39;:\u0026#39; read -r name type \u0026lt;\u0026lt;\u0026lt; \u0026#34;$record\u0026#34; if [ \u0026#34;$type\u0026#34; == \u0026#34;A\u0026#34; ] \u0026amp;\u0026amp; [ -n \u0026#34;$IPV4\u0026#34; ]; then update_record \u0026#34;$name\u0026#34; \u0026#34;A\u0026#34; \u0026#34;$IPV4\u0026#34; elif [ \u0026#34;$type\u0026#34; == \u0026#34;AAAA\u0026#34; ] \u0026amp;\u0026amp; [ -n \u0026#34;$IPV6\u0026#34; ]; then update_record \u0026#34;$name\u0026#34; \u0026#34;AAAA\u0026#34; \u0026#34;$IPV6\u0026#34; else echo \u0026#34; - Skipping $type record for $name due to missing IP address.\u0026#34; fi done 这个脚本要求用户指定想要解析的域名和解析类型（A或AAAA），格式为domain_name:record_type，例如host1.example.com:A。如果有多个域名需要解析，可以在脚本中添加多个记录，格式为host1.example.com:A、host2.example.com:AAAA等。 该脚本会自动检测当前的IPv4和IPv6地址，并将其更新到Cloudflare DNS服务器上。脚本中使用了jq命令来解析JSON格式的数据，因此需要安装jq命令： 1 sudo apt install jq 该脚本使用了Cloudflare的API进行动态域名解析，因此需要在Cloudflare上生成API Token和Zone ID。下面简单介绍一下如何在Cloudflare上获取API Token和Zone ID。将Zone_ID填入上面脚本中的ZONE_ID变量中。 获取API Token和Zone ID\n登录Cloudflare账号，点击你的域名，在右侧有一列菜单。菜单靠下的部分有Zone ID和Account ID。在这两个ID下面就是“Get your API token\u0026quot;的链接，点击它。 在User API Token的页面，选择“Create Token”。这里需要选择创建API Token的类型，这里选择“Edit Zone DNS”类型就行，即这个Token只能修改DNS记录。 然后会让你设置Token的权限，这里根据需要设置就行： 设置完成后，点击“Continue to summary”，然后点击“Create Token”按钮。创建完成后，会显示Token的值，页面也会提示你可以在目标电脑上运行一个curl命令来测试Token是否有效。测试有效之后把API Token复制下来填入上面脚本中的API_TOKEN变量中。 注意： 这个API Token和上面的Zone_ID都是非常重要的，千万不要公开它们。如果泄露了API Token，应该立即登录Cloudflare账号删除该Token。Zone_ID和API Token是你在Cloudflare上进行操作的凭证，泄露后可能会导致你的域名被恶意修改。\n测试运行DDNS脚本\n在修改完上面的脚本之后，保存为ddns.sh文件。然后\n确保安装了curl和jq命令：\n1 sudo apt install curl jq 赋予脚本可执行权限：\n1 chmod +x ddns.sh 运行脚本：\n1 ./ddns.sh 如果脚本运行成功，会输出类似下面的结果：\n1 2 3 4 5 6 7 8 9 10 Detected IPv4: 111.111.111.111 Detected IPv6: 12aa3:4567:89ab:cdef Processing A record for host1.example.com - Updated A record to 111.111.111.111 Processing AAAA record for host1.example.com - Updated AAAA record to 12aa3:4567:89ab:cdef Processing A record for host2.example.com - Updated A record to 111.111.111.111 Processing AAAA record for host2.example.com - Updated AAAA record to 12aa3:4567:89ab:cdef 设置定时任务\n如果电脑/服务器无固定IP，那么IP每隔一段时间就会改变，我们需要在IP改变之后尽快更新Cloudflare DNS服务器上的IP记录。 人工监控IP变化不太方便，我们可以每隔一段时间（例如每小时）让系统自动运行一次DDNS脚本，这样我们就可以做到在IP改变之后一小时内更新。 类Unix系统中的定时任务可以使用系统程序crontab实现，你可以在这里看到一些设置crontab的方法。 Linux 在Linux系统中，设置crontab定时任务非常简单。我们可以在命令行输入以下命令： shell crontab -e 此命令会打开一个包含crontab定时任务的配置文件，在文件末尾加入一行 shell 42 * * * * cd ~/.config/ddns \u0026amp;\u0026amp; ./ddns.sh 即可。上述命令表示每小时的第42分钟运行ddns.sh脚本。你可以根据需要修改时间。\n之后可以使用如下命令查看crontab定时任务是否在运行： 1 crontab -l macOS 在macOS上由于其系统安全性的考虑，在使用crontab命令前，还需要经过一些特殊的设置。 1. 为`cron`命令添加访问全盘文件的权限： - 打开“系统偏好设置”-\u0026gt;“安全性与隐私”-\u0026gt;“隐私”选项卡，点击左下角的锁图标解锁设置。 - 在左侧列表中选择“完全磁盘访问”，然后点击右侧的加号按钮，添加`/usr/sbin/cron`程序。注意，在点击加号按钮后会弹出一个Finder窗口，让你选择要添加的程序。但`cron`程序默认是隐藏的，在Finder窗口中无法直接找到。你可以在Finder中按下`Command + Shift + G`组合键，输入`/usr/sbin/`，然后选择`cron`程序添加即可。 - 关闭“系统偏好设置”窗口，确保权限已打开。 2. 这个脚本需要用到的`curl`、`jq`、`yq`和`ifconfig`这些命令，但是`cron`运行脚本的时候并不会加载用户的环境变量，所以需要在脚本中指定这些命令的完整路径。可以通过如下命令查看这些命令的完整路径： ```shell which curl jq yq ifconfig ``` 然后将这些命令的完整路径填入脚本中，例如： ```bash export PATH=\u0026quot;$PATH:/usr/local/bin:/usr/bin:/opt/local/bin:/sbin\u0026quot; ``` 3. 为此，我专门写了一个脚本作为调用`ddns.sh`的中介脚本，命名为`cron_ddns.sh`，内容如下： ```bash #!/bin/bash echo \u0026quot;[$(date '+%Y-%m-%d %H:%M:%S')] Starting cron job for Cloudflare DDNS update...\u0026quot; export PATH=\u0026quot;$PATH:/usr/local/bin:/usr/bin:/opt/local/bin:/sbin\u0026quot; echo \u0026quot;PATH is $PATH\u0026quot; cd /Users/lijin/.config/ddns echo \u0026quot;Current directory is $(pwd)\u0026quot; echo \u0026quot;Running script cloudflare.sh\u0026quot; /bin/bash ddns.sh ``` 4. 赋予脚本可执行权限： ```shell chmod +x cron_ddns.sh ``` 5. 然后在`crontab`中添加定时任务： ```shell crontab -e ``` 在文件末尾添加一行： ```shell 42 * * * * /Users/lijin/.config/ddns/cron_ddns.sh \u0026gt;\u0026gt; /tmp/ddns.log 2\u0026gt;\u0026amp;1 ``` ","date":"2025年4月10日","externalUrl":null,"permalink":"/p/%E5%8A%A8%E6%80%81%E5%9F%9F%E5%90%8D%E8%A7%A3%E6%9E%90--cloudflare/","section":"Posts","summary":"之前就写过关于动态域名解析的文章，使用的是阿里云的API。最近将域名转移到了Cloudflare上，顺便记录一下如何使用Cloudflare的API进行动态域名解析。\n","title":"动态域名解析--Cloudflare","type":"post"},{"content":"","date":"2025年4月10日","externalUrl":null,"permalink":"/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/","section":"Categories","summary":"","title":"计算机","type":"categories"},{"content":"","date":"8 四月 2025","externalUrl":null,"permalink":"/en/tags/remote-desktop/","section":"Tags","summary":"","title":"Remote Desktop","type":"tags"},{"content":"","date":"8 四月 2025","externalUrl":null,"permalink":"/en/series/remote-desktop-series/","section":"Series","summary":"","title":"Remote Desktop Series","type":"series"},{"content":"","date":"2025年4月8日","externalUrl":null,"permalink":"/tags/vnc/","section":"Tags","summary":"","title":"VNC","type":"tags"},{"content":"","date":"2025年4月8日","externalUrl":null,"permalink":"/tags/%E8%BF%9C%E7%A8%8B%E6%A1%8C%E9%9D%A2/","section":"Tags","summary":"","title":"远程桌面","type":"tags"},{"content":" 缘起 # 之前一直在用AnyDesk作为远程桌面软件，但最近学校的IT部门说AnyDesk有安全隐患，不能再使用了。在Linux上允许使用的远程桌面软件只有VNC和XRDP。VNC和RDP都是远程桌面协议，但VNC是一个开源的协议，而RDP是微软的专有协议。VNC的优点是跨平台，支持Linux、Windows、macOS等操作系统；虽然RDP是微软的专有协议，但Linux上也有开源的实现，比如XRDP。\nVNC的缺点是速度较慢，延迟较高。其优点是在Linux系统上使用VNC时，远程桌面和本地桌面是完全分开的，互不影响。而且很多Linux系统和macOS默认支持VNC服务，不需要额外安装软件。\nmacOS连接Ubuntu # Ubuntu自带VNC服务，macOS自带VNC客户端，因此在macOS上连接Ubuntu非常简单。在连接之前，我们需要做一些准备工作。\n在Ubuntu上打开VNC服务。虽然Ubuntu上自带VNC服务，但默认是关闭的，我们需要先打开它。如果使用Gnome桌面环境，进入“设置”界面，点击“共享”，然后打开“远程桌面”选项：\n设置VNC密码。在开启“远程桌面”选项后，就可以看到VNC服务的设置界面。按照提示进行设置：\n开启密码验证。勾选\u0026quot;Enable Legacy VNC Protocol\u0026quot;选项后，点击右边的三个点，勾选“Require a password”：\n如果不勾选这个选项，那么在每次连接时都会弹出一个确认窗口，要求确认是否允许连接。如果远程电脑没有人在旁边，就无法连接了：\n在macOS上连接Ubuntu。使用macOS自带的文件浏览器Finder，就可以通过VNC连接远程电脑。但在此之前，我们需要先用ssh连接远程电脑，并做端口转发：\n1 ssh -L 22590:localhost:5900 username@remote_ip 上述命令在连接远程电脑的同时，将远程电脑的5900端口映射到本地的22590端口。之所以这么做，是因为VNC服务默认使用5900端口，但由于安全性的原因，很多系统会将5900端口关闭，因此我们需要使用ssh进行端口转发。ssh连接成功后，就可以在Finder中连接远程电脑了：\n在弹出的窗口中输入VNC密码： 如果一切顺利，就可以看到远程电脑的桌面了。\n","date":"2025年4月8日","externalUrl":null,"permalink":"/p/%E8%BF%9C%E7%A8%8B%E6%A1%8C%E9%9D%A2%E4%BA%8Cvnc%E9%85%8D%E7%BD%AE/","section":"Posts","summary":" 缘起 # 之前一直在用AnyDesk作为远程桌面软件，但最近学校的IT部门说AnyDesk有安全隐患，不能再使用了。在Linux上允许使用的远程桌面软件只有VNC和XRDP。VNC和RDP都是远程桌面协议，但VNC是一个开源的协议，而RDP是微软的专有协议。VNC的优点是跨平台，支持Linux、Windows、macOS等操作系统；虽然RDP是微软的专有协议，但Linux上也有开源的实现，比如XRDP。\n","title":"远程桌面（二）：VNC配置","type":"post"},{"content":"","date":"2025年4月8日","externalUrl":null,"permalink":"/series/%E8%BF%9C%E7%A8%8B%E6%A1%8C%E9%9D%A2%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"远程桌面系列","type":"series"},{"content":"","date":"2024年11月26日","externalUrl":null,"permalink":"/tags/ffmpeg/","section":"Tags","summary":"","title":"Ffmpeg","type":"tags"},{"content":" 缘起 # FFmpeg是一个非常强大的音视频处理工具，可以用来剪辑、转码、合并、分离、提取视频音频等等。FFmpeg是一个自由开源软件，最初由法国程序员法布里斯·贝拉（Fabrice Bellard）发起，现在由米夏埃尔·尼德迈尔（Michael Niedermayer）维护。目前市面上的很多播放器、视频剪辑软件、转码软件，比如 Blender，Kodi， Plex， Shotcut， VLC media playe， YouTube等，都是基于FFmpeg的。当然，也有很多软件使用了FFmpeg的代码，但并未遵守FFmpeg的开源协议，被钉在了“FFmpeg耻辱柱”上。\nFFmpeg是一个命令行工具，使用起来稍有些复杂，但功能非常强大。我只用到过几个简单的功能，比如视频剪切、合并、调整分辨率、转码，视频加速、慢放，音视频分离。但我之前一直都只用CPU来处理，现在电脑上有了显卡，就想使用GPU来加速。这确实费了我一点儿功夫，这里就来总结记录一下。\n这是“Nvidia显卡”系列文章的第二篇，本系列文章主要记录如何使用Nvidia显卡，尤其是在Linux系统上使用Nvidia显卡。本系列的其他文章参见：\nNvidia显卡（一）：Ubuntu下的游戏、CUDA、深度学习、Docker等 Nvidia显卡（三）：Fedora下的游戏、CUDA、深度学习、Docker等 Nvidia显卡（四）：容器化配置Nvidia显卡的CUDA编程和深度学习环境 前提 # 电脑上有NVIDIA显卡 Linux系统 从源码编译安装FFmpeg # 我使用Ubuntu 22.04系统，之前我的FFmpeg是通过apt安装的，但是这个版本不支持GPU加速。所以我需要从源码编诹安装。如果你之前使用apt安装的FFmpeg，需要先卸载掉：\n```bash sudo apt-get remove ffmpeg ``` 编译安装过程我参考了英伟达官方的文档。但遗憾的是，这个文档似乎有些过时了，直接按照文档的步骤编译安装，会出现一些问题。下面是我成功编译安装的步骤。\n安装依赖 # 依赖主要有三方面：\n英伟达的显卡驱动。这个请参考我之前的文章Ubuntu 22.04安装英伟达显卡驱动。\n英伟达的编码接口库。使用下面的命令从源码编译安装。\n1 2 git clone https://github.com/FFmpeg/nv-codec-headers.git cd nv-codec-headers \u0026amp;\u0026amp; sudo make install \u0026amp;\u0026amp; cd – FFmpeg的依赖库。使用下面的命令安装。\n1 sudo apt-get install build-essential yasm cmake libtool libc6 libc6-dev unzip wget libnuma1 libnuma-devsudo apt-get install build-essential yasm cmake libtool libc6 libc6-dev unzip wget libnuma1 libnuma-dev 编译安装 # 下载FFmpeg源码。\n1 git clone https://git.ffmpeg.org/ffmpeg.git 配置编译参数。\n1 ./configure --enable-nonfree --enable-cuda-nvcc --enable-libnpp --extra-cflags=-I/usr/local/cuda/include --extra-ldflags=-L/usr/local/cuda/lib64 --disable-static --enable-shared --disable-x86asm 这里的/usr/local/cuda是英伟达显卡驱动的安装路径，如果你的安装路径不同，请修改。\n--enable-nonfree是为了支持非自由的编码器。\n--enable-cuda-nvcc是为了支持CUDA加速。\n--enable-libnpp是为了支持NPP加速。NPP是NVIDIA Performance Primitives的缩写，是英伟达提供的一套高性能图像和信号处理函数库，FFmpeg默认是不支持的。\n--disable-x86asm是为了避免编译时出现如下错误：\n1 nasm not found or too old. Please install/update nasm or use --disable-x86asm for a build without hand-optimized assembly. 如果上述命令执行没有问题，则可以继续编译。\n编译。\n1 make -j8 这里的-j8是指使用8个线程并行编译，可以根据自己的CPU核心数来调整。\n安装。\n1 sudo make install 上述命令会把FFmpeg安装到/usr/local/bin目录下。\n问题解决 # 在按照上面步骤编译安装后，当我在命令行运行ffmpeg时，出现了如下错误：\n1 ffmpeg: error while loading shared libraries: libavdevice.so.61: cannot open shared object file: No such file or directory 这是因为FFmpeg的库文件没有正确链接，需要手动链接：\n1 sudo ldconfig 如果上述命令不能解决问题，这是因为FFmpeg在编译安装时把链接库安装到了/usr/local/lib目录下，而系统默认的链接库路径是/usr/lib。这时需要把/usr/local/lib添加到链接库路径中：\n1 2 sudo echo \u0026#34;/usr/local/lib\u0026#34; \u0026gt; /etc/ld.so.conf.d/ffmpeg.conf sudo ldconfig FFmpeg的基本使用 # 这里简单列举几个我使用过的命令：\n视频剪切。\n1 ffmpeg -i input.mp4 -ss 00:00:00 -t 00:00:10 -c copy output.mp4 -i input.mp4：输入文件。 -ss 00:00:00：开始时间。 -t 00:00:10：持续时间。 -c copy：复制编码。 视频转码。\n比如把mkv格式转为mp4格式。\n1 ffmpeg -i input.mkv -codec copy output.mp4 -codec copy：复制编码。 视频加速或慢放。\n1 ffmpeg -i input.mp4 -vf \u0026#34;setpts=0.5*PTS\u0026#34; output.mp4 -vf \u0026quot;setpts=0.5*PTS\u0026quot;：加速倍数。小于1表示加速，大于1表示减速。这里的0.5表示加速2倍。 视频分辨率调整。\n1 ffmpeg -i input.mp4 -vf scale=1920:1080 output.mp4 -vf scale=1920:1080：目标分辨率。 音视频分离。\n1 2 ffmpeg -i input.mp4 -vn -acodec copy output.aac ffmpeg -i input.mp4 -an -vcodec copy output.mp4 -vn：不包含视频。 -acodec copy：复制音频编码。 -an：不包含音频。 -vcodec copy：复制视频编码。 视频合并\n如果要合并的视频分辨率、帧率、编码等参数一致，且视频只有两个，可以使用下面的命令：\n1 ffmpeg -i input1.mp4 -i input2.mp4 -filter_complex \u0026#34;[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[outv][outa]\u0026#34; -map \u0026#34;[outv]\u0026#34; -map \u0026#34;[outa]\u0026#34; output.mp4 -filter_complex \u0026quot;[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[outv][outa]\u0026quot;：合并视频和音频。 如果要合并的视频数量较多，则推荐把视频列表写入一个文本文件，然后使用concat协议来合并。\n1 ffmpeg -f concat -safe 0 -i list.txt -c copy output.mp4 -f concat：指定协议。\n-safe 0：允许读取任意文件。\n-i list.txt：视频列表文件。内容如下：\n1 2 3 file \u0026#39;input1.mp4\u0026#39; file \u0026#39;input2.mp4\u0026#39; file \u0026#39;input3.mp4\u0026#39; -c copy：复制编码。\n使用GPU加速 # 使用GPU加速需要在编译时添加--enable-cuda-nvcc和--enable-libnpp参数。上面的编译安装步骤中已经添加了这两个参数。\n使用GPU加速需要指定-hwaccel cuda参数。比如：\n1 ffmpeg -y -hwaccel cuda -hwaccel_output_format cuda -i input.mkv -c:v h264_nvenc -ss 0:00:00 -to 0:01:23 output.mp4 -hwaccel cuda：指定使用CUDA加速。 -hwaccel_output_format cuda：指定输出格式为CUDA。 -c:v h264_nvenc：指定使用NVIDIA的h264编码器。当然你也可以使用其他编码器，比如hevc_nvenc。若要查看支持的编码器，可以使用ffmpeg -h encoder=nvenc命令。 当然，上面介绍的那些命令也可以使用GPU加速，只需要添加-hwaccel cuda参数即可。\n我的CPU是i5-9600K，显卡是英伟达的RTX 4060 Ti。只使用CPU时，处理一个分辨率为1920x1080的视频，CPU占用率约100%，处理速度约为每秒30帧。使用GPU加速后，GPU占用率约为33%，处理速度约为每秒500帧。可以看到，GPU加速后处理速度提升了约16倍。\n清晰度问题 # 使用GPU加速后，视频的清晰度可能会有所下降。这是因为GPU加速时，FFmpeg会使用NPP库来处理图像，NPP库的处理精度可能不如CPU。\n如果你分别用CPU和GPU处理同一个视频，然后对比两个视频的大小，会发现GPU处理的视频大小要小很多。例如，我用CPU剪切的一个视频大小为300MB，用GPU剪切同一个视频，大小只有50MB。\n查看两个视频的码率，会发现GPU剪切的视频的码率远小于源视频的码率。这是因为GPU处理时，会对视频进行压缩，导致视频的清晰度下降。如果你想保持视频的清晰度，可以指定码率。\n1 ffmpeg -y -hwaccel cuda -hwaccel_output_format cuda -i input.mkv -c:v h264_nvenc -b:v 20M -ss 0:00:00 -to 0:01:23 output.mp4 -b:v 20M：指定码率为20M。这里的20M是指20Mbps，可以根据自己的需求调整。 FFmpeg在使用GPU加速时默认优先考虑速度，而不是清晰度。如果你想保持视频的清晰度，还可以使用-preset slow参数来指定编码速度。\n1 ffmpeg -y -hwaccel cuda -hwaccel_output_format cuda -i input.mkv -c:v h264_nvenc -preset slow -ss 0:00:00 -to 0:01:23 output.mp4 -preset slow：指定编码速度为slow。这里的slow是指慢速，处理的速度慢了，但清晰度会提高。 你也可以将上述两个参数结合起来使用。\n1 ffmpeg -y -hwaccel cuda -hwaccel_output_format cuda -i input.mkv -c:v h264_nvenc -b:v 20M -preset slow -ss 0:00:00 -to 0:01:23 output.mp4 ","date":"2024年11月26日","externalUrl":null,"permalink":"/p/nvidia%E6%98%BE%E5%8D%A1%E4%BA%8C%E8%A7%86%E9%A2%91%E5%89%AA%E8%BE%91%E8%BD%AC%E7%A0%81%E5%B7%A5%E5%85%B7ffmpeg%E4%BD%BF%E7%94%A8gpu%E5%8A%A0%E9%80%9F/","section":"Posts","summary":" 缘起 # FFmpeg是一个非常强大的音视频处理工具，可以用来剪辑、转码、合并、分离、提取视频音频等等。FFmpeg是一个自由开源软件，最初由法国程序员法布里斯·贝拉（Fabrice Bellard）发起，现在由米夏埃尔·尼德迈尔（Michael Niedermayer）维护。目前市面上的很多播放器、视频剪辑软件、转码软件，比如 Blender，Kodi， Plex， Shotcut， VLC media playe， YouTube等，都是基于FFmpeg的。当然，也有很多软件使用了FFmpeg的代码，但并未遵守FFmpeg的开源协议，被钉在了“FFmpeg耻辱柱”上。\n","title":"Nvidia显卡（二）：视频剪辑转码工具FFmpeg使用GPU加速","type":"post"},{"content":"","date":"23 九月 2024","externalUrl":null,"permalink":"/en/tags/attention-mechanism/","section":"Tags","summary":"","title":"Attention Mechanism","type":"tags"},{"content":"","date":"23 九月 2024","externalUrl":null,"permalink":"/en/tags/language-model/","section":"Tags","summary":"","title":"Language Model","type":"tags"},{"content":"","date":"23 九月 2024","externalUrl":null,"permalink":"/en/categories/machine-learning/","section":"Categories","summary":"","title":"Machine Learning","type":"categories"},{"content":"","date":"23 九月 2024","externalUrl":null,"permalink":"/en/tags/machine-learning/","section":"Tags","summary":"","title":"Machine Learning","type":"tags"},{"content":"","date":"23 九月 2024","externalUrl":null,"permalink":"/en/tags/neural-network/","section":"Tags","summary":"","title":"Neural Network","type":"tags"},{"content":"","date":"2024年9月23日","externalUrl":null,"permalink":"/tags/pytorch/","section":"Tags","summary":"","title":"PyTorch","type":"tags"},{"content":"","date":"2024年9月23日","externalUrl":null,"permalink":"/tags/transformer/","section":"Tags","summary":"","title":"Transformer","type":"tags"},{"content":"","date":"2024年9月23日","externalUrl":null,"permalink":"/categories/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/","section":"Categories","summary":"","title":"机器学习","type":"categories"},{"content":"","date":"2024年9月23日","externalUrl":null,"permalink":"/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/","section":"Tags","summary":"","title":"机器学习","type":"tags"},{"content":"","date":"2024年9月23日","externalUrl":null,"permalink":"/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/","section":"Tags","summary":"","title":"深度学习","type":"tags"},{"content":"","date":"2024年9月23日","externalUrl":null,"permalink":"/tags/%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/","section":"Tags","summary":"","title":"神经网络","type":"tags"},{"content":"","date":"2024年9月23日","externalUrl":null,"permalink":"/series/%E5%AE%9E%E4%BE%8B%E5%AD%A6pytorch/","section":"Series","summary":"","title":"实例学PyTorch","type":"series"},{"content":" 背景 # 这是“实例学PyTorch”系列的第7篇文章。在第6篇文章“实例学PyTorch（6）：语言模型（一）——使用LSTM实现词级语言模型”中，我们简单介绍了使用LSTM实现一个词级语言模型。\nLSTM以及其他基于循环神经网络（RNN）的模型在自然语言处理中有着广泛的应用，但是这些模型在处理长距离依赖问题时存在一些问题，例如梯度消失和梯度爆炸。为了解决这些问题，研究者提出了Transformer模型，Transformer模型使用了注意力机制，能够更好地处理长距离依赖问题。本文我们就简单介绍一下Transformer模型，并使用Transformer实现一个简单的词级语言模型。本文参考了PyTorch官方的示例代码中的word_language_model示例。\n本文的代码可以在我的GitHub仓库https://github.com/jin-li/pytorch-tutorial中的T06_word_lstm文件夹中找到。\nTransformer模型和注意力机制 # 2017年，Google的研究人员发布了一篇题为《Attention is All You Need》的论文，提出了Transformer模型。由于其在语言模型上的表现十分优秀，它很快取代了LSTM和GRU等循环神经网络模型，成为自然语言处理领域的主流模型。\nTransformer模型的结构如下图所示：\n可以看到，Transformer模型由编码器（Encoder）和解码器（Decoder）组成。编码器和解码器都是由多个相同的层堆叠而成，每个层包含一个多头自注意力机制（Multi-Head Self-Attention）和一个前馈神经网络（Feed-Forward Neural Network）。\n自注意力机制 # 所谓自注意力机制，就是模型可以同时关注输入序列中的不同位置，从而更好地捕捉输入序列中的信息。多头自注意力机制是指模型可以同时关注输入序列中的不同位置，并且可以通过多个头（head）来学习不同的注意力权重，从而更好地捕捉输入序列中的信息。\n举个例子，假设我们有一个输入序列[I, love, you]，我们希望模型能够根据I和you的关系来预测love。在LSTM模型中，模型会逐个处理输入序列中的每个词，但是在Transformer模型中，模型可以同时关注I和you，从而更好地捕捉它们之间的关系。由于Transformer模型可以同时关注输入序列中的不同位置，因此它可以更好地处理长距离依赖问题。但是同时，由于Transformer模型没有循环结构，因此它不能像LSTM那样处理序列中的顺序信息，这就需要在输入序列中加入位置编码（Positional Encoding）来表示词的位置信息。\n自注意力机制的计算过程如下：\n首先，我们需要计算查询（Query）、键（Key）和值（Value）的向量表示。这里我们使用输入序列的词向量作为查询、键和值的向量表示。这三个向量表示可以通过一个线性变换得到，即$Q = XW^Q$、$K = XW^K$和$V = XW^V$，其中$X$是输入序列的词向量，$W^Q$、$W^K$和$W^V$是线性变换的权重。\n然后，我们计算注意力分数（Attention Scores）$A$，注意力分数是查询向量$Q$和键向量$K$的点积，再除以$\\sqrt{d_k}$，其中$d_k$是查询向量$Q$的维度。即$A = \\frac{QK^T}{\\sqrt{d_k}}$。\n接着，我们计算注意力权重（Attention Weights）$W$，注意力权重是注意力分数$A$经过Softmax函数得到的。即$W = \\text{Softmax}(A)$。\n最后，我们计算自注意力输出（Self-Attention Output）$O$，自注意力输出是注意力权重$W$和值向量$V$的加权和。即$O = W \\cdot V$。\n在实际应用中，我们通常会使用多头自注意力机制，即将输入序列的词向量分别通过多个线性变换得到多组查询、键和值的向量表示，然后分别计算多组注意力分数、注意力权重和自注意力输出，最后将多组自注意力输出拼接起来，再通过一个线性变换得到最终的输出。\n前馈神经网络 # 前馈神经网络是Transformer模型中的另一个重要组件，它由两个全连接层和一个激活函数组成。前馈神经网络的计算过程如下：\n首先，我们将自注意力输出$O$通过一个全连接层得到中间表示$M$，即$M = O \\cdot W_1 + b_1$，其中$W_1$和$b_1$是全连接层的权重和偏置。\n然后，我们将中间表示$M$通过一个激活函数（通常是ReLU）得到前馈神经网络的输出$F$，即$F = \\text{ReLU}(M)$。\n最后，我们将前馈神经网络的输出$F$通过另一个全连接层得到最终的输出$O\u0026rsquo;$，即$O\u0026rsquo; = F \\cdot W_2 + b_2$，其中$W_2$和$b_2$是全连接层的权重和偏置。\n前馈神经网络的作用是对自注意力输出$O$进行非线性变换，从而更好地捕捉输入序列中的信息。\n编码器和解码器 # 自注意力加上前馈神经网络构成了Transformer模型中的一个层，整个Transformer模型是由多个这样的层堆叠而成的，如下图所示：\nTransformer模型一般可分为编码器（Encoder）和解码器（Decoder）两部分。编码器用于将输入序列编码成一个上下文向量，解码器用于根据上下文向量生成输出序列。编码器和解码器都是由多个相同的层堆叠而成，每个层包含一个多头自注意力机制和一个前馈神经网络。\n编码器的输入是一个词序列，输出是一个上下文向量，解码器的输入是一个上下文向量和一个词序列，输出是一个词序列。在机器翻译等任务中，我们可以将源语言的词序列作为编码器的输入，将目标语言的词序列作为解码器的输入，从而实现源语言到目标语言的翻译。\n编码器和解码器的区别在于，解码器在计算自注意力时还会计算编码器的输出的注意力，这是为了更好地捕捉输入序列和输出序列之间的关系。另外，解码器使用的自注意力机制是掩码自注意力机制（Masked Self-Attention），即在计算注意力权重时，解码器只能关注当前位置之前的位置，不能关注当前位置之后的位置。这当然是因为我们的目标就是预测当前和之后的词，自然不能使用之后词的信息，否则就是作弊了。\nTransformer模型的分类 # 虽然标准的Transformer模型包含编码器和解码器，但是在实际应用中，我们也可以只使用编码器或解码器，或者同时使用编码器和解码器。而且同时使用编码器和解码器的模型也并不一定就比只使用编码器或解码器的模型效果更好，这取决于具体的任务和数据集。\n编码器模型 # 编码器模型只包含编码器，用于将输入序列编码成一个上下文向量。编码器模型常用于文本分类、情感分析等任务。比较常用的编码器模型有BERT、RoBERTa等。\n解码器模型 # 解码器模型仅包含解码器，用于根据上下文向量生成输出序列。解码器模型常用于机器翻译、文本生成等任务。比较常用的解码器模型有T5、GPT等。\n编码器-解码器模型 # 编码器-解码器模型同时包含编码器和解码器，用于将输入序列编码成一个上下文向量，并根据上下文向量生成输出序列。编码器-解码器模型通常也用于机器翻译、文本生成等任务。比较常用的编码器-解码器模型有Transformer、BART等。\n使用PyTorch中的Transformer实现词级语言模型 # PyTorch里面提供了torch.nn.Transformer模块，可以方便地实现Transformer模型。下面我们就将上一篇文章中的LSTM模型替换为Transformer模型，实现一个简单的词级语言模型。\n准备数据 # 和上一篇文章中一样，这里我们依然使用WikiText-2数据集，数据集的下载和处理方法可以参考上一篇文章, 这里不再赘述。\n定义模型 # PyTorch已经内置了一个Transformer模块torch.nn.Transformer，我们可以直接使用这个模块来实现Transformer模型。但是在使用torch.nn.Transformer模块之前，我们需要先定义一个词嵌入层（Embedding Layer）和一个位置编码层（Positional Encoding Layer）。\n词嵌入层 # 词嵌入的知识在上一篇文章中已经介绍过了，这里我们直接可以使用PyTorch内置的torch.nn.Embedding模块来定义一个词嵌入层。\n1 input_embedding = nn.Embedding(vocab_size, embed_size) 其中vocab_size是数据集中字典的大小，embed_size是词嵌入的维度。\n位置编码层 # 所谓位置编码，就是为输入序列中的每个词添加一个位置信息，以便模型更好地捕捉输入序列中的信息。我们可以使用下面的公式来计算位置编码：\n$p_{(pos, 2i)} = \\sin(pos / 10000^{2i / d_{model}})$\n$p_{(pos, 2i+1)} = \\cos(pos / 10000^{2i / d_{model}})$\n其中$d_{model}$是词嵌入的维度，$pos$是词的位置，$i$是词嵌入的维度的索引。简单来说，上面的位置编码公式的效果就是为每个词的每个维度添加一个正弦或余弦函数的位置信息。使用正余弦函数进行位置编码的原因是，这样可以保证不同位置的词的位置编码之间的距离是相等的，从而更好地捕捉输入序列中的信息。\n我们可以使用代码来实现上面的位置编码，这里我们参考了PyTorch官方的示例代码中的PositionalEncoding类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class PositionalEncoding(nn.Module): r\u0026#34;\u0026#34;\u0026#34;Inject some information about the relative or absolute position of the tokens in the sequence. The positional encodings have the same dimension as the embeddings, so that the two can be summed. Here, we use sine and cosine functions of different frequencies. .. math: \\text{PosEncoder}(pos, 2i) = sin(pos/10000^(2i/d_model)) \\text{PosEncoder}(pos, 2i+1) = cos(pos/10000^(2i/d_model)) \\text{where pos is the word position and i is the embed idx) Args: d_model: the embed dim (required). dropout: the dropout value (default=0.1). max_len: the max. length of the incoming sequence (default=5000). Examples: \u0026gt;\u0026gt;\u0026gt; pos_encoder = PositionalEncoding(d_model) \u0026#34;\u0026#34;\u0026#34; def __init__(self, d_model, dropout=0.1, max_len=5000): super(PositionalEncoding, self).__init__() self.dropout = nn.Dropout(p=dropout) pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0).transpose(0, 1) self.register_buffer(\u0026#39;pe\u0026#39;, pe) def forward(self, x): r\u0026#34;\u0026#34;\u0026#34;Inputs of forward function Args: x: the sequence fed to the positional encoder model (required). Shape: x: [sequence length, batch size, embed dim] output: [sequence length, batch size, embed dim] Examples: \u0026gt;\u0026gt;\u0026gt; output = pos_encoder(x) \u0026#34;\u0026#34;\u0026#34; x = x + self.pe[:x.size(0), :] return self.dropout(x) 在上面的代码中，我们定义了一个PositionalEncoding类，这个类继承自nn.Module类，用于实现位置编码。在__init__方法中，我们首先定义了一个位置编码矩阵pe，然后计算了正余弦函数的位置编码，最后将位置编码矩阵pe添加到模型的缓冲区中。在forward方法中，我们将输入序列x和位置编码矩阵pe相加，然后通过Dropout层得到最终的输出。\nTransformer模型 # 有了词嵌入层和位置编码层之后，我们就可以使用PyTorch内置的torch.nn.Transformer模块来定义一个Transformer模型了。除了继承nn.Module类，并实现__init__和forward方法，我们还需要定义一个generate_square_subsequent_mask方法，用于生成一个掩码矩阵，这个掩码矩阵在计算注意力权重时会用到。另外，我们再定义一个init_weights方法，用于初始化模型的权重。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class TransformerModel(nn.Transformer): \u0026#34;\u0026#34;\u0026#34;Container module with an encoder, a recurrent or transformer module, and a decoder.\u0026#34;\u0026#34;\u0026#34; def __init__(self, ntoken, ninp, nhead, nhid, nlayers, dropout=0.5): super(TransformerModel, self).__init__(d_model=ninp, nhead=nhead, dim_feedforward=nhid, num_encoder_layers=nlayers) self.model_type = \u0026#39;Transformer\u0026#39; self.src_mask = None self.pos_encoder = PositionalEncoding(ninp, dropout) self.input_emb = nn.Embedding(ntoken, ninp) self.ninp = ninp self.decoder = nn.Linear(ninp, ntoken) self.init_weights() def _generate_square_subsequent_mask(self, sz): return torch.log(torch.tril(torch.ones(sz,sz))) def init_weights(self): initrange = 0.1 nn.init.uniform_(self.input_emb.weight, -initrange, initrange) nn.init.zeros_(self.decoder.bias) nn.init.uniform_(self.decoder.weight, -initrange, initrange) def forward(self, src, has_mask=True): if has_mask: device = src.device if self.src_mask is None or self.src_mask.size(0) != len(src): mask = self._generate_square_subsequent_mask(len(src)).to(device) self.src_mask = mask else: self.src_mask = None src = self.input_emb(src) * math.sqrt(self.ninp) src = self.pos_encoder(src) output = self.encoder(src, mask=self.src_mask) output = self.decoder(output) return F.log_softmax(output, dim=-1) 在上面的代码中，我们定义了一个TransformerModel类，这个类继承自nn.Transformer类，用于实现Transformer模型。在__init__方法中，我们首先调用super(TransformerModel, self).__init__方法初始化父类，然后定义了一个模型类型model_type，一个源掩码src_mask，一个位置编码层pos_encoder，一个词嵌入层input_emb，一个线性层decoder，最后调用init_weights方法初始化模型的权重。\n运行模型 # 有了上述模型之后，我们就可以定义一个训练函数和一个测试函数，然后封装一个加载数据、训练和测试的主函数，来运行我们的模型了。注意，相比上一篇文章中的LSTM模型，Transformer模型需要传入一个注意力头数量参数nhead。我们可以通过调用下面的命令来运行我们的模型：\n1 python language_transformer.py --plot 每次运行训练模型的代码时，一旦运行超过一个epoch，原来的模型文件model.pt会被覆盖，所以如果想要保存之前的模型，需要手动将model.pt文件重命名，或者在再次训练时通过--save参数指定保存的模型文件名（可以包含路径）。在一个epoch之后，模型会在验证集上计算损失值，如果验证集上的损失值比之前的最小损失值小，模型会保存在model.pt文件中。\n在我的个人电脑上，如果使用GPU（Nvidia GeForce RTX 4060 Ti）训练，每个epoch大约需要28秒，显存占用约548MB；如果使用CPU（Intel i5 9600K）训练，每个epoch大约需要516秒。这里我一共训练了50个epoch，训练集和验证集的损失值如下图所示：\n可以看到，这个结果和上一篇文章中的LSTM模型的结果差不多，Transformer模型的收敛速度比LSTM模型稍微快一点儿，但最终在验证集上的损失值稍大一点儿。\n生成文本 # 和上一篇文章中一样，我们在训练完模型后，代码会默认把模型保存在model.pt文件中，我们可以加载这个模型，然后使用这个模型生成一些文本。生成文本的方法和上一篇文章中的类似，代码在generate_text.py文件中。我们可以调用下面的命令来生成一些文本：\n1 python generate_text.py 一个诡异的情况是，在我的电脑上生成文本只能使用GPU。如果在运行上面的命令时指定了--no-cuda参数使用CPU来，我的电脑会直接崩溃重启，甚至连日志都没来得及输出（至少我没找到）。我能想到的可能原因是，我的电脑CUDA版本是12.5，而PyTorch的CUDA版本是12.1，崩溃重启的原因也可能和这个有关（但我也不确定，因为训练模型时可以不用CUDA，只用CPU，理论上只用CPU生成文本也不会用到CUDA，所以崩溃不应该和CUDA有关）。另外，我的电脑内存有64GB，比GPU显存大得多，所以内存不足也应该不是问题。如果有人遇到过类似的问题，欢迎留言告诉我，我们可以讨论一下，谢谢！\nTransformer模型生成文本的速度比LSTM模型略慢，在我的电脑上用GPU生成1000个词大约需要6秒。由于使用CPU生成文本会导致电脑崩溃重启，因此我没有测试CPU生成文本的速度。\n总结 # 本文中，我们简单介绍了Transformer模型和注意力机制，然后参照PyTorch官方的“word_language_model”示例，使用PyTorch中的torch.nn.Transformer模块实现了一个简单的词级语言模型。我们还介绍了Transformer模型的分类，包括编码器模型、解码器模型和编码器-解码器模型，模型的训练结果和LSTM差不多。最后，我们可以使用训练好的Transformer模型生成一些文本。\n","date":"2024年9月23日","externalUrl":null,"permalink":"/p/%E5%AE%9E%E4%BE%8B%E5%AD%A6pytorch7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E4%BA%8C%E4%BD%BF%E7%94%A8transformer%E5%AE%9E%E7%8E%B0%E8%AF%8D%E7%BA%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B/","section":"Posts","summary":" 背景 # 这是“实例学PyTorch”系列的第7篇文章。在第6篇文章“实例学PyTorch（6）：语言模型（一）——使用LSTM实现词级语言模型”中，我们简单介绍了使用LSTM实现一个词级语言模型。\n","title":"实例学PyTorch（7）：语言模型（二）——使用Transformer实现词级语言模型","type":"post"},{"content":"","date":"2024年9月23日","externalUrl":null,"permalink":"/tags/%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B/","section":"Tags","summary":"","title":"语言模型","type":"tags"},{"content":"","date":"2024年9月23日","externalUrl":null,"permalink":"/tags/%E6%B3%A8%E6%84%8F%E5%8A%9B%E6%9C%BA%E5%88%B6/","section":"Tags","summary":"","title":"注意力机制","type":"tags"},{"content":" 缘起 # 之前给我的Ubuntu电脑安装了Steam游戏平台和Proton兼容层，可以通过这个兼容层在Linux系统上运行Windows游戏。但我没怎么玩过游戏，所以一直没有仔细研究，只知道Proton兼容层还挺强大的，大部分游戏运行起来都没有问题，即使是游戏的“系统要求”中只显示支持Windows系统。\n后来装了一个Nvidia RTX 4060 Ti显卡，主要是想学习一下CUDA编程和深度学习。由于最近《黑神话：悟空》大热，室友想在我的电脑上玩，所以他就在我的电脑上登录了Steam账号，下载了《黑神话：悟空》，发现运行起来完全没问题。\n看室友玩了一段时间后，我也想试试，但又不想来回切换Steam账号。所以我就在Ubuntu上另建了一个系统账号，然后启动并登录Steam，但我发现之前室友在另一个账号上下载的游戏并没有显示在我的账号上。由于这些游戏都非常大，我不想再重新下载一遍，所以我就在想办法在我的账号上运行室友下载的游戏。在此过程中遇到了一个又一个问题，最后终于解决了，这里记录一下，以便遇到同样问题的人参考。\n前提 # 如果你只想一个人在Linux系统上玩Steam游戏，那么只需要安装Steam和Proton即可，那么你只需要参照我之前的文章《Ubuntu上Nvidia显卡设置：游戏、CUDA、深度学习、Docker等》中的步骤安装即可，如果你不想点进去看，我这里简单列一下步骤：\n安装Steam # 下载Steam安装包： 1 wget https://cdn.cloudflare.steamstatic.com/client/installer/steam.deb 安装Steam： 1 sudo dpkg -i steam.deb 安装可能缺失的依赖项： 1 sudo apt-get install -f 运行Steam： 1 steam 登录Steam账号 安装Proton # 在Steam中，选择可以在Linux上运行的游戏，然后点击Settings，在Steam Play选项卡中，勾选Enable Steam Play for supported titles和Enable Steam Play for all other titles，然后在Steam Play下拉菜单中选择一个Proton版本，点击OK。\n等待Proton安装完成后，就可以在Linux上运行Windows游戏了。\n安装游戏 # 如果你是第一次使用Steam，尚未购买任何游戏，你可以选择一些免费的游戏进行测试，例如《Dota 2》、《Counter-Strike: Global Offensive》等。\n多用户共享游戏 # 如果你想多个用户都能玩同一个游戏，而不想重复下载（毕竟一个大型游戏可能有几十GB甚至上百GB），那么可以接着看我是怎么操作的。\nSteam运行环境 # 在创建第二个用户并共享同一份游戏文件之前，我们需要先了解一下Linux下Steam的运行环境以及主要文件的位置。\n在按照上文的步骤安装Steam和Proton后，Steam安装到了系统目录下，因此所有用户都可以使用Steam客户端。\n在Ubuntu用户user1启动了Steam客户端后，Steam会在user1用户的家目录下创建一个.steam目录，该目录下包含了用户的Steam配置文件、游戏文件等。在.steam目录下，有一个steam目录，该目录下包含了用户的Steam客户端程序，还有一个steamapps目录，该目录下包含了用户下载的游戏文件。\n因此，如果我们想复用一个游戏，其本质就是让其他用户也能访问到/home/user1/.steam/steam/steamapps目录下的游戏文件。\n创建第二个用户 # 我们为第二个用户创建一个新的系统账号，将其添加到sudo组，并设置密码：\n1 2 3 sudo adduser user2 sudo usermod -aG sudo user2 sudo passwd user2 切换到user2用户：\n点击屏幕右上角的用户图标，选择Switch Account，然后选择user2用户登录。\n打开并登录Steam：\n打开Steam客户端后，Steam会在user2用户的家目录下创建一个.steam目录，但是此时user2用户的.steam/steam/steamapps目录下并没有游戏文件。因此，当你点击Library页面时，你会发现库里看不到之前user1用户下载的游戏。\n下面我们就主要来解决这个问题，让user2用户也能访问到之前user1用户下载的游戏文件，并正常运行游戏。\n共享游戏文件 # 之前user1用户下载的游戏文件位于/home/user1/.steam/steam/steamapps目录下，但我们想让多个用户共享这些游戏文件，所以我们最好将这些游戏文件移动到一个公共目录下，例如/opt/games/steam下。\n1 2 sudo mkdir -p /opt/games/steam sudo mv /home/user1/.steam/steam/steamapps /opt/games/steam 为了让user2用户能够访问到/opt/games/steam目录下的游戏文件，我们需要将/opt/games/steam目录的权限设置为755：\n1 sudo chmod 775 /opt/games/steam 然后我们创建一个新的用户组steam，将user1和user2用户都添加到这个用户组中：\n1 2 3 sudo groupadd steam sudo usermod -aG steam user1 sudo usermod -aG steam user2 接着，我们将/opt/games/steam目录的用户组设置为steam：\n1 sudo chown -R user1:steam /opt/games/steam 最后，我们需要为共享文件夹设置setgid权限，这样可以保证，任何在共享文件夹中新建的文件和目录，其用户组和之前共享文件夹的用户组相同。即/opt/games/steam的用户组是steam，之后在/opt/games/steam中创建的文件和文件夹的用户组都将是steam：\n1 sudo chmod g+s /opt/games/steam 为了让user1的Steam客户端能够访问到/opt/games/steam目录下的游戏文件，我们需要在user1用户的家目录下创建一个软链接：\n1 ln -s /opt/games/steam/steamapps /home/user1/.steam/steam/steamapps 然后我们在user1下，打开Steam客户端，点击Library页面，你会发现之前下载的游戏文件都在了。如果你没有看到，可以尝试重启Steam客户端或者重启电脑。\nuser2用户也类似，我们需要在user2用户的家目录下创建一个软链接：\n1 ln -s /opt/games/steam/steamapps /home/user2/.steam/steam/steamapps 然后我们在user2下，打开Steam客户端（可能需要先重启Steam客户端或者重启电脑），点击库页面，你应该可以看到之前下载的游戏文件了。\n然而，如果你点击开始游戏按钮，你会发现游戏并没有运行起来，而且很可能没有任何提示。如果你想通过错误日志来查找问题，你可以退出Steam客户端，然后在终端中运行Steam客户端，这样你就可以在终端中看到Steam的输出信息，大概是这样：\n1 pressure-vessel-wrap[44758]: E: openat(/opt/games/steam/steamapps/common/SteamLinuxRuntime_sniper/sniper_platform_0.20240806.97927/files/.ref): Permission denied 或者是这样：\n1 2 3 4 5 pressure-vessel-wrap[4109614]: W: For best results, \u0026#34;/opt/games/steam/steamapps/common/SteamLinuxRuntime_sniper/sniper_platform_0.20240820.99315/files\u0026#34; and \u0026#34;/opt/games/steam/steamapps/common/SteamLinuxRuntime_sniper/var/tmp-7P6EU2/usr\u0026#34; should both be on the same fully-featured Linux filesystem. Adding process 4109770 for gameID 1264970 wineserver: /opt/games/steam/steamapps/compatdata/1264970/pfx is not owned by you wine: using kernel write watches, use_kernel_writewatch 1. wine: \u0026#39;/opt/games/steam/steamapps/compatdata/1264970/pfx\u0026#39; is not owned by you 你也可以在库页面中右键点击游戏，然后选择属性，在已安装文件选项卡中点击检查完整性，你会发现不能通过完整性检查。\n上面这个问题很诡异，因为我们已经将/opt/games/steam目录的权限设置为755，并且将user1和user2用户都添加到steam用户组中，但上面的错误信息表明，user2用户下的Steam仍然无法访问/opt/games/steam/steamapps/common/SteamLinuxRuntime_sniper/sniper_platform_0.20240806.97927/files/.ref文件。更加诡异的是，这个文件的大小为0！我也不知道这个文件是干嘛的，但经过一番探索，我在网上找到了一些相关的讨论：\nhttps://ubuntuforums.org/showthread.php?t=2494677 https://github.com/ValveSoftware/Proton/issues/4820 https://github.com/ValveSoftware/steam-for-linux/issues/3942 这些讨论中主要提出了两种解决方案：\n在切换系统账号后，更改/opt/games/steam目录的所有权为当前用户：\n1 sudo chown -R user2:steam /opt/games/steam 这样user2用户就可以访问/opt/games/steam目录下的所有文件了。\n但显然，这个解决方案是一种治标不治本的方法，因为每次切换系统账号后，都需要更改/opt/games/steam目录的所有权，这样显然不太方便。\n上面的问题的本质是Proton兼容层使用的Wine的问题。由于安全原因，Wine不允许用户访问所有权为其他用户的prefix目录。因此，解决方案就是在启动Steam时，不同的用户使用不同的prefix目录。Wine的prefix目录是Proton兼容层的一个重要概念，它存储了游戏的配置文件、缓存文件等。默认情况下，Proton会将prefix目录存储在~/.steam/steam/steamapps/compatdata/目录下，但我们可以通过修改Proton的代码，根据当前用户创建不同的prefix目录。\n为了实现这个方案，我们需要修改Proton的代码，然后重新编译Proton。其实要修改的代码只有两行，可以参见Proton拉取请求#4861，GitHub上显示这个拉取请求创建于2021年5月，但至今还没有合并，我也不太清楚Proton的开发者为啥一直不合并这个简单但有用的拉取请求。既然官方没有加入这个功能，我们接下来就自己动手实现。\n修改并编译Proton # 首先，我们需要下载Proton的源代码：\n1 2 git clone --recurse-submodules https://github.com/ValveSoftware/Proton.git proton cd proton 然后，我们需要切换到一个稳定的分支，例如proton_9.0。我们还需要更新子模块：\n1 2 git checkout proton_9.0 git submodule update --init --recursive 接着，我们需要修改Proton的代码。为此我们新建一个分支，例如叫做myfeature-pr4861：\n1 git checkout -b myfeature-pr4861 打开Proton项目根目录下的proton文件（这是一个Python文件），找到CompatData类的初始化函数__init__。需要修改self.base_dir这一行，并添加一行。修改后的代码如下：\n1 2 3 4 class CompatData: def __init__(self, compatdata): self.base_dir = compatdata + \u0026#34;/\u0026#34; + str(os.getuid()) + \u0026#34;/\u0026#34; os.makedirs(self.base_dir, exist_ok=True) 我们只需要修改这两行代码就够了，参见Proton拉取请求#4861。\n这个思路其实很简单，proton的底层是使用Wine来运行游戏的，而Wine的prefix目录是存储在~/.steam/steam/steamapps/compatdata/目录下的。在做出上述修改以前，当我们以user1运行某个游戏时，Proton会将prefix目录存储在~/.steam/steam/steamapps/compatdata/目录下，其所有权为user1。当我们以user2运行同一个游戏时，Wine会尝试访问~/.steam/steam/steamapps/compatdata/目录下的prefix目录，但由于其所有权为user1，因此Wine会拒绝访问。\n上面的修改就是让Proton根据当前用户创建不同的prefix目录，例如user1的uid为1000，user2的uid为1001，那么user1运行游戏时，Proton会将prefix目录存储在~/.steam/steam/steamapps/compatdata/1000/目录下，其所有权为user1；user2运行游戏时，Proton会将prefix目录存储在~/.steam/steam/steamapps/compatdata/1001/目录下，其所有权为user2。这就解决了上面的所有权冲突问题。\n接下来我们编译Proton。Proton使用了容器化技术，因此我们需要有一个容器工具，例如Docker或Podman。我的电脑上已经安装了Docker，但Docker的运行一般需要sudo权限，这样会导致一系列的权限问题。因此这里推荐使用不需要sudo权限的Podman。\n首先，我们需要安装Podman：\n1 sudo apt install podman 然后，我们创建一个编译用的文件夹，可以放在跟Proton项目同级的目录下：\n1 2 3 cd .. mkdir build cd build 接着，使用Proton项目自带的配置工具配置编译环境：\n1 ../proton/configure.sh --enable-ccache --build-name=myfeature-9.0-pr4861 最后，我们开始编译Proton：\n1 make 编译完成后我们创建Proton的安装文件：\n1 make redist 这样我们在build目录下就会生成一个redist目录，里面包含了Proton的安装文件。\n安装Proton。对于用户自定义的兼容层，我们可以将其放在~/.steam/root/compatibilitytools.d/目录下。我们将redist目录整体复制到~/.steam/root/compatibilitytools.d/目录下：\n1 cp -r redist ~/.steam/root/compatibilitytools.d/myfeature-9.0-pr4861 注意，不管在哪个账号下使用Steam，我们都需要把redist目录复制到~/.steam/root/compatibilitytools.d/目录下，并修改目录的所有权为当前用户。例如如果是user_x用户，我们可以这样：\n1 sudo chown -R user_x:user_x ~/.steam/root/compatibilitytools.d/myfeature-9.0-pr4861 使用自定义Proton # 完成上面的步骤后，Steam就能检测到我们自定义的Proton版本了。\n不管在哪个账号下使用Steam，我们只需要在Steam中选择我们自定义的Proton版本即可。打开Steam后，点击左上角的Steam，然后选择设置，在侧边栏选项卡中选择兼容性，在运行其他产品中使用下拉菜单中选择我们自定义的Proton版本myfeature-9.0-pr4861，然后点击确定。\n最后，选择一个游戏并点击开始游戏，会弹出一个对话框，让你选择是否以兼容模式运行游戏，我发现其实不选兼容模式也没问题：\nSteam会花一会儿时间编译着色器，之后就能进入游戏界面了！\n一个小问题 # 在玩游戏时，我发现当游戏播放较长时间的过场动画时，由于没有操作，显示器在约30秒后就会显示无输入信号。我怀疑是显卡的电源管理机制导致的，运行xset -dpms命令可以关闭电源管理，这样可以确保显示信号不会中断。\n使用xset q命令可以查看当前的电源管理设置情况，直接使用xset命令可以查看xset命令的帮助信息。\n","date":"2024年9月19日","externalUrl":null,"permalink":"/p/linux%E7%B3%BB%E7%BB%9F%E4%B8%8A%E5%A4%9A%E7%94%A8%E6%88%B7%E5%85%B1%E4%BA%ABsteam%E6%B8%B8%E6%88%8F%E4%BB%A5ubuntu%E4%B8%BA%E4%BE%8B/","section":"Posts","summary":" 缘起 # 之前给我的Ubuntu电脑安装了Steam游戏平台和Proton兼容层，可以通过这个兼容层在Linux系统上运行Windows游戏。但我没怎么玩过游戏，所以一直没有仔细研究，只知道Proton兼容层还挺强大的，大部分游戏运行起来都没有问题，即使是游戏的“系统要求”中只显示支持Windows系统。\n","title":"Linux系统上多用户共享Steam游戏（以Ubuntu为例）","type":"post"},{"content":"","date":"2024年9月19日","externalUrl":null,"permalink":"/tags/proton/","section":"Tags","summary":"","title":"Proton","type":"tags"},{"content":"","date":"2024年9月19日","externalUrl":null,"permalink":"/tags/steam/","section":"Tags","summary":"","title":"Steam","type":"tags"},{"content":"","date":"9 九月 2024","externalUrl":null,"permalink":"/en/series/learn-pytorch-by-examples/","section":"Series","summary":"","title":"Learn PyTorch by Examples","type":"series"},{"content":"","date":"2024年9月9日","externalUrl":null,"permalink":"/tags/lstm/","section":"Tags","summary":"","title":"LSTM","type":"tags"},{"content":"","date":"9 九月 2024","externalUrl":null,"permalink":"/en/tags/recurrent-neural-network/","section":"Tags","summary":"","title":"Recurrent Neural Network","type":"tags"},{"content":" 背景 # 这是“实例学PyTorch”系列的第6篇文章。在第4篇文章和第5篇文章中，我们介绍了序列预测问题，并用RNN、GRU和LSTM实现了对正弦函数的预测。\n“实例学PyTorch（4）：序列预测（一）——循环神经网络（RNN）” “实例学PyTorch（5）：序列预测（二）——门控循环单元（GRU）和长短期记忆网络（LSTM）” 我们在第4篇文章中提到过，除了Sine函数这种时间序列，序列数据还可以是语言模型中的单词序列，本文就来简单介绍使用LSTM实现一个词级语言模型。\n本文的代码可以在我的GitHub仓库https://github.com/jin-li/pytorch-tutorial中的T06_word_lstm文件夹中找到。\n语言模型 # 语言模型是自然语言处理中的一个重要问题，它是用来评估一个句子的概率的模型。语言模型可以用来预测下一个单词是什么，也可以用来生成一个句子。语言模型在机器翻译、语音识别、文本生成等任务中都有广泛的应用。\n语言模型也是一个序列预测问题，即我们在预测下一个单词时，不仅需要考虑当前单词，还需要考虑前面的单词。例如，当前的单词是“苹果”，我们在预测下一个单词时，首先需要考虑这个“苹果”是指水果还是公司，这就需要用到前面的单词提供的信息。例如，前面的单词里有“吃”、“香蕉”、“梨”之类的，那么“苹果”很可能是指水果；如果前面的单词有“手机”、“电脑”、“乔布斯”之类的，那么“苹果”很可能是指公司。\n在前面的文章中，我们简单介绍过，解决序列预测的问题可以使用循环神经网络（RNN）及其变种，例如长短期记忆网络（LSTM）和门控循环单元（GRU）。在PyTorch官方的示例代码中有一个使用RNN等和Transformer实现语言模型的示例，这里我们将基于这个示例代码，使用LSTM实现一个简单的词级语言模型。另外，本文中的部分图片和代码参考和引用了YouTube博主Donato Capitella的视频《LLM Chronicles #4.4: Building a Word-Level Language Model in PyTorch using RNNs》和所附代码中的内容。\n当然，近年来，随着Transformer的出现，语言模型的效果得到了极大的提升，但是LSTM作为一种经典的循环神经网络，仍然有着广泛的应用。所以这里我们先介绍用LSTM来实现一个简单的语言模型。在后续的文章中，我们将介绍Transformer及其变种，以及如何使用Transformer实现语言模型。\n语言模型分为字符级、词级、次词级等，它们的区别在于对序列的划分不同。字符级即以单个字符为单位，词级以单词为单位，次词级以词素为单位。所谓次词级，是指将一个单词按照词素划分，例如“joyfulness”一词可以划分为“joy”、“ful”和“ness“三个词素。\n词级语言模型 # 词级语言模型是一种用来预测下一个单词是什么的模型。词级语言模型的输入是一个单词序列，输出也是一个单词序列。在训练阶段，我们将一个句子中的前面的单词作为输入，后面的单词作为输出，通过最小化预测单词和真实单词之间的差异来训练模型。在测试阶段，我们可以使用模型来预测下一个单词。\n用LSTM实现词级语言模型的流程和用LSTM或RNN实现正弦函数预测的流程类似：\n准备数据：我们需要将单词序列转换为整数序列，以便输入模型。 构建模型：我们需要构建一个LSTM模型，用于预测下一个单词。 训练模型：我们需要使用数据集训练模型，使模型能够预测下一个单词。 测试模型：我们需要使用模型来预测下一个单词。 准备数据 # 这里我们使用的数据库是WikiText-2，它是一个常用的语言模型数据集，包含了一些维基百科的词条，其内容可以在这里查看：WikiText-2。\n语言模型和正弦函数序列模型不同之处在于，正弦函数序列的输入是数字，而语言模型的输入是单词。PyTorch能处理的都是数学张量，因此我们需要将单词转换为数字。\n数据预处理 # WikiText-2数据集是一个文本文件，我们需要将文本文件转换为单词序列。PyTorch本来有一个torchtext库，可以用来处理文本数据，但在2024年4月之后，这个库就停止维护了。所以这里我们需要自己写一些代码来预处理训练了数据。\n预处理的基本思路是：\n读取全部文本，用一个字典来保存所有不重复的单词。字典的键是单词，值是单词对应的编号。 将原来文本中的单词全部替换为编号。 这样我们就将训练的文本数据转换为了一个整数序列。这里我们用的代码来自PyTorch官方的示例代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import os from io import open import torch class Dictionary(object): def __init__(self): self.word2idx = {} self.idx2word = [] def add_word(self, word): if word not in self.word2idx: self.idx2word.append(word) self.word2idx[word] = len(self.idx2word) - 1 return self.word2idx[word] def __len__(self): return len(self.idx2word) class Corpus(object): def __init__(self, path): self.dictionary = Dictionary() self.train = self.tokenize(os.path.join(path, \u0026#39;train.txt\u0026#39;)) self.valid = self.tokenize(os.path.join(path, \u0026#39;valid.txt\u0026#39;)) self.test = self.tokenize(os.path.join(path, \u0026#39;test.txt\u0026#39;)) def tokenize(self, path): \u0026#34;\u0026#34;\u0026#34;Tokenizes a text file.\u0026#34;\u0026#34;\u0026#34; assert os.path.exists(path) # Add words to the dictionary with open(path, \u0026#39;r\u0026#39;, encoding=\u0026#34;utf8\u0026#34;) as f: for line in f: words = line.split() + [\u0026#39;\u0026lt;eos\u0026gt;\u0026#39;] for word in words: self.dictionary.add_word(word) # Tokenize file content with open(path, \u0026#39;r\u0026#39;, encoding=\u0026#34;utf8\u0026#34;) as f: idss = [] for line in f: words = line.split() + [\u0026#39;\u0026lt;eos\u0026gt;\u0026#39;] ids = [] for word in words: ids.append(self.dictionary.word2idx[word]) idss.append(torch.tensor(ids).type(torch.int64)) ids = torch.cat(idss) return ids 这里我们定义了一个Dictionary类和一个Corpus类，Dictionary类用来保存单词和编号的对应关系，Corpus类用来读取文本文件，并将文本文件转换为整数序列。这里我们分别转换了训练集、验证集和测试集。\n经过处理后，我们得到了训练集、验证集和测试集的3个整数序列，其长度分别为2088628、217646和245569。字典的大小为33278。\n数据分批 # 经过上一步的处理，我们得到了一个非常长的整数序列，为了便于训练，我们将这个序列划分为若干个小的序列。例如对于一个长度为10000的序列，我们想分为20个批次，那么每个批次的长度就是500。这样我们就得到了一个形状为$500 \\times 20$的矩阵。\n注意：\n转换后的矩阵是列优先的，即第一列是第一个批次的数据，第二列是第二个批次的数据，以此类推。 若数据长度不能整除批次长度，我们将舍弃多余的数据。 各个批次之间是相互独立的，即第一个批次的最后一个数据和第二个批次的第一个数据之间没有关系。这也意味着分批之后的数据会丢失一些上下文信息。 分批化的代码很简单：\n1 2 3 4 5 6 7 8 def batchify(data, bsz): # Work out how cleanly we can divide the dataset into bsz parts. nbatch = data.size(0) // bsz # Trim off any extra elements that wouldn\u0026#39;t cleanly fit (remainders). data = data.narrow(0, 0, nbatch * bsz) # Evenly divide the data across the bsz batches. data = data.view(bsz, -1).t().contiguous() return data 词嵌入 # 我们在之前的文章中，我们使用过one-hot对标签进行编码。但是对于单词，one-hot编码就不太合适了，因为单词的数量太多（例如在Wikitext-2的字典中有33278个单词），one-hot编码会导致维度过高，计算量过大。因此我们需要使用另一种方法来表示单词，将单词映射到一个低维空间。这个步骤就叫做词嵌入（word embedding）。\n简单来说，词嵌入的想法是，使用若干个特征来表示一个单词，这些特征可以是单词的词性、情感、语义等信息。例如，我们选取“活物（living being）“、“猫科（feline）”、“人类（human）”、“性别（gender）”、“皇家（royalty）”、“动词（verb）”、“复数（plural）”7个特征，对于一个单词，我们分别用一个-1到1之间的数值来描述这些特征的程度，把表示所有特征的值组合起来得到一个7维向量，这个向量就是这个单词的词嵌入。例如对于单词“man”，我们可以向量$[0.6, -0.2, 0.8, 0.9, -0.1, -0.9, -0.7]$来表示单词“man”的词嵌入。\n类似地，我们可以将字典里的单词都表示为词嵌入。下图是几个例子：\n词嵌入是语言模型中一个非常重要的概念，因为它可以帮助我们的语言模型更好地表示单词的语义信息，也可以清楚地看到单词之间的关系。例如，我们将“king”和“man”相减，再加上“woman”，得到的结果应该和“queen”很接近。\n当然，实际应用中，我们不会仅使用7个特征，而是一般会使用几百个特征，这样可以全面地表示单词的语义信息。例如对于本问题，我们选择使用200个特征，那么我们需要将字典中的33278个单词映射到一个200维的空间。如果用矩阵来表示，这个词嵌入矩阵就是一个$33278 \\times 200$的矩阵。\n这个想法看上去挺合理的，但是如何得到这个词嵌入矩阵呢？我们也可以通过训练一个神经网络来得到这个词嵌入矩阵。这个神经网络的输入是一个单词的编号，输出是这个单词的词嵌入。这个神经网络的结构可以是一个全连接层，也可以是一个卷积层，也可以是一个LSTM层。这个神经网络的训练目标是最小化预测的词嵌入和真实的词嵌入之间的差异。这个神经网络的训练过程和语言模型的训练过程是类似的，只是输入和输出不同。\n当然，这里我们并不需要自己来训练这个词嵌入矩阵，因为研究者训练过很多这样的词嵌入矩阵，我们可以直接使用这些词嵌入矩阵。这些词嵌入矩阵可以是通用的，也可以是针对某个特定任务训练的。在PyTorch中，我们可以使用torch.nn.Embedding来加载这些词嵌入矩阵。\n1 torch.nn.Embedding(ntoken, emsize) 其中第一个参数是字典的大小，第二个参数是词嵌入的维度。这个函数会返回一个Embedding对象，我们将这个对象作为LSTM模型中的一层。\nLSTM模型 # 有了词嵌入之后，我们就可以构建LSTM模型了。这里我们以PyTorch中的torch.nn.LSTM为基础，构建一个简单的LSTM模型。这个模型的结构如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import torch.nn as nn import torch.nn.functional as F class LanguageLSTM(nn.Module): \u0026#34;\u0026#34;\u0026#34;Container module with an encoder, a recurrent module, and a decoder.\u0026#34;\u0026#34;\u0026#34; def __init__(self, rnn_type, ntoken, ninp, nhid, nlayers, dropout=0.5, tie_weights=False): super(LanguageLSTM, self).__init__() self.ntoken = ntoken self.drop = nn.Dropout(dropout) self.encoder = nn.Embedding(ntoken, ninp) self.rnn = getattr(nn, rnn_type)(ninp, nhid, nlayers, dropout=dropout) self.decoder = nn.Linear(nhid, ntoken) self.init_weights() self.rnn_type = rnn_type self.nhid = nhid self.nlayers = nlayers def init_weights(self): initrange = 0.1 nn.init.uniform_(self.encoder.weight, -initrange, initrange) nn.init.zeros_(self.decoder.bias) nn.init.uniform_(self.decoder.weight, -initrange, initrange) def forward(self, input, hidden): emb = self.drop(self.encoder(input)) output, hidden = self.rnn(emb, hidden) output = self.drop(output) decoded = self.decoder(output) decoded = decoded.view(-1, self.ntoken) return F.log_softmax(decoded, dim=1), hidden def init_hidden(self, bsz): weight = next(self.parameters()) return (weight.new_zeros(self.nlayers, bsz, self.nhid), weight.new_zeros(self.nlayers, bsz, self.nhid)) 这个模型的结构和之前的LSTM模型类似，只是这里我们使用了词嵌入层。这个模型的输入是一个整数序列，输出是一个概率分布，表示下一个单词是哪个单词的概率。这个模型的训练目标是最小化预测的概率分布和真实的概率分布之间的差异。\n训练和测试模型 # 有了数据和模型之后，我们就可以开始训练模型了。训练模型的代码和之前的LSTM模型类似，只是这里的损失函数criterion我们一般使用CrossEntropyLoss。\n除了在训练时在命令行输出损失值，我们在最后也把一个epoch内所有批次的损失值加权平均后返回，便于之后的绘图。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 def train(device, model, epoch, train_data, batch_size, criterion, lr, log_interval, seq_len): model.train() total_loss = 0. loss_all = [] data_cnt = [] start_time = time.time() hidden = model.init_hidden(batch_size) for batch, i in enumerate(range(0, train_data.size(0) - 1, seq_len)): data, targets = get_batch(train_data, i) data, targets = data.to(device), targets.to(device) model.zero_grad() hidden = repackage_hidden(hidden) output, hidden = model(data, hidden) loss = criterion(output, targets) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 0.25) for p in model.parameters(): p.data.add_(p.grad, alpha=-lr) total_loss += loss.item() loss_all.append(loss.item()) data_cnt.append(len(data)) if batch % log_interval == 0 and batch \u0026gt; 0: cur_loss = total_loss / log_interval elapsed = time.time() - start_time print(\u0026#39;| epoch {:3d} | {:5d}/{:5d} batches | lr {:02.2f} | ms/batch {:5.2f} | \u0026#39; \u0026#39;loss {:5.2f} | ppl {:8.2f}\u0026#39;.format( epoch, batch, len(train_data) // seq_len, lr, elapsed * 1000 / log_interval, cur_loss, math.exp(cur_loss))) total_loss = 0 start_time = time.time() return np.average(loss_all, weights=data_cnt) 测试模型的代码也和之前的LSTM模型类似，这里不再赘述。\n模型性能 # 全部的代码参见我的GitHub仓库https://github.com/jin-li/pytorch-tutorial中的T06_word_lstm文件夹中。在配置好环墶后，我们可以运行language_lstm.py来训练模型。\n1 python language_lstm.py 在我的个人电脑上，如果使用GPU（Nvidia GeForce RTX 4060 Ti）训练，每个epoch大约需要26秒，显存占用约540MB；如果使用CPU（Intel i5 9600K）训练，每个epoch大约需要506秒。这里我一共训练了50个epoch，训练集和验证集的损失值如下图所示：\n可以看到，在训练20个epoch之后，模型的损失值就基本稳定在4.1左右，验证集的损失值也基本稳定在4.7左右。这说明模型的泛化能力大致还行。而且我们的训练数据不算多，所以20个epoch的训练就基本够了。\n使用模型生成文本 # 模型训练完成时，训练好的模型会作为model.pt文件保存在当前目录下。我们可以使用这个模型来生成文本。生成文本的代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 def generate_text(device, checkpoint, data_source, words, temperature, log_interval): with open(checkpoint, \u0026#39;rb\u0026#39;) as f: model = torch.load(f, map_location=device) model.eval() corpus = data.Corpus(data_source) ntokens = len(corpus.dictionary) hidden = model.init_hidden(1) input = torch.randint(ntokens, (1, 1), dtype=torch.long).to(device) generated_text = [] with torch.no_grad(): # no tracking history for i in range(words): output, hidden = model(input, hidden) word_weights = output.squeeze().div(temperature).exp().cpu() word_idx = torch.multinomial(word_weights, 1)[0] input.fill_(word_idx) word = corpus.dictionary.idx2word[word_idx] generated_text.append(word) if i % log_interval == 0: print(\u0026#39;| Generated {}/{} words\u0026#39;.format(i, words)) return generated_text 这个函数里的checkpoint是模型文件的路径，data_source是数据集的路径，words是生成的单词数量，temperature是控制生成文本的多样性的参数，log_interval是每隔多少单词输出一次。调用这个函数生成文本并保存的完整代码参见generate_text.py。运行这个代码：\n1 python generate_text.py 我们可以得到生成的文本，这里我生成了1000个单词，其中的一部分如下：\n1 – \u0026lt;unk\u0026gt; , a year then with the software . It usually was sold for nearly half the day time . For this reason , the Nevermind run surpassed and a new group of canned \u0026lt;unk\u0026gt; . It had benefited from the unhealthy content , which have been leveled on the \u0026lt;unk\u0026gt; \u0026#39;s gates through the design the effects products associated with other birds and tested stewardship of those articles , ranging from an upright system with \u0026lt;unk\u0026gt; \u0026lt;unk\u0026gt; . 由于我们训练使用的WikiText-2数据集是维基百科的词条，其中包含了很多非ASCII字符，因此生成的文本中可能会有一些\u0026lt;unk\u0026gt;字符，这是因为这些字符不在我们的字典中。另外，这里的文本是随机生成的，因此可能不通顺。我们可以调整temperature参数来控制生成文本的多样性，temperature越大，生成的文本越多样化，temperature越小，生成的文本越保守。\n总结 # 本文介绍了使用LSTM实现一个简单的词级语言模型。语言模型是自然语言处理中的一个重要问题，它可以用来预测下一个单词是什么，也可以用来生成一个句子。语言模型是一个序列预测问题，这里我们使用LSTM来解决这个问题，当然也可以使用其他的RNN模型。\n使用RNN及其变种来实现语言模型固然可以得到不错的效果，但其在复杂任务上的表现仍然有很大的不足。近年来，随着Transformer的出现，语言模型的效果得到了极大的提升。在后续的文章中，我们将介绍Transformer及其变种，以及如何使用Transformer实现语言模型。\n","date":"2024年9月9日","externalUrl":null,"permalink":"/p/%E5%AE%9E%E4%BE%8B%E5%AD%A6pytorch6%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E4%B8%80%E4%BD%BF%E7%94%A8lstm%E5%AE%9E%E7%8E%B0%E8%AF%8D%E7%BA%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B/","section":"Posts","summary":" 背景 # 这是“实例学PyTorch”系列的第6篇文章。在第4篇文章和第5篇文章中，我们介绍了序列预测问题，并用RNN、GRU和LSTM实现了对正弦函数的预测。\n“实例学PyTorch（4）：序列预测（一）——循环神经网络（RNN）” “实例学PyTorch（5）：序列预测（二）——门控循环单元（GRU）和长短期记忆网络（LSTM）” 我们在第4篇文章中提到过，除了Sine函数这种时间序列，序列数据还可以是语言模型中的单词序列，本文就来简单介绍使用LSTM实现一个词级语言模型。\n","title":"实例学PyTorch（6）：语言模型（一）——使用LSTM实现词级语言模型","type":"post"},{"content":"","date":"2024年9月9日","externalUrl":null,"permalink":"/tags/%E5%BA%8F%E5%88%97%E9%A2%84%E6%B5%8B/","section":"Tags","summary":"","title":"序列预测","type":"tags"},{"content":"","date":"2024年8月13日","externalUrl":null,"permalink":"/tags/chatgpt/","section":"Tags","summary":"","title":"ChatGPT","type":"tags"},{"content":"","date":"2024年8月13日","externalUrl":null,"permalink":"/tags/lobechat/","section":"Tags","summary":"","title":"LobeChat","type":"tags"},{"content":" 缘起 # 在给自己的服务器安装了一个显卡后，我就想充分利用这块显卡，于是我就想到了部署一个大语言模型。这样就不用每月订阅付费的ChatGPT了。毕竟有时我用ChatGPT的频率并不高，每月20美元的订阅费用有点贵。如果可以自己部署一个私有的大语言模型，一来是可以使用各种大语言模型，不会局限于ChatGPT，二来即使开源的大语言模型效果不如付费版的ChatGTP，也可以使用OpenAI的API调用ChatGPT的接口，可以达到和使用ChatGPT付费版差不多的效果，但每月的费用应该用不到20美元。\n前提 # 已有一个服务器，或者本地电脑 已安装了Docker和docker-compose 若有显卡，已安装了相关驱动 前置知识 # 大语言模型（LLM）是一种基于深度学习的自然语言处理模型，它可以生成自然语言文本。大语言模型的训练需要大量的计算资源，因此通常需要在显卡上进行训练。目前，有很多开源的大语言模型，如GPT-2、GPT-3、T5等。这些模型都是基于Transformer架构的，它们在自然语言处理任务上取得了很好的效果。\n要想自己部署一个大语言模型，一般需要三个组件：\n训练好的模型：是一些公司或者研究机构训练好的大语言模型，一般以PyTorch或TensorFlow的模型文件的形式保存。模型文件通常很大，需要几GB到几十GB的存储空间。一般根据自己的显卡显存大小来选择，例如很多模型是7B的，表示模型的参数有7亿个，一般可以在8GB显存的显卡上运行。 运行模型的框架：是一个可以加载模型文件，并在显卡上运行模型的程序。这里我们使用ollama，它可以加载各种模型文件，并允许用户训练、微调、部署大模型。 用户交户的前端界面：是一个可以与用户交互的界面，用户可以输入文本，模型会生成回复。这里我们使用LobeChat，它是一个基于Web的用户交互界面，可以与ollama集成。 ollama # ollama是一个开源的大语言模型框架，它可以加载各种模型文件，并允许用户训练、微调、部署大模型。ollama支持PyTorch和TensorFlow的模型文件，可以在CPU和GPU上运行。ollama提供了一个RESTful API，用户可以通过HTTP请求调用模型。\nollama支持Windows、Linux和macOS系统，可以在本地电脑或者服务器上运行，也可以在Docker容器中运行。这里我们使用Docker容器运行ollama。\n安装 # 我们使用Docker容器来部署ollama，用我们在文章\u0026ldquo;容器（2）：docker最佳实践指南——docker-compose和Portainer\u0026rdquo;中介绍的方法，将ollama部署在Docker容器中。\n在~/docker目录下创建一个子目录ollama，用于存放ollama的相关文件。\n在~/docker/ollama目录下创建docker-compose.yml文件，用于定义ollama的配置。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 services: ollama: container_name: ollama image: ollama/ollama environment: - OLLAMA_ORIGINS=* - OLLAMA_HOST=0.0.0.0 - OLLAMA_MODELS=/root/.ollama/models ports: - \u0026#34;11434:11434\u0026#34; deploy: resources: reservations: devices: - driver: nvidia capabilities: [\u0026#34;gpu\u0026#34;] count: all volumes: - ollama:/root/.ollama restart: always volumes: ollama: 运行docker-compose up -d命令启动ollama容器。\n使用 # ollama中常用的命令有：\nollama train：用于训练模型。 ollama pull：用于下载预训练模型。 ollama serve：用于启动ollama服务。 我们可以通过docker exec -it ollama bash命令进入ollama容器，然后执行命令。\n首先我们需要下载一个预训练模型，例如我们可以下载一个llama3.1模型：\n1 ollama pull llama3.1 当然也有很多其他模型，你可以在ollama模型库中查看。\n然后我们测试是否可以使用这个模型：\n1 2 3 4 5 6 7 curl http://127.0.0.1:11434/api/generate -d \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;llama3.1\u0026#34;, \u0026#34;prompt\u0026#34;: \u0026#34;Why is the sky blue?\u0026#34;, \u0026#34;options\u0026#34;: { \u0026#34;num_ctx\u0026#34;: 4096 } }\u0026#39; 我们向通过ollama提供的RESTful API发送一个HTTP POST请求，问llama3.1模型为什么天空是蓝色的。如果我们部署的ollama运行良好的话，llama3.1会返回一个JSON格式的响应，包含生成的文本。\nLobeChat # LobeChat是一个基于Web的用户交互界面，可以与ollama集成。用户可以在LobeChat中输入文本，ollama会生成回复。LobeChat提供了一个简单的界面，用户可以在浏览器中使用。\nLobeChat还支持语音合成、图片识别、多模态、插件等功能，用户可以通过LobeChat与ollama进行多种交互。\n安装 # LobeChat是一个基于Node.js的应用程序，可以在各种操作系统上运行。我们可以使用Docker容器来部署LobeChat。\n在~/docker目录下创建一个子目录lobe-chat，用于存放LobeChat的相关文件。\n在~/docker/lobe-chat目录下创建docker-compose.yml文件，用于定义LobeChat的配置。\n1 2 3 4 5 6 7 8 9 services: lobe-chat: container_name: lobe-chat image: lobe-chat/lobe-chat environment: - OLLAMA_URL=http://127.0.0.1:11434 ports: - \u0026#34;3000:3000\u0026#34; restart: always 运行docker-compose up -d命令启动LobeChat容器。\n使用 # 在完成上述步骤后，我们可以在浏览器中访问http://localhost:3000，即可进入LobeChat的界面（第一次打开这个页面可能需要等待几十秒完成初始化）：\n然后，我们就可以利用LobeChat与ollama进行交互了，就像使用ChatGPT一样。需要注意的是，我们需要选择在ollama中已经下载的模型。例如上面我们下载了Llama3.1 8B模型，我们就可以在LobeChat中选择Llama3.1模型。\n配置其他模型 # LobeChat还支持其他很多模型，例如Open AI，Google的Gemini，通义千问等。我们可以点击左上角的头像，在菜单中选择“设置”，打开设置页面，然后选择“语言模型”选项卡，即可看到所有支持的模型。你可以选择开启或关闭某个模型，也可以填入模型需要的API Key来使用这些模型。\n远程访问 # 如果想要在远程也能使用LobeChat，你可以根据之前的文章“从公网访问个人网站——Nginx反向代理配置”中介绍的方法，用Nginx设置一个反向代理，和你的域名绑定，然后通过域名访问LobeChat。\n","date":"2024年8月13日","externalUrl":null,"permalink":"/p/%E6%9C%AC%E5%9C%B0%E8%BF%90%E8%A1%8C%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E4%B8%80%E4%BD%BF%E7%94%A8ollama%E5%92%8Clobechat%E5%9C%A8%E6%9C%AC%E5%9C%B0%E6%88%96%E8%80%85%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B8%8A%E9%83%A8%E7%BD%B2/","section":"Posts","summary":" 缘起 # 在给自己的服务器安装了一个显卡后，我就想充分利用这块显卡，于是我就想到了部署一个大语言模型。这样就不用每月订阅付费的ChatGPT了。毕竟有时我用ChatGPT的频率并不高，每月20美元的订阅费用有点贵。如果可以自己部署一个私有的大语言模型，一来是可以使用各种大语言模型，不会局限于ChatGPT，二来即使开源的大语言模型效果不如付费版的ChatGTP，也可以使用OpenAI的API调用ChatGPT的接口，可以达到和使用ChatGPT付费版差不多的效果，但每月的费用应该用不到20美元。\n","title":"本地运行大语言模型（一）：使用ollama和LobeChat在本地或者服务器上部署","type":"post"},{"content":" 缘起 # 最近买了一块Nvidia RTX 4060 Ti (8GB)显卡，为了物尽其用，我想在Ubuntu上使用这块显卡进行游戏、CUDA编程、深度学习等。但是在Ubuntu上使用Nvidia显卡并不是一件容易的事情，需要进行一些设置，下面记录一下我在Ubuntu上使用Nvidia显卡的相关设置。\n**更新：**我现在从Ubuntu迁移到了Fedora，Fedora里的Nvidia显卡驱动安装与Ubuntu里有所不同，具体可以参考本系列文章中的第三篇和第四篇。本系列的其他文章参见：\nNvidia显卡（二）：视频剪辑转码工具FFmpeg使用GPU加速 Nvidia显卡（三）：Fedora下的游戏、CUDA、深度学习、Docker等 Nvidia显卡（四）：容器化配置Nvidia显卡的CUDA编程和深度学习环境 Ubuntu安装Nvidia显卡驱动 # 查看显卡信息 # 首先，我们需要查看一下我们的显卡信息，打开终端，输入以下命令：\n1 lspci | grep VGA 如果你的电脑上安装了英伟达的显卡，你会看到类似以下的输出：\n1 01:00.0 VGA compatible controller: NVIDIA Corporation Device 2803 (rev a1) 不知道为啥，我的电脑上显示的是NVIDIA Corporation Device 2803，而不是RTX 4060 Ti，不过没关系，我们只需要知道这是一块英伟达的显卡就行了。\n安装Nvidia显卡驱动 # 首先删除可能已经安装的Nvidia显卡驱动： 1 2 3 sudo apt-get remove --purge \u0026#39;^nvidia-.*\u0026#39; sudo apt-get remove --purge \u0026#39;^libnvidia-.*\u0026#39; sudo apt-get remove --purge \u0026#39;^cuda-.*\u0026#39; 安装可能缺失的依赖项： 1 sudo apt-get install linux-headers-$(uname -r) 添加Nvidia显卡驱动PPA源并更新： 1 2 sudo add-apt-repository ppa:graphics-drivers sudo apt-get update 安装Nvidia显卡驱动： 1 sudo ubuntu-drivers autoinstall 重启电脑\n确认显卡驱动是否安装成功：\n1 nvidia-smi 如果你看到了类似以下的输出，那么恭喜你，你的Nvidia显卡驱动安装成功了：\n1 2 3 4 5 6 7 8 9 10 11 +-----------------------------------------------------------------------------+ | NVIDIA-SMI 470.57.02 Driver Version: 470.57.02 CUDA Version: 12.5 | |-------------------------------+----------------------+----------------------+ | GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |===============================+======================+======================| | 0 NVIDIA RTX 4060 Ti Off | 00000000:01:00.0 Off | N/A | | N/A 41C P8 10W / N/A | 0MiB / 7611MiB | 0% Default | | | | N/A | +-------------------------------+----------------------+----------------------+ 禁用Nouveau显卡驱动 # Nouveau是一个开源的Nvidia显卡驱动，但是它的性能不如Nvidia官方的闭源驱动，所以我们需要禁用Nouveau显卡驱动。\n查看Nouveau显卡驱动是否被加载： 1 lsmod | grep nouveau 如果你看到了类似以下的输出，那么说明Nouveau显卡驱动被加载了：\n1 2 3 4 5 6 7 nouveau 2457600 1 mxm_wmi 16384 1 nouveau ttm 106496 1 nouveau drm_kms_helper 217088 1 nouveau drm 552960 3 drm_kms_helper,nouveau,ttm wmi 36864 2 mxm_wmi,nouveau video 49152 1 nouveau 禁用Nouveau显卡驱动： 1 cat \u0026lt;\u0026lt;EOF | sudo tee /etc/modprobe.d/blacklist-nouveau.conf\\nblacklist nouveau\\noptions nouveau modeset=0\\nEOF 更新initramfs： 然后我们需要更新启动时加载的内核模块：\n1 sudo update-initramfs -u 重启电脑\n确认Nouveau显卡驱动是否被禁用：\n1 lsmod | grep nouveau 如果没有任何输出，那么说明Nouveau显卡驱动已经被禁用了。\n测试显卡 # 我们可以使用glmark2来测试显卡性能。\n安装glmark2： 1 sudo apt-get install glmark2 运行glmark2： 1 glmark2 如果在终端中看到了类似以下的输出，那么说明显卡性能测试成功：\n1 2 3 4 5 6 7 8 ======================================================= glmark2 2021.02 ======================================================= OpenGL Information GL_VENDOR: NVIDIA Corporation GL_RENDERER: NVIDIA GeForce RTX 4060 Ti/PCIe/SSE2 GL_VERSION: 4.6.0 NVIDIA 555.58.02 ======================================================= 并且会有一个窗口弹出来，显示正在测试的内容。测试完成后，终端中会显示测试的分数。\n游戏 # Linux系统本来是不太适合玩游戏的，但是随着Steam的推广，越来越多的游戏可以通过Proton在Linux上运行。Proton是Valve开发的一个基于Wine的工具，可以在Linux上运行Windows游戏。\n安装Steam # 下载Steam安装包： 1 wget https://cdn.cloudflare.steamstatic.com/client/installer/steam.deb 安装Steam： 1 sudo dpkg -i steam.deb 安装可能缺失的依赖项： 1 sudo apt-get install -f 运行Steam： 1 steam 登录Steam账号 安装Proton # 在Steam中，选择可以在Linux上运行的游戏，然后点击Settings，在Steam Play选项卡中，勾选Enable Steam Play for supported titles和Enable Steam Play for all other titles，然后在Steam Play下拉菜单中选择一个Proton版本，点击OK。\n等待Proton安装完成后，就可以在Linux上运行Windows游戏了。\n安装游戏 # 如果你是第一次使用Steam，尚未购买任何游戏，你可以选择一些免费的游戏进行测试，例如《Dota 2》、《Counter-Strike: Global Offensive》等。\nCUDA编程 # CUDA是英伟达公司推出的并行计算平台和编程模型，可以利用GPU的并行计算能力，加速计算密集型应用程序。CUDA编程需要安装Nvidia显卡驱动和CUDA工具包。而且CUDA版本和Nvidia显卡驱动版本有一定的对应关系，需要根据自己的显卡驱动版本选择合适的CUDA版本。\n安装CUDA # 查看Nvidia显卡驱动需要的CUDA版本： 1 nvidia-smi 在CUDA Version一行中，可以看到Nvidia显卡驱动需要的CUDA版本，例如CUDA Version: 12.5。即我们需要安装CUDA 12.5。\n下载CUDA安装包： 在Nvidia官网下载对应的CUDA安装包，选择合适的操作系统、架构、发行版、版本等。\n注意，我们需要安装的是CUDA 12.5，但进入官网之后默认显示的是12.6，不过这也没有关系。我们选择通过网络安装，会看到下面的安装指南：\n我们只需要把最后一行的命令改成我们需要的CUDA版本即可。\n安装CUDA： 1 2 3 4 wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb sudo dpkg -i cuda-keyring_1.1-1_all.deb sudo apt-get update sudo apt-get install cuda-toolkit-12-5 配置环境变量： 如果安装成功，你会在/usr/local/cuda-12.5目录下看到CUDA的安装文件。同时你会在/usr/local/cuda目录下看到一个指向/usr/local/cuda-12.5的软链接。如果之后升级CUDA版本，/usr/local/cuda会指向新的CUDA版本目录。\n我们需要配置环境变量，使得CUDA可以被找到。将cuda的bin目录和lib64目录添加到PATH和LD_LIBRARY_PATH环境变量时，我们使用/usr/local/cuda目录，而不是/usr/local/cuda-12.5目录，因为这样可以在以后升级CUDA版本时不需要修改环境变量。\n如果你使用bash，可以在~/.bashrc文件中添加以下内容：\n1 2 3 echo \u0026#39;export PATH=/usr/local/cuda/bin${PATH:+:${PATH}}\u0026#39; \u0026gt;\u0026gt; ~/.bashrc echo \u0026#39;export LD_LIBRARY_PATH=/usr/local/cuda/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}\u0026#39; \u0026gt;\u0026gt; ~/.bashrc source ~/.bashrc 如果使用其他shell，可以将以上内容添加到对应的配置文件中。\n测试CUDA： 1 nvcc --version 如果你看到了类似以下的输出，那么说明CUDA安装成功：\n1 2 3 4 5 nvcc: NVIDIA (R) Cuda compiler driver Copyright (c) 2005-2024 NVIDIA Corporation Built on Thu_Jun__6_02:18:23_PDT_2024 Cuda compilation tools, release 12.5, V12.5.82 Build cuda_12.5.r12.5/compiler.34385749_0 编译CUDA程序 # 可以使用Nvidia提供的CUDA示例程序来测试CUDA编程环境。\n下载CUDA示例程序： 1 git clone https://github.com/NVIDIA/cuda-samples.git 编译CUDA示例程序： 1 cd cuda-samples 本项目使用CMake进行编译，如果你的系统中没有安装CMake，可以使用以下命令安装：\n1 sudo apt-get install cmake 然后使用以下命令编译CUDA示例程序：\n1 2 3 mkdir build \u0026amp;\u0026amp; cd build cmake .. make 运行CUDA示例程序： 1 2 cd Samples/1_Utilities/deviceQuery ./deviceQuery 如果你看到了类似以下的输出，那么说明CUDA编程环境设置成功：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 ./deviceQuery Starting... CUDA Device Query (Runtime API) version (CUDART static linking) Detected 1 CUDA Capable device(s) Device 0: \u0026#34;NVIDIA GeForce RTX 4060 Ti\u0026#34; CUDA Driver Version / Runtime Version 12.5 / 12.5 CUDA Capability Major/Minor version number: 8.9 Total amount of global memory: 7810 MBytes (8188919808 bytes) (034) Multiprocessors, (128) CUDA Cores/MP: 4352 CUDA Cores GPU Max Clock rate: 2565 MHz (2.57 GHz) Memory Clock rate: 9001 Mhz Memory Bus Width: 128-bit L2 Cache Size: 33554432 bytes Maximum Texture Dimension Size (x,y,z) 1D=(131072), 2D=(131072, 65536), 3D=(16384, 16384, 16384) Maximum Layered 1D Texture Size, (num) layers 1D=(32768), 2048 layers Maximum Layered 2D Texture Size, (num) layers 2D=(32768, 32768), 2048 layers Total amount of constant memory: 65536 bytes Total amount of shared memory per block: 49152 bytes Total shared memory per multiprocessor: 102400 bytes Total number of registers available per block: 65536 Warp size: 32 Maximum number of threads per multiprocessor: 1536 Maximum number of threads per block: 1024 Max dimension size of a thread block (x,y,z): (1024, 1024, 64) Max dimension size of a grid size (x,y,z): (2147483647, 65535, 65535) Maximum memory pitch: 2147483647 bytes Texture alignment: 512 bytes Concurrent copy and kernel execution: Yes with 2 copy engine(s) Run time limit on kernels: Yes Integrated GPU sharing Host Memory: No Support host page-locked memory mapping: Yes Alignment requirement for Surfaces: Yes Device has ECC support: Disabled Device supports Unified Addressing (UVA): Yes Device supports Managed Memory: Yes Device supports Compute Preemption: Yes Supports Cooperative Kernel Launch: Yes Supports MultiDevice Co-op Kernel Launch: Yes Device PCI Domain ID / Bus ID / location ID: 0 / 1 / 0 Compute Mode: \u0026lt; Default (multiple host threads can use ::cudaSetDevice() with device simultaneously) \u0026gt; deviceQuery, CUDA Driver = CUDART, CUDA Driver Version = 12.5, CUDA Runtime Version = 12.5, NumDevs = 1 Result = PASS 这里显示的是我的电脑上的显卡是GeForce RTX 4060 Ti，CUDA版本是12.5，还有显存大小、CUDA核心数、GPU时钟频率等信息。如果你看到了类似的输出，那么说明CUDA编程环境设置成功了。\n然后你就可以开始编写自己的CUDA程序了。\n深度学习 # 深度学习是最常用的机器学习方法之一，可以用来解决图像识别、自然语言处理、推荐系统等问题。深度学习通常需要大量的数据和计算资源，因此使用GPU来加速深度学习训练是非常常见的。\n安装深度学习框架 # 目前比较流行的深度学习框架有TensorFlow、PyTorch、Keras等，这些框架都支持GPU加速。在安装深度学习框架之前，我们需要先安装CUDA和cuDNN。\n安装cuDNN： cuDNN是Nvidia提供的深度学习库，可以加速深度学习框架的运行。我们需要在Nvidia官网下载对应的cuDNN安装包，然后安装。\n安装深度学习框架： 在安装cuDNN之后，我们可以安装深度学习框架了。以PyTorch为例：\n1 pip install torch torchvision torchaudio 测试深度学习框架： 1 2 import torch print(torch.cuda.is_available()) 如果你看到了True，那么说明PyTorch安装成功，并且可以使用GPU加速。\n训练模型 # 现在你可以使用GPU来加速深度学习模型的训练了。例如我们可以下载PyTorch官方提供的例子来测试。\n1 2 3 4 git clone https://github.com/pytorch/examples.git cd examples/time_sequence_prediction python generate_sine_wave.py python train.py 这是一个利用长短期记忆网络（LSTM）来预测正弦波的例子，你可以根据自己的需求修改模型和数据。\nDocker容器 # 我之前用Docker运行了不少应用，但都是运行在CPU上的，现在有了Nvidia显卡，我想在Docker容器中使用GPU加速，比如将之前运行的大语言模型迁移到GPU上。\n安装Nvidia Container Toolkit # Nvidia Container Toolkit是Nvidia提供的一个工具，可以让Docker容器访问Nvidia显卡。\n添加Nvidia Container Toolkit PPA源： 1 2 3 4 5 curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \\ sed \u0026#39;s#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g\u0026#39; | \\ sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list sudo apt-get update 安装Nvidia Container Toolkit： 1 2 sudo apt-get install nvidia-container-toolkit sudo nvidia-ctk runtime configure --runtime=docker 重启Docker服务： 1 sudo systemctl restart docker 使用GPU加速Docker容器 # 现在我们可以在Docker容器中使用GPU加速了，例如：\n1 docker run --gpus all nvidia/cuda:12.5-base nvidia-smi 我一般使用docker-compose来管理Docker容器，可以在docker-compose.yml文件中添加GPU配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 version: \u0026#39;3\u0026#39; services: service-name: container_name: container-name image: image-source/image-name:tag environment: - SOME_ENV_VAR=some_value ports: - \u0026#34;host_port:container_port\u0026#34; deploy: resources: reservations: devices: - driver: nvidia capabilities: [\u0026#34;gpu\u0026#34;] count: all volumes: - /path/on/host:/path/in/container restart: always 这样就可以在Docker容器中使用GPU的计算能力了。\n问题解决 # 我在第一次安装完Nvidia驱动后直接重启电脑，结果电脑无法进入桌面，卡在了命令行界面，而且我无法输入任何命令。我怀疑是Nouveau显卡驱动没有被禁用，导致Nvidia显卡驱动无法正常工作。\n而且我似乎没有设置过恢复模式，所以没法进入恢复模式来修复问题。我又不想重装系统，因为原来的系统里有很多重要的东西。\n最后我只能通过U盘启动了一个Ubuntu系统，挂载原来的系统分区，通过chroot命令进入原来的系统，然后卸载Nvidia驱动，再重启电脑。\n但这一过程又出现了一些小插曲，因为我U盘中的Ubuntu系统是24.04，而我原来电脑里的系统是22.04，所以在chroot进入原来的系统后，apt-get update时似乎安装了一些错误的驱动，导致电脑重启后网络没了，无论是无线还是网线都无法连接。而且内核版本号也给我更新到了更新的版本，似乎也出现了一些兼容性的问题。\n所以我只能在U盘里重新搞了一个22.04的Ubuntu系统，然后再次chroot进入原来的系统，使用apt-get update更新，再补上Linux相关的额外模块dpkg -s linux-modules-extra-$(uname -r) | grep status，最后更新启动时的初始化内存文件系统update-initramfs -u、更新启动项update-grub。重启电脑后终于恢复正常了，除了系统的内核更新到了一个新的版本，其他的应该恢复到了原来的状态。\n最后重新安装Nvidia显卡驱动，这次不再重启电脑，而是先禁用Nouveau显卡驱动，再更新初始化内存文件系统和启动项，最后再重启电脑，终于成功安装了Nvidia显卡驱动。\n","date":"2024年8月12日","externalUrl":null,"permalink":"/p/nvidia%E6%98%BE%E5%8D%A1%E4%B8%80ubuntu%E4%B8%8B%E7%9A%84%E6%B8%B8%E6%88%8Fcuda%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0docker%E7%AD%89/","section":"Posts","summary":" 缘起 # 最近买了一块Nvidia RTX 4060 Ti (8GB)显卡，为了物尽其用，我想在Ubuntu上使用这块显卡进行游戏、CUDA编程、深度学习等。但是在Ubuntu上使用Nvidia显卡并不是一件容易的事情，需要进行一些设置，下面记录一下我在Ubuntu上使用Nvidia显卡的相关设置。\n","title":"Nvidia显卡（一）：Ubuntu下的游戏、CUDA、深度学习、Docker等","type":"post"},{"content":"","date":"2024年8月12日","externalUrl":null,"permalink":"/tags/ubuntu/","section":"Tags","summary":"","title":"Ubuntu","type":"tags"},{"content":" 缘起 # 这是docker系列的第二篇文章。本系列其他文章链接如下：\n容器（1）：容器相关知识简介——容器化、docker、docker-compose、Kubernetes / K8s等 容器（3）：docker最佳实践指南——数据卷volume的管理 容器（4）：docker最佳实践指南——容器更新、升级和迁移 容器（5）：docker最佳实践指南——容器更新监测工具WUD（What\u0026rsquo;s Up Docker） 容器（6）：使用Docker时的一些误区、坏习惯和问题 在\u0026ldquo;容器（1）：容器相关知识简介——容器化、docker、docker-compose、Kubernetes / K8s等\u0026rdquo;中，我们介绍了容器化的概念，以及如何使用Docker进行容器化。在实际使用Docker时，我们可能会遇到一些问题，例如：\n当我们运行多个容器之后，如何清晰地知道这些容器都在哪里、运行状态如何？ 当我们需要更新容器时，如何保证更新的容器和原来的容器一致？ 当我们需要运行多个容器时，如何保证这些容器之间的依赖关系？ 最开始我在使用docker时，有些容器是用docker run命令启动的，有些容器是用docker-compose启动的，docker-compose.yml文件放置的位置也不统一，导致我在管理这些容器时非常混乱。后来我重新整理了一下，将所有容器都用docker-compose启动，并将docker-compose.yml文件统一放置在一个目录下，这样管理起来就方便多了。\n当然，这里称之为“最佳实践指南”可能有些夸张，但这的确是现阶段我觉得最好的使用docker的方法。\n前提 # 已安装Docker和docker-compose 了解Docker的基本概念和基本用法 如果你还没有安装Docker和docker-compose，或者没有了解Docker和容器化的基本概念和基本用法，可以参考\u0026ldquo;容器（1）：容器相关知识简介——容器化、docker、docker-compose、Kubernetes / K8s等\u0026rdquo;，里面有较为详细的介绍。\ndocker最佳实践 # 使用docker-compose统一管理容器 # 创建一个目录，用于存放docker-compose.yml文件和相关文件，例如放在~/docker下。 每需要运行一个容器，都在这个目录下创建一个子目录，用于存放该容器的相关文件，例如~/docker/nginx。 在子目录中创建docker-compose.yml文件，用于定义该容器的配置，例如~/docker/nginx/docker-compose.yml。 我们可以定义一些创建docker-compose.yml文件的模板，这样每次创建新的容器时，只需要复制一下模板，修改一下配置即可。例如\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 version: \u0026#39;3\u0026#39; services: service-name: container_name: container-name image: image-source/image-name:tag environment: - SOME_ENV_VAR=some_value ports: - \u0026#34;host_port:container_port\u0026#34; deploy: resources: reservations: devices: - driver: nvidia capabilities: [\u0026#34;gpu\u0026#34;] count: all volumes: - /path/on/host:/path/in/container restart: always 这个模板定义了一个服务，服务名为service-name，容器名为container-name，使用image-source/image-name:tag镜像，设置了环境变量SOME_ENV_VAR，映射了端口host_port:container_port，设置了资源限制，挂载了卷，设置了容器重启策略。并且可以使用Nvidia的GPU。\n使用docker-compose启动容器 # 在子目录中的docker-compose.yml文件定义好之后，我们可以使用docker-compose命令启动容器。在子目录中执行：\n1 docker-compose up -d 如果是第一次启动容器，活着需要重新构建容器，可以使用：\n1 docker-compose up -d --build 如果需要停止容器，可以使用：\n1 docker-compose down 如果想要删除容器的数据卷，可以使用：\n1 docker-compose down -v 使用portainer管理容器 # Portainer是一个轻量级的容器管理工具，可以用来管理Docker容器、镜像、网络等。包括查看所有容器的运行状态、日志、资源使用情况等，也可以通过Web界面启动、停止、删除容器。\nPortainer有付费的商业版（Portainer BE），也有免费的社区版（Portainer CE），我们这里使用免费的社区版就可以。\n我们可以使用docker-compose启动Portainer，然后通过Web界面管理容器，具体步骤如下：\n在~/docker目录下创建一个子目录portainer，在portainer目录下创建docker-compose.yml文件，内容如下：\n1 2 3 4 5 6 7 8 9 10 11 version: \u0026#39;3\u0026#39; services: portainer: image: portainer/portainer-ce:latest ports: - \u0026#34;8001:8001\u0026#34; - \u0026#34;9443:9443\u0026#34; restart: always volumes: - /var/run/docker.sock:/var/run/docker.sock - ./portainer_data:/data 注意，在我使用Portainer时，官方的最新镜像portainer/portainer-ce:latest版本号是2.19.4，但2.19版本的Portainer不能完全支持Docker 26及以上版本。而我使用的Docker版本是27.0.3，所以我使用了2.20.1版本的Portainer，即portainer/portainer-ce:2.20.1。\n在portainer目录下执行：\n1 docker-compose up -d 打开浏览器，访问http://localhost:8001，输入用户名和密码（第一次登录需要设置用户名和密码），即可进入Portainer的管理界面。\n点击Local，即可查看所有的容器、镜像、网络等：\n如果想要通过Portainer远程管理某个机器上的Docker，你可以根据之前的文章“从公网访问个人网站——Nginx反向代理配置”中介绍的方法，用Nginx设置一个反向代理，和你的域名绑定，然后通过域名访问Portainer。\n","date":"2024年8月11日","externalUrl":null,"permalink":"/p/%E5%AE%B9%E5%99%A82docker%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E6%8C%87%E5%8D%97docker-compose%E5%92%8Cportainer/","section":"Posts","summary":" 缘起 # 这是docker系列的第二篇文章。本系列其他文章链接如下：\n容器（1）：容器相关知识简介——容器化、docker、docker-compose、Kubernetes / K8s等 容器（3）：docker最佳实践指南——数据卷volume的管理 容器（4）：docker最佳实践指南——容器更新、升级和迁移 容器（5）：docker最佳实践指南——容器更新监测工具WUD（What’s Up Docker） 容器（6）：使用Docker时的一些误区、坏习惯和问题 在“容器（1）：容器相关知识简介——容器化、docker、docker-compose、Kubernetes / K8s等”中，我们介绍了容器化的概念，以及如何使用Docker进行容器化。在实际使用Docker时，我们可能会遇到一些问题，例如：\n","title":"容器（2）：docker最佳实践指南——docker-compose和Portainer","type":"post"},{"content":"","date":"2024年8月4日","externalUrl":null,"permalink":"/tags/gru/","section":"Tags","summary":"","title":"GRU","type":"tags"},{"content":"","date":"4 八月 2024","externalUrl":null,"permalink":"/en/tags/sequence-prediction/","section":"Tags","summary":"","title":"Sequence Prediction","type":"tags"},{"content":"","date":"4 八月 2024","externalUrl":null,"permalink":"/en/tags/time-series/","section":"Tags","summary":"","title":"Time Series","type":"tags"},{"content":"","date":"2024年8月4日","externalUrl":null,"permalink":"/tags/%E6%97%B6%E9%97%B4%E5%BA%8F%E5%88%97/","section":"Tags","summary":"","title":"时间序列","type":"tags"},{"content":" 背景 # 这是“实例学PyTorch”系列的第5篇文章。在第4篇文章“实例学PyTorch（4）：序列预测（一）——循环神经网络（RNN）”中，我们介绍了序列预测问题，以及如何使用一个简单的循环神经网络（RNN）来实现对正弦函数的预测。在本文中，我们将更进一步，介绍序列预测中另外两种常用的神经网络：门控循环单元（Gated Recurrent Unit，GRU）和长短期记忆网络（Long Short-Term Memory，LSTM）。\n本文的代码可以在我的GitHub仓库https://github.com/jin-li/pytorch-tutorial中的T05_series_rnn文件夹中找到。\nRNN的问题与解决方法 # 在第4篇文章中，我们介绍了循环神经网络（RNN）的基本概念和工作原理。RNN是一种可以处理序列数据的神经网络，它在每个时间步都会保存之前的数据信息，从而可以处理序列数据。但是，RNN也有一些问题，例如梯度消失和梯度爆炸问题。梯度消失和梯度爆炸是深度学习中的一个常见问题，它们会导致模型无法收敛，或者收敛速度非常慢。\n梯度消失和梯度爆炸是深度学习中的一个常见问题。在反向传播算法中，梯度是通过链式法则计算的：\n$D_n = \\sigma^{\u0026rsquo;}(z_1) w_1 \\cdot \\sigma^{\u0026rsquo;}(z_2) w_2 \\cdot \\ldots \\cdot \\sigma^{\u0026rsquo;}(z_{n-1}) w_{n-1} \\cdot \\sigma^{\u0026rsquo;}(z_n) w_n$\n其中，$D_n$是第$n$层的梯度，$\\sigma^{\u0026rsquo;}(z_i)$是第$i$层的激活函数的导数，$w_i$是第$i$层的权重。可以看到，梯度是通过每一层的激活函数的导数和权重相乘得到的。如果激活函数的导数小于1，那么梯度会随着层数的增加指数级地减小，导致梯度消失；如果激活函数的导数大于1，那么梯度会随着层数的增加指数级地增大，导致梯度爆炸。\n长短期记忆网络（LSTM）和门控循环单元（GRU）是为了解决RNN中的梯度消失和梯度爆炸问题而提出的。它们通过引入门控机制来控制信息的流动，从而解决了RNN中的长期依赖问题。\nLSTM和GRU简介 # LSTM和GRU都引入了门控机制来控制信息的流动。所谓门控机制，就是把数据乘以一个0到1之间的系数，从而控制是否传递数据以及传递多大比例的数据，这个系数是由一个sigmoid激活函数计算得来的。\nLSTM和GRU的区别在于LSTM有三个门：遗忘门（Forget Gate）、输入门（Input Gate）和输出门（Output Gate），而GRU只有两个门：重置门（Reset Gate）和更新门（Update Gate）。GRU相对于LSTM来说，参数更少，计算量更小，但LSTM的表现一般比GRU更好。\n长短期记忆网络（LSTM） # 长短期记忆网络（Long Short-Term Memory，LSTM）是一种门控循环神经网络，由Hochreiter和Schmidhuber于1997年提出。LSTM引入了三个门：遗忘门（Forget Gate）、输入门（Input Gate）和输出门（Output Gate），通过这三个门来控制信息的流动，从而解决了RNN中的长期依赖问题。\nLSTM的结构如下图所示：\n遗忘门、输入门和输出门的具体计算公式的推导这里不再给出，感兴趣的读者可以参考这篇文章Understanding LSTM Networks。我们这里只简单介绍一下LSTM的工作原理：\n我们假设LSTM中的记忆数据是$C_t$，隐藏状态是$h_t$，输入数据是$x_t$，遗忘门是$f_t$，输入门是$i_t$，输出门是$o_t$。LSTM的工作原理如下：\n遗忘门：遗忘门的输入是当前时间步的输入数据$x_t$和上一个时间步的隐藏状态$h_{t-1}$，这两个输入数据经过sigmoid函数后的输出是一个0到1之间的系数$f_t$。$f_t$决定了上一个时间步的数据需要保留多少，如果$f_t$接近0，那么上一个时间步的数据就会被遗忘；如果$f_t$接近1，那么上一个时间步的数据就会被保留。\n输入门：输入门的输入是当前时间步的输入数据$x_t$和上一个时间步的隐藏状态$h_{t-1}$，这两个输入数据经过sigmoid函数后的输出是一个0到1之间的系数$i_t$。$i_t$决定了当前时间步的输入数据需要保留多少，如果$i_t$接近0，那么当前时间步的输入数据就会被忽略；如果$i_t$接近1，那么当前时间步的输入数据就会被保留。\n更新记忆：更新记忆的公式是$C_t = f_t \\cdot C_{t-1} + i_t \\cdot \\tilde{C}t$，其中$\\tilde{C}t$是当前时间步的输入数据$x_t$和上一个时间步的隐藏状态$h{t-1}$经过tanh函数后的输出。$C_t$ 是当前时间步的记忆数据，$f_t \\cdot C{t-1}$是上一个时间步的记忆数据，$i_t\\cdot\\tilde{C}_t$是当前时间步的输入数据。\n输出门：输出门的输入是当前时间步的输入数据$x_t$和上一个时间步的隐藏状态$h_{t-1}$，这两个输入数据经过sigmoid函数后的输出是一个0到1之间的系数$o_t$。$o_t$决定了当前时间步的输出数据$h_t$需要保留多少，如果$o_t$接近0，那么当前时间步的输出数据就会被忽略；如果$o_t$接近1，那么当前时间步的输出数据就会被保留。\n门控循环单元（GRU） # 门控循环单元（Gated Recurrent Unit，GRU）在LSTM的基础上做了一些简化，由Cho等人于2014年提出。GRU只有两个门：重置门（Reset Gate）和更新门（Update Gate）。相比于LSTM，GRU的参数更少，计算量更小。虽然LSTM的表现一般更好，但GRU由于其简单性也颇受欢迎。\nGRU中的重置门和更新门实际上是LSTM中三个门的简化版，其具体计算公式的推导这里不再给出，感兴趣的读者可以参考这篇文章Understanding LSTM Networks。我们这里只简单介绍一下GRU的工作原理：\n我们假设GRU中的记忆数据是$h_t$，输入数据是$x_t$，重置门是$r_t$，更新门是$z_t$。GRU的工作原理如下：\n重置门：重置门的输入是当前时间步的输入数据$x_t$和上一个时间步的隐藏状态$h_{t-1}$，这两个输入数据经过sigmoid函数后的输出是一个0到1之间的系数$r_t$。$r_t$决定了上一个时间步的数据需要保留多少，如果$r_t$接近0，那么上一个时间步的数据就会被忽略；如果$r_t$接近1，那么上一个时间步的数据就会被保留。\n更新记忆：更新记忆的公式是$\\tilde{h}t = \\tanh(W [r_t h{t-1}, x_t]) = \\tanh(W_{xh} x_t + r_t \\odot W_{hh} h_{t-1})$，其中$\\odot$是元素乘法。$\\tilde{h}t$是当前时间步的记忆数据，$W{xh} x_t$是当前时间步的输入数据，$r_t \\odot W_{hh} h_{t-1}$是上一个时间步的隐藏状态。\n更新门：更新门的输入是当前时间步的输入数据$x_t$和上一个时间步的隐藏状态$h_{t-1}$，这两个输入数据经过sigmoid函数后的输出是一个0到1之间的系数$z_t$。$z_t$决定了当前时间步的记忆数据需要保留多少，如果$z_t$接近0，那么当前时间步的记忆数据就会被忽略；如果$z_t$接近1，那么当前时间步的记忆数据就会被保留。\n使用LSTM和GRU实现序列预测的代码 # 数据准备 # 我们继续使用上一篇文章中生成的正弦序列数据，具体参见“实例学PyTorch（4）：使用循环神经网络实现序列预测（一）”。\n定义模型 # LSTM模型 # PyTorch中已经实现了LSTM模型，我们这里封装一下以用于本问题。代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import torch import torch.nn as nn class SimpleLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=10, output_size=1, num_layers=1): super(SimpleLSTM, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers self.lstm = nn.LSTM(input_size=input_size, hidden_size=hidden_size, dropout=(0 if num_layers == 1 else 0.05), num_layers=num_layers, batch_first=True) self.fc = nn.Linear(hidden_size, output_size) # Linear layer is output of model def forward(self, x): out, _ = self.lstm(x) out = self.fc(out[:, -1, :]) # Use the last output of the LSTM return out GRU模型 # PyTorch中也已经实现了GRU模型，我们这里封装一下以用于本问题。代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 import torch import torch.nn as nn class SimpleGRU(nn.Module): def __init__(self, input_size=1, hidden_size=10, output_size=1, num_layers=1): super(SimpleGRU, self).__init__() self.gru = nn.GRU(input_size=input_size, hidden_size=hidden_size, dropout=(0 if num_layers == 1 else 0.05), num_layers=num_layers, batch_first=True) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): out, _ = self.gru(x) out = self.fc(out[:, -1, :]) # Use the last output of the GRU return out 训练模型 # 在上一篇文章中使用的代码的基础上，我们只需要做一些简单的修改即可。\n首先我们把上面定义的LSTM和GRU模型放入主文件中，然后修改模型调用的部分。这里我们给main()函数添加一个命令行参数model_type，用于指定使用RNN、LSTM还是GRU模型：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 if model_type == \u0026#39;RNN\u0026#39;: model = SimpleRNN(hidden_size=hidden_size).float().to(device) elif model_type == \u0026#39;LSTM\u0026#39;: model = SimpleLSTM(hidden_size=hidden_size).float().to(device) elif model_type == \u0026#39;GRU\u0026#39;: model = SimpleGRU(hidden_size=hidden_size).float().to(device) else: exit(\u0026#39;Invalid model type. Please choose one of the following: RNN, LSTM, GRU\u0026#39;) loss_function = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=lr) if os.path.exists(\u0026#39;train_data.pt\u0026#39;) and os.path.exists(\u0026#39;test_data.pt\u0026#39;): print(\u0026#39;Loading train data \\\u0026#34;train_data.pt\\\u0026#34; and test data \\\u0026#34;test_data.pt\\\u0026#34;\u0026#39;) train_data = torch.load(\u0026#39;train_data.pt\u0026#39;, weights_only=False) test_data = torch.load(\u0026#39;test_data.pt\u0026#39;, weights_only=False) else: exit(\u0026#39;No train data or test data found. Please generate data first by running the script with the --generate-data flag.\u0026#39;) dataloader = DataLoader(train_data, batch_size=batch_size, shuffle=False) test_dataloader = DataLoader(test_data, batch_size=test_batch_size, shuffle=True) train_loss = [] test_loss = [] for epoch in range(1, epochs+1): train_loss_tmp = train(model, device, dataloader, loss_function, optimizer, epoch, 10) train_loss.append(train_loss_tmp) test_loss_tmp = test(model, device, test_dataloader, loss_function) test_loss.append(test_loss_tmp) 为了对比三种模型的性能，我们另写一个Python脚本来调用这三种模型。之前的代码用的是命令行参数，这里我们将参数改为函数参数，并让函数返回训练中的损失值、测试的准确率，以及模型的预测结果。这样我们就可以在调用函数的脚本中绘制模型的性能曲线。\n修改后的主脚本参见GitHub仓库T05_series_gru_lstm文件夹中的time_series_models.py文件。比较性能的脚本参见T05_series_gru_lstm文件夹中的compare_results.py文件。\n运行代码 # 我们运行比较性能的脚本，可以得到如下结果：\n三种模型的损失值曲线：\n三种模型的预测结果：\ncompare_results.py脚本依次运行了RNN、LSTM和GRU三种模型，如果在我的电脑上用GPU（NVIDIA GeForce GTX 4060 Ti）运行，总运行时间约30秒，最高显存占用约354 MB。若使用CPU（Intel i5-9600K）运行，总运行时间约3分33秒。\n我运行了这个比较性能的脚本多次，每次的结果都有一些差异。大部分情况下，三个模型都能够很好地拟合正弦函数的序列数据，但总体上LSTM和GRU的表现要略好于RNN。\n总结 # 在本文中，我们介绍了门控循环单元（GRU）和长短期记忆网络（LSTM）的工作原理，并使用PyTorch实现了这两种模型。我们使用这两种模型来实现对正弦函数的序列预测，并与简单循环神经网络（RNN）进行了比较。我们发现，LSTM和GRU相对于RNN来说，能够更好地捕捉序列数据中的长期依赖关系，从而提高了模型的性能。\n","date":"2024年8月4日","externalUrl":null,"permalink":"/p/%E5%AE%9E%E4%BE%8B%E5%AD%A6pytorch5%E5%BA%8F%E5%88%97%E9%A2%84%E6%B5%8B%E4%BA%8C%E9%97%A8%E6%8E%A7%E5%BE%AA%E7%8E%AF%E5%8D%95%E5%85%83gru%E5%92%8C%E9%95%BF%E7%9F%AD%E6%9C%9F%E8%AE%B0%E5%BF%86%E7%BD%91%E7%BB%9Clstm/","section":"Posts","summary":" 背景 # 这是“实例学PyTorch”系列的第5篇文章。在第4篇文章“实例学PyTorch（4）：序列预测（一）——循环神经网络（RNN）”中，我们介绍了序列预测问题，以及如何使用一个简单的循环神经网络（RNN）来实现对正弦函数的预测。在本文中，我们将更进一步，介绍序列预测中另外两种常用的神经网络：门控循环单元（Gated Recurrent Unit，GRU）和长短期记忆网络（Long Short-Term Memory，LSTM）。\n","title":"实例学PyTorch（5）：序列预测（二）——门控循环单元（GRU）和长短期记忆网络（LSTM）","type":"post"},{"content":"","date":"2024年8月4日","externalUrl":null,"permalink":"/tags/%E5%BE%AA%E7%8E%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/","section":"Tags","summary":"","title":"循环神经网络","type":"tags"},{"content":"","date":"2024年7月21日","externalUrl":null,"permalink":"/tags/conda/","section":"Tags","summary":"","title":"Conda","type":"tags"},{"content":"","date":"2024年7月21日","externalUrl":null,"permalink":"/tags/env/","section":"Tags","summary":"","title":"Env","type":"tags"},{"content":"","date":"21 七月 2024","externalUrl":null,"permalink":"/en/tags/environment/","section":"Tags","summary":"","title":"Environment","type":"tags"},{"content":"","date":"2024年7月21日","externalUrl":null,"permalink":"/tags/mamba/","section":"Tags","summary":"","title":"Mamba","type":"tags"},{"content":"","date":"2024年7月21日","externalUrl":null,"permalink":"/tags/python/","section":"Tags","summary":"","title":"Python","type":"tags"},{"content":" 缘起 # 在使用Python开发时，遇到了很多配置环境的情形。有时需要安装不同版本的Python，有时需要安装不同的Python包，有时需要安装不同的Python包的不同版本。\n最开始使用时我基本上都是看网上的教程，不同的Python项目可能使用不同的环境管理工具，不知不觉就用过了pip、conda、mamba、virtualenv、venv等等。这些工具各有优劣，有时候我也会混用，导致环境变量混乱，有时候还会出现冲突。\n由于一直没有什么一致性，导致电脑上的Python环境比较乱，我自己也感觉云里雾里的，所以我打算总结一下这些工具的使用方法，以及它们的优缺点。最后会给出我自己推荐的一些使用方法。\nPython管理工具分类 # Python管理工具可以分为两类：\n包管理工具：用于安装、卸载、更新Python包，如pip、conda、mamba等。\n环境管理工具：用于管理Python环境，如venv、virtualenv、conda、mamba等。\n我们这里主要讨论环境管理工具，因为包管理工具的使用方法比较简单，而且大多数情况下我们都会使用pip，所以这里不再赘述。\n之前我也写过一篇关于Linux下使用module管理软件包和环境变量的文章，可以参考\u0026ldquo;Linux下使用module管理软件包和环境变量\u0026rdquo;\n环境管理工具 # venv # venv是Python自带的环境管理工具，其使用起来是最简单，也是最不需要操心的。\n使用方法 # 创建一个新的环境：\n1 python -m venv .venv 这样就会在当前目录下创建一个.venv的文件夹，里面包含了一个新的Python环境。当然，这里的.venv可以替换成任何你想要的名字，用.venv是因为这样可以创建一个自动隐藏的文件夹，在git中也很容易把它忽略掉。\n注意，这里的python是你想要创建环境的Python版本，如果你有多个Python版本，可以使用python3或者python3.10等。\n激活这个环境：\n如果是Linux系统或者MacOS系统，可以使用如下命令：\n1 source .venv/bin/activate 如果是Windows系统，可以使用如下命令：\n1 .venv\\Scripts\\activate 安装Python包：\n在激活环境之后，使用pip安装Python包会把包安装到这个环境中，而不是全局环境。安装的包会放在.venv/lib/python3.10/site-packages目录下。\n1 pip install numpy 退出环境：\n在环境中使用deactivate命令可以退出环境。\n1 deactivate 优缺点 # 优点：\n原生工具，简单易用，不需要安装额外的软件。 速度快，因为不需要下载额外的软件包。 环境隔离，不会影响全局环境。 可以在任何地方创建环境，不需要管理员权限。 每个项目中的环境都在本项目目录下，与其他项目做到了物理隔离，不易弄混。 缺点：\n每个项目都需要创建一个新的环境，如果有很多项目，会占用很多空间。 无法共享环境，如果有多个项目使用同一个环境，需要重复创建。 不好切换Python版本，每个环境都是一个Python版本，无法切换。 只适用于Python 3.3及以上版本。 简单来说，如果你不在意空间占用，不需要切换Python版本，那么venv是一个很好的选择。\nvirtualenv # 由于venv有很大的占用空间的问题，所以有人开发了virtualenv，它是一个第三方的环境管理工具，可以解决venv的一些问题。\nvirtualenv的使用方法和venv基本一样，只是需要安装virtualenv这个包。\n使用方法 # 安装virtualenv：\n1 pip install virtualenv 创建一个新的环境：\n1 virtualenv .venv 如果你想指定Python版本，可以使用如下命令：\n1 virtualenv -p python3.10 .venv 激活这个环境的方式和venv一样。\n安装Python包的方式和venv一样。\n但它和venv的区别在于，virtualenv会复用系统的Python库，不会重复安装，所以占用的空间会小很多。如果你不想复用系统的Python库，可以使用--no-site-packages选项，在本项目中安装全新的Python库。\n退出环境的方式和venv一样。 优缺点 # 优点：\n可以指定Python版本。 可以共享环境，不需要重复创建。 占用空间小，不会重复安装Python库。 适用于Python 2.7及以上版本。 缺点：\n非原生工具，需要安装额外的软件。 简单来说，如果你在意空间占用，且想要和venv相似的使用体验，那么virtualenv是一个很好的选择。\nconda # conda是一个非常强大的环境管理工具，它不仅可以管理Python环境，还可以管理其他语言的环境，如R、Julia等。\nconda、Anaconda、miniconda、mamba、micromamba之间的关系 # conda是Anaconda的一部分，Anaconda是一个数据科学家常用的工具包，里面包含了很多数据科学家常用的软件包，如numpy、pandas、scipy等。但有用户反映Anaconda安装的软件包太多，占用空间太大，所以有人开发了miniconda，它是Anaconda的精简版，只包含了conda和一些基本的软件包。\n不管是Anaconda还是miniconda，都是使用conda这个包管理工具来管理环境，但conda在安装软件包时是单线程的，速度比较慢，所以有人开发了mamba，它是conda的一个加速版，速度比conda快很多。\nmamba和conda的使用方法基本一样，只是在安装软件包时使用mamba代替conda。用户基本可以认为，mamba命令是conda命令的一个别名。\nmicromamba是mamba的一个精简版，micromamba的base环境是空的，不包含任何软件包。\nconda的原理（也适用于mamba） # conda环境管理工具会在用户的家目录下创建一个.conda文件夹，里面包含了所有的环境，每个环境都是一个文件夹，里面包含了一个Python环境和一个软件包列表。\n用户每创建一个新的环境，conda环境管理工具就会在.conda文件夹下创建一个新的文件夹，里面包含了一个新的Python环境和一个新的软件包列表。用户每安装一个新的软件包，conda包管理工具就会把这个软件包安装到对应的环境中。\n用户每激活一个环境，conda环境管理工具就会把这个环境的Python环境和软件包列表添加到环境变量中，用户每退出一个环境，conda环境管理工具就会把这个环境的Python环境和软件包列表从环境变量中删除。\n创建好的conda环境跟某个项目无关，用户可以在任何地方激活这个环境，不需要在项目目录下创建环境。\n使用方法（以mamba为例） # 安装conda或者mamba：\n你可以在miniforge找到最小化的conda或者mamba安装包，下载安装即可。\n创建一个新的环境：\n1 mamba create -n myenv python=3.10 这里的myenv是环境的名字，python=3.10是指定Python的版本。\n激活这个环境：\n1 mamaba activate myenv 安装Python包：\n1 mamba install numpy 退出环境：\n1 mamba deactivate 优缺点 # 优点：\n可以指定Python版本。 可以共享环境，不需要重复创建。 可以安装其他语言的环境。 速度快，因为有mamba这个加速版。 可以在任何地方激活环境，不需要在项目目录下创建环境。 可以共享软件包，不需要重复下载。 缺点：\n非原生工具，需要安装额外的软件。 环境跟具体项目是分离的，对于较久未使用的环境，可能会忘记这个环境是干什么的。对于较久未使用的项目，可能会忘记这个项目使用的是哪个环境。 简单来说，如果你想要一个强大的环境管理工具，而且想要安装包时速度快，那么conda或者mamba是一个很好的选择。但在开发某个项目时，最好标注一下这个项目使用的是哪个环境，以免忘记。\n冻结环境 # 无论使用哪种环境管理工具，都可以使用pip freeze命令来冻结环境，即把当前环境的软件包列表保存到一个文件中。\n1 pip freeze \u0026gt; requirements.txt 这样就会在当前目录下创建一个requirements.txt文件，里面包含了当前环境的软件包列表。当你需要在另一个环境中安装这些软件包时，可以使用如下命令：\n1 pip install -r requirements.txt 这样就会安装requirements.txt文件中列出的所有软件包。\n总结 # 如果你不在意空间占用，适用高于3.3版本的Python，不需要切换Python版本，只想要一个最简单的环境管理工具，那么venv是一个很好的选择。 如果你在意空间占用，想要共享环境，适用于2.7及以上版本的Python，那么virtualenv是一个很好的选择。 如果你想要一个强大的环境管理工具，想要安装包时速度快，那么conda或者mamba是一个很好的选择。 推荐的使用方法 # 如果你不在于空间占用，且可能用到很多科学计算的包，在你的主力开发电脑上，可以安装Anaconda。 如果你在意空间占用，只想在需要时安装包，可以在你的主力开发电脑上安装micromamba。 如果你不在意空间占用，在你的测试电脑上，或者在你的服务器上，可以使用venv。 ","date":"2024年7月21日","externalUrl":null,"permalink":"/p/python%E7%8E%AF%E5%A2%83%E7%AE%A1%E7%90%86%E6%96%B9%E5%BC%8F%E6%80%BB%E7%BB%93/","section":"Posts","summary":" 缘起 # 在使用Python开发时，遇到了很多配置环境的情形。有时需要安装不同版本的Python，有时需要安装不同的Python包，有时需要安装不同的Python包的不同版本。\n","title":"Python环境管理方式总结","type":"post"},{"content":"","date":"2024年7月21日","externalUrl":null,"permalink":"/tags/%E7%8E%AF%E5%A2%83/","section":"Tags","summary":"","title":"环境","type":"tags"},{"content":"","date":"2024年7月14日","externalUrl":null,"permalink":"/tags/anydesk/","section":"Tags","summary":"","title":"AnyDesk","type":"tags"},{"content":" 缘起 # 我在家和在办公室都有数台电脑，运行的操作系统包括Windows、macOS和Linux。\n简介 # 市场上有不少远程桌面软件，例如Teamviewer、Chrome Remote Desktop、ToDesk、RustDesk等。之前我使用过Teamviewer，但是Teamviewer的免费版有使用时间限制，我用了一段时间后不知道为啥取消了我的免费使用资格，后来我就开始用AnyDesk了。\nAnyDesk是一款跨平台的远程桌面软件，支持Windows、macOS、Linux和Android系统。AnyDesk的特点有：\n个人使用免费，商业使用需要付费。 支持多种操作系统。 连接速度较快，画质清晰。 安装 # Windows和macOS # 在AnyDesk官网下载对应系统的安装包，然后安装即可。\nLinux # AnyDesk提供了.deb和.rpm两种Linux系统的安装包，可以在AnyDesk官网下载对应的安装包，Debian/Ubuntu系统使用.deb包：\n```bash sudo dpkg -i anydesk_x.x.x-x_amd64.deb ``` RedHat/Fedora/OpenSUSE系统使用.rpm包：\n```bash sudo rpm -i anydesk-x.x.x-x.x86_64.rpm ``` 安卓 # Anydesk也有安卓手机版，可以在Google Play下载安装。\n这也是Anydesk的一个很大的优势，因为其他很多远程桌面软件都没有安卓版。有了安卓版，就可以使用手机对远程电脑做一些简单的控制（复杂的操作在手机上还是太局促了），这在一些特殊情况下还是很有用的。\n配置和使用 # 基本使用很简单，这里不再赘述。比较有用的包括：\n如果在macOS系统上使用AnyDesk，可以在系统设置中开启分享屏幕权限。 如果想要远程电脑在无人值守的情况下，需要开启Unattended Access功能。Unattended Access功能需要设置密码，然后在输入远程电脑的地址和密码即可连接。 在设置中的User Interface可以为你的电脑设置一个别名，这样在连接时就不用记住电脑地址的一长串数字了。 问题 # 在Linux系统上使用AnyDesk时，我遇到了一个问题：The session has ended. Status: display_server_not_supported。\n这个问题是因为AnyDesk不支持Wayland显示服务器，只支持X11显示服务器。解决方法是在登录界面选择X11显示服务器，然后再登录。\n在系统重启之后如果直接尝试从远程连接Anydesk，会遇到另一个错误：The session has ended. Status: desk_rt_ipc_error。\n这是因为重启后没有登录进系统，AnyDesk无法正常工作。解决方法是在系统重启后登录进系统，然后再连接AnyDesk。但是如果你不在远程电脑旁边，没法登录进系统，可以设置自动登录，这样重启后AnyDesk就可以正常工作了。\n1 /etc/gdm3/custom.conf enable automatic login 也可以编辑/etc/gdm3/custom.conf文件，添加以下内容：\n1 2 3 4 5 [daemon] AutomaticLoginEnable=True AutomaticLogin=username WaylandEnable=false 其中username是你的用户名。\nWaylandEnable=false是为了禁用Wayland，因为AnyDesk不支持Wayland。\n","date":"2024年7月14日","externalUrl":null,"permalink":"/p/%E8%BF%9C%E7%A8%8B%E6%A1%8C%E9%9D%A2%E4%B8%80anydesk%E7%9A%84%E5%AE%89%E8%A3%85%E4%B8%8E%E4%BD%BF%E7%94%A8/","section":"Posts","summary":" 缘起 # 我在家和在办公室都有数台电脑，运行的操作系统包括Windows、macOS和Linux。\n","title":"远程桌面（一）：AnyDesk的安装与使用","type":"post"},{"content":"","date":"2024年6月30日","externalUrl":null,"permalink":"/tags/arch-linux/","section":"Tags","summary":"","title":"Arch Linux","type":"tags"},{"content":" 缘起 # 手上有一个2018年的老Surface Pro 6，现在性能已经有些跟不上了，但还可以拿来玩一玩，就准备安装一两个轻量级的Linux系统，顺便熟悉熟悉不同的Linux发行版，为之后更新主力机的Linux系统做准备。\n一直听说Arch Linux是Linux发行版中的“邪教”，以其极简、高度定制、滚动更新等特点而著称，所以就决定尝试一下，看看Arch Linux到底有多“邪教”。\nArch Linux简介 # Arch Linux是一个极其轻量级的Linux发行版，它的设计哲学是简单、轻量、灵活。Arch Linux主要特点有：\n滚动更新：Arch Linux是一个滚动更新的发行版，不像Ubuntu、Debian等发行版有固定的版本号，而是持续更新软件包，用户可以一直用最新的系统。但这也意味着用户需要经常更新系统，否则可能会出现软件包不兼容的问题，而且需要承担新系统有bug或不稳定的风险，俗称更新系统时会“滚挂”（尽管我认为现在的Arch Linux系统发生“滚挂”的频率已经很低了）。 简单：Arch Linux的设计哲学是简单，它提供了一个干净的系统，用户可以根据自己的需求定制系统。Arch Linux的安装包不包含图形界面，也不含预装软件，用户需要自己选择安装。 软件丰富：Arch Linux提供了一个强大的软件包管理工具pacman，用户可以通过pacman安装、卸载、更新软件包，还可以通过AUR（Arch User Repository）安装第三方软件包。pacman+AUR可能是Linux发行版中最大的软件仓库，用户可以在AUR中找到几乎所有的软件包，但有些软件的打包质量可能不高，需要用户自己注意。 这些特点使得Arch Linux有利有弊，总的来说，Arch Linux适合那些有一定经验、喜欢折腾、有洁癖、追求新功能的用户。\nArch Linux安装 # 早年间Arch Linux的安装完全依靠用户手敲命令，现在Arch Linux提供了一个安装向导archinstall，大大简化了安装过程。尽管如此，安装Arch Linux依然需要用户知道一些基础知识，比如磁盘分区、文件系统、引导方式、网络配置等。\n准备安装U盘 # 首先我们需要制作一个启动盘，建议使用Ventoy工具。Ventoy是一个非常好用的工具，可以将多个ISO文件写入U盘，方便我们选择不同的系统安装，也不耽误U盘的正常使用。具体用法可参见官方文档。\n下载Arch Linux ISO # 从Arch Linux官网下载最新的Arch Linux ISO文件，然后将ISO文件复制到U盘中。Arch Linux的系统镜像一般每月1号更新一次。\n安装 # 插入U盘，启动Surface Pro 6，按住Volume Up键进入BIOS设置，将U盘设置为第一启动项。对于其他设备，可能需要按F2、F12、ESC等键进入BIOS设置。\n选择Boot Arch Linux (x86_64)，进入Arch Linux安装界面。然后会看到滚动的一堆命令，等待一会儿就会进入Root用户的命令行界面。\n在开始安装之前，我们需要连接网络。一般Arch Linux会自动识别有线网络，如果是无线网络，需要使用命令行工具iwctl连接。常用的命令有\ndevice list station \u0026lt;device_name\u0026gt; connect \u0026lt;wifi_name\u0026gt; 然后输入密码 station \u0026lt;device_name\u0026gt; show查看连接状态 运行 archinstall 开始安装\n建议事先给磁盘分好区 安装时选择manual partition，依次选择磁盘和分区，给分区选择挂载点。至于如何分区，可参考多系统启动项配置 至于网络选项，我也没太搞懂，但基本原则是： 如果选择不装图形界面，就选择复制ISO里的网络设置，即执行archinstall之前的联网设置 如果选择装图形界面，就选NetworkManager。但是Network Manager似乎需要需要disable iwd，并且需要安装Surface特定的驱动。 安装结束后会自动chroot到安装好的系统里，可以执行安装后的命令。\n安装Surface联网需要的驱动：pacman -S linux-firmware-marvell。 如果不装图形界面，要想重启后顺利联网，需要安装iwd和dhcpcd。 重启\n我用的rEFInd作为启动加载器，rEFInd会扫描到两处Arch的启动项，但总有一个不能启动 重启后联网\n若使用图形界面，如果没有网络图标，那要么是没装NetworkManager，要么没装驱动 若不装图形界面，需要使用iwd联网，先启动iwd： 1 sudo systemctl start iwd 然后启动dhcpcd.service 1 sudo systemctl start dhcpcd.service 在命令行界面也可以使用NetworkManager联网： nmcli device show nmcli device wifi connect \u0026lt;wifi_name\u0026gt; password \u0026lt;password\u0026gt; ","date":"2024年6月30日","externalUrl":null,"permalink":"/p/arch-linux%E7%B3%BB%E7%BB%9F%E5%AE%89%E8%A3%85%E4%B8%8E%E9%85%8D%E7%BD%AE/","section":"Posts","summary":" 缘起 # 手上有一个2018年的老Surface Pro 6，现在性能已经有些跟不上了，但还可以拿来玩一玩，就准备安装一两个轻量级的Linux系统，顺便熟悉熟悉不同的Linux发行版，为之后更新主力机的Linux系统做准备。\n","title":"Arch Linux系统安装与配置","type":"post"},{"content":"","date":"30 六月 2024","externalUrl":null,"permalink":"/en/categories/os/","section":"Categories","summary":"","title":"OS","type":"categories"},{"content":"","date":"2024年5月24日","externalUrl":null,"permalink":"/tags/efi/","section":"Tags","summary":"","title":"EFI","type":"tags"},{"content":"","date":"2024年5月24日","externalUrl":null,"permalink":"/tags/refind/","section":"Tags","summary":"","title":"REFInd","type":"tags"},{"content":" 缘起 # 我之前在电脑上装过双系统或者多系统，但对于多个系统的启动项一直云里雾里，经常出现启动项丢失、启动项重复等问题。这次我打算好好研究一下多系统启动项的配置，包括启动加载器的设置、EFI/Grub/rEFInd等，以及如何定制多系统启动器主题。\n更新：本文主要讲启动引导的基本概念和多系统启动项的配置方法。我本来以为这一篇文章就把多系统启动的问题讲明白了，但万万没想到我在配置双硬盘Windows/Linux双系统启动时还踩了一个巨大的坑，不得不再写一篇文章，从而把启动引导也搞成了一个系列。如果你想搞双硬盘Windows/Linux双系统启动，强烈建议你再参考一下本系列的第二篇文章：\n启动引导（二）：双硬盘下Windows/Linux双系统启动项配置踩坑 操作系统启动流程 # 计算机的启动过程主要分为4步：1. 上电；2. BIOS/UEFI启动；3. 加载启动项；4. 操作系统启动。第1步和第4步一般不需要用户干预，而第2步和第3步需要用户配置。所以这里主要介绍第2步和第3步的内容。\nBIOS/UEFI启动 # BIOS（Basic Input/Output System）和UEFI（Unified Extensible Firmware Interface）是计算机的固件，负责启动计算机并加载操作系统。BIOS是旧的启动方式，UEFI是新的启动方式，UEFI相对于BIOS有更多的功能和更好的性能。无论是UEFI还是BIOS，它们都是主板上的固件。\nBIOS启动流程 # BIOS一般在比较旧的计算机上使用，其启动流程比较固定，用户定制的自由度比较低。主要分为以下步骤：\nBIOS POST（Power-On Self-Test）自检\nBIOS读取MBR（Master Boot Record）中的引导程序\n引导程序读取分区表，找到活动分区\n引导程序读取活动分区的引导扇区，加载操作系统\nMBR的位置是固定的，一般在硬盘的第一个扇区，大小为512字节。MBR中包含了分区表和引导程序，分区表记录了硬盘的分区信息，引导程序负责加载操作系统。\nUEFI启动流程 # UEFI也是BIOS的一种，一般在比较新的计算机上使用。其启动流程比较灵活，包含的功能更多，用户定制的自由度比较高。尽管UEFI的功能更多，但其流程更为统一，主要分为以下步骤：\nUEFI POST（Power-On Self-Test）自检\nUEFI读取EFI分区中的引导程序\n引导程序读取EFI分区中的引导文件，加载操作系统\n相比BIOS，UEFI使用的EFI分区的位置并不固定，甚至一块硬盘上可以有多个EFI分区。\nUEFI的引导程序和引导文件都存储在EFI分区中，包含了引导程序、引导文件、驱动程序等。只要引导文件符合UEFI的规范，无论是Windows、Linux还是macOS的引导程序，也无论引导文件放在哪个EFI分区，UEFI都可以找到并加载。\n由于UEFI更灵活也更新，所以接下来讲的多系统启动项配置都是基于UEFI的。\n概念辨析：启动加载器、启动项、启动文件 # 在讲多系统启动项配置之前，我们先来梳理一下启动加载器、启动项和启动文件的概念。\n启动加载器（Boot Loader）：负责加载操作系统的启动引导程序，每个启动引导程序就是一个启动项（Boot Entry）。常用的启动加载器有Grub（Grub2）、rEFInd、Clover等。\n启动项（Boot Entry）：启动加载器加载的每个操作系统就是一个启动项，每个启动项对应一个操作系统。启动项包含了操作系统的启动文件、内核、驱动程序等。\n启动文件（Boot File）：启动项中的启动文件，是操作系统的引导程序，负责加载操作系统。Windows的启动文件是bootmgfw.efi，Linux的启动文件是vmlinuz。\n在此基础上，我们再来看UEFI是如何启动操作系统的。\n每个启动加载器都对应着一个.efi文件，这个.efi文件就是启动加载器的启动文件，负责加载启动加载器。在UEFI完成自检后，会读取EFI分区中的所有.efi文件，如果你在此时进入BIOS设置，看到的启动项就是EFI分区中的.efi文件。这些启动项是有优先顺序的，其顺序可以在BIOS设置中调整。\n例如下图中最高优先级的启动项是rEFInd，其次是Windows Boot Manager，最后是ubuntu。电脑启动时会先试图加载rEFInd，如果rEFInd不存在，再尝试加载Windows Boot Manager，以此类推。\n如果rEFInd成功加载，rEFInd会读取EFI分区中的refind.conf配置文件，根据配置文件生成启动菜单，显示在屏幕上。其中每个菜单项对应一个启动项，启动项一般有两类：一类是操作系统内核镜像，另一类是启动加载器。\n操作系统内核镜像：对应的是操作系统的内核文件，例如Linux的vmlinuz文件，Windows的bootmgfw.efi文件。 启动加载器：对应的是启动加载器的.efi文件，例如Grub2的grubx64.efi文件。 用户选择一个菜单项后，rEFInd会加载对应的启动项，如果启动项是操作系统内核镜像，rEFInd会直接加载操作系统；如果启动项是启动加载器，rEFInd会加载启动加载器，启动加载器再加载操作系统。\n如果启动加载器要启动一个Linux系统，那么首先会加载Linux的内核文件vmlinuz-xxx，然后加载Linux的初始化文件系统文件initrd-xxx.img或者initramfs-xxx.img，最后加载Linux系统。这两个文件都在Linux系统的/boot目录下。\n多系统安装时的启动项配置 # 上面说了UEFI的引导程序和引导文件都存储在EFI分区中，所以在安装多个系统时，每个系统都会在EFI分区中创建一个引导文件，这样就会有多个引导文件，每个引导文件对应一个系统。这样就可以通过选择不同的引导文件来启动不同的系统。\n在安装多个操作系统时，我们可以把各个系统的引导文件放入同一个EFI分区中，也可以把各个系统的引导文件放入不同的EFI分区中。两种方法各有利弊，具体选择哪种方法取决于个人喜好。\n同一个EFI分区\n这种情况下的硬盘分区结构如下：\n优点：方便管理，只需要一个EFI分区，不需要多个EFI分区。更易于配置启动加载器。\n缺点：所有的引导文件都在一个EFI分区中，万一EFI分区损坏，所有的系统都无法启动。\n安装多个系统的具体操作如下：\n硬盘分区：在硬盘上分出一个EFI分区，大小最好为1GB以上，格式为FAT32。其他分区可以按照各个操作系统的需求而定，例如Windows一般需要至少52GB的空间，Linux一般需要至少20GB的空间。 安装Windows：如果安装的多个操作系统既有Windows又有Linux，建议先安装Windows。因为Windows的引导程序会覆盖其他已安装操作系统的引导程序，导致先前安装的Linux无法启动。安装Windows时，选择给Windows分配的分区，Windows会自动在EFI分区中创建引导文件。 依次安装各个Linux：对于每个Linux系统，安装时一般选择自定义分区，手动挂载EFI分区到/boot/efi，挂载为Linux准备的分区到/，可选择创建交换分区和/home分区。安装完成后，Linux会在EFI分区中创建引导文件。 配置启动加载器：多个操作系统依次安装完成后，我们需要配置启动加载器，让我们可以选择要启动的操作系统。这些内容会在后面详细介绍。 不同的EFI分区\n这种情况下的硬盘分区结构如下：\n优点：每个系统有自己的EFI分区，互不干扰。一个系统的EFI分区损坏，其他系统不受影响。\n缺点：需要多个EFI分区，管理起来比较麻烦。每个系统的引导文件都在不同的EFI分区中，不易于配置启动加载器。\n安装多个系统的具体操作如下：\n硬盘分区：在硬盘为每个系统分别分出一块空间。暂时不需要创建EFI分区，安装系统时会在每个系统各自的分区中分别创建EFI分区。 安装Windows：安装Windows时，选择给Windows分配的分区，Windows会自动在该分区中创建EFI分区。 安装Linux：对于每个Linux系统，安装时选择为这个系统分配的分区，在这个分区中创建大小为200MB左右的EFI分区，挂载点为/boot/efi；剩余的空间挂载为Linux准备的分区，挂载点为/；可选择创建交换分区和/home分区。 配置启动加载器：多个操作系统依次安装完成后，我们需要配置启动加载器，让我们可以选择要启动的操作系统。这些内容会在后面详细介绍。 启动目录结构 # 我们来看一下启动目录的结构，以及安装一个操作系统时，这个操作系统会创建哪些文件。这里假设多个操作系统的引导文件都放在同一个EFI分区中，我们安装了3个操作系统：Windows、Ubuntu、Arch Linux。其中Ubuntu系统使用Grub2作为启动加载器，Arch Linux系统使用systemd-boot作为启动加载器。我们还在Ubuntu系统下安装了rEFInd作为启动加载器。\nEFI分区\nEFI分区是一个FAT32格式的独立的分区，一般大小为1GB左右。EFI分区中主要包含各个启动加载器的引导文件。\nEFI分区的目录结构如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 EFI ├── Boot │ └── bootx64.efi ├── Microsoft │ └── Boot │ ├── BCD │ ├── BCD.LOG │ ├── BCD.LOG1 │ ├── BCD.LOG2 │ ├── bootmgfw.efi │ ├── bootmgr.efi │ └── boot.stl ├── systemd │ └── systemd-bootx64.efi └── ubuntu ├── grub.cfg ├── grubx64.efi ├── mmx64.efi └── shimx64.efi EFI/Boot/bootx64.efi：UEFI的默认引导文件，一般指向Windows的引导文件EFI/Microsoft/Boot/bootmgfw.efi。 EFI/Microsoft/Boot/bootmgfw.efi：Windows的引导文件，负责加载Windows操作系统。 EFI/systemd/systemd-bootx64.efi：systemd-boot的引导文件，负责加载Linux操作系统。 EFI/ubuntu/grubx64.efi：Grub2的引导文件，负责加载Linux操作系统。 EFI/ubuntu/shimx64.efi：Secure Boot的引导文件，用于加载Grub2。 EFI/ubuntu/mmx64.efi：Grub2的引导文件，用于加载Grub2。 EFI/ubuntu/grub.cfg：Grub2的配置文件，包含了各个启动项的配置。 安装操作系统\n安装Ubuntu时，选择自定义分区，手动挂载EFI分区到/boot/efi，挂载为Linux准备的分区到/。安装完成后，Ubuntu会在EFI分区中创建EFI/ubuntu目录，其中包含了Grub2的引导文件和配置文件。Ubuntu会在/boot目录中创建两个文件：vmlinuz和initrd.img，这两个文件是Linux的内核文件和初始化文件系统文件。Grub2的引导文件会识别这两个文件，并创建启动项，在启动时选择相应的启动项，就可以加载Ubuntu系统。\n在Ubuntu系统中，可以进入/boot/efi/EFI中查看EFI分区的目录结构。\n1 2 3 4 5 6 7 8 9 /boot ├── efi │ └── EFI │ ├── Boot │ ├── Microsoft │ ├── systemd │ └── ubuntu ├── vmlinuz └── initrd.img 注意，虽然/boot/efi/EFI在/boot下，但/boot/efi/EFI中的文件在EFI分区里，而/boot下的其他文件在Ubuntu系统的分区里。/boot下的vmlinuz和initrd.img文件在其他系统中是无法访问的，而若其他系统也挂载了EFI分区，那么其他系统也可以访问/boot/efi/EFI中的文件。\n如果继续安装更多的操作系统，一般操作系统都会让你选择启动加载器，你可以选择使用Grub2，也可以选择systemd-boot，或者其他启动加载器。下面介绍一下启动加载器的配置。\n启动加载器 # 启动加载器（Boot Loader）负责加载操作系统的启动引导程序，每个启动引导程序就是一个启动项（Boot Entry）。常用的启动加载器有Grub（Grub2）、rEFInd、Clover等。这里主要介绍Grub2和rEFInd。\nGrub2 # Grub2（GNU GRand Unified Bootloader 2）是Linux系统上常用的启动加载器，功能强大，支持多系统启动。Grub2的工作文件夹是/boot/grub（Ubuntu等系统）或/boot/grub2（fedora等系统），Grub2的配置文件是位于工作文件夹内的grub.cfg，一般由grub-mkconfig命令生成。\nGrub2的配置文件比较复杂，但功能强大，可以定制各种启动项。Grub2的主题也可以定制，但相对比较复杂。\nGrub2的配置 # 配置文件\nGrub2的主配置文件是/boot/grub/grub.cfg或/boot/grub2/grub.cfg，可以通过编辑这个文件来配置Grub2的启动项。\n1 2 sudo cat /boot/grub/grub.cfg # Ubuntu sudo cat /boot/grub2/grub.cfg # fedora 不过一般不建议直接编辑grub.cfg文件，因为这个文件是由grub-mkconfig命令生成的，如果直接编辑grub.cfg文件，下次更新Grub2时会被覆盖。建议通过配置/etc/default/grub文件，以及/etc/grub.d/目录下的配置文件来配置Grub2。\n启动项的配置\nGrub2的启动项配置比较复杂，例如：\n1 2 3 4 5 GRUB_DEFAULT=0 GRUB_TIMEOUT=5 GRUB_DISTRIBUTOR=`lsb_release -i -s 2\u0026gt; /dev/null || echo Debian` GRUB_CMDLINE_LINUX_DEFAULT=\u0026#34;quiet splash\u0026#34; GRUB_CMDLINE_LINUX=\u0026#34;\u0026#34; GRUB_DEFAULT：默认启动项的编号，从0开始。 GRUB_TIMEOUT：启动菜单显示的时间，单位为秒。 GRUB_DISTRIBUTOR：系统的发行商。 GRUB_CMDLINE_LINUX_DEFAULT：Linux内核的默认启动参数。 GRUB_CMDLINE_LINUX：Linux内核的启动参数。 定制主题\nGrub2的主题可以定制，可以把想要的主题文件放在/boot/grub/themes目录下。主题目录下包含了主题文件、字体文件、背景图片等，而且有一个名为theme.txt的配置文件，可以配置主题的各种属性。\n在配置文件/etc/default/grub中可以指定主题文件：\n1 GRUB_THEME=\u0026#34;/boot/grub/themes/theme-name/theme.txt\u0026#34; Grub2配置生效\nGrub2的配置文件/boot/grub/grub.cfg或/boot/grub2/grub.cfg是由grub-mkconfig命令生成的，所以每次修改配置文件后，都需要重新生成grub.cfg文件。\n1 2 sudo grub-mkconfig -o /boot/grub/grub.cfg # Ubuntu sudo grub2-mkconfig -o /boot/grub2/grub.cfg # fedora 也可以使用update-grub命令来更新Grub2配置。\n1 2 sudo update-grub # Ubuntu sudo grub2-mkconfig -o /boot/grub2/grub.cfg # fedora 更多的Grub2配置可以参考Grub2配置。\nrEFInd # rEFInd是一个基于UEFI的启动加载器，功能强大，支持多系统启动。rEFInd的配置文件是/boot/efi/EFI/refind/refind.conf，一般由refind-install命令生成。\nrEFInd的配置文件比较简单，但功能强大，可以定制各种启动项。rEFInd的主题也可以定制，相对比较简单。\nrEFInd安装 # rEFInd的安装比较简单，可以直接下载rEFInd的安装包，解压到EFI分区中。也可以使用refind-install命令安装rEFInd。\n1 refind-install 安装完成后，rEFInd会在EFI分区中创建EFI/refind目录，其中包含了rEFInd的引导文件和配置文件。\nrEFInd配置 # rEFInd的配置文件是/boot/efi/EFI/refind/refind.conf，可以通过编辑这个文件来配置rEFInd的启动项。\n1 sudo vim /boot/efi/EFI/refind/refind.conf 启动项的配置\nrEFInd中启动项的配置比较简单，例如：\n1 2 3 timeout 5 hideui singleuser scanfor manual,external timeout：启动菜单显示的时间，单位为秒。 hideui：隐藏启动菜单，只显示单用户模式。 scanfor：扫描启动项的方式，可以扫描手动配置的启动项，也可以扫描外部设备的启动项。 定制主题\nrEFInd的主题可以定制，可以把想要的主题文件放在/boot/efi/EFI/refind/theme目录下。主题文件是一个CSS文件，可以修改颜色、字体、背景等。\n在配置文件/boot/efi/EFI/refind/refind.conf中可以指定主题文件：\n1 include themes/theme-name/theme.conf 屏蔽启动项\n有时候rEFInd会扫描到很多不需要的启动项，例如如果你在安装系统时选择了使用Grub2作为启动加载器（Deepin, Fedora等系统会默认安装Grub2，而且似乎没法在安装时取消），那么rEFInd会扫描到Grub2的启动项。你可以通过配置文件/boot/efi/EFI/refind/refind.conf来屏蔽这些启动项。rEFInd可以选择忽略某些文件（dont_scan_files）、工具（dont_scan_tools）、目录（dont_scan_dirs）和磁盘（dont_scan_volumes）。\n1 2 dont_scan_files shim.efi,mmx64.efi dont_scan_dirs ESP:/EFI/ubuntu,EFI/deepin,EFI/fedora 问题解决 # 在SD卡或U盘上安装系统\n如果你的电脑有SD卡插槽，你可以选择把系统安装在SD卡上。 跟上面讲的安装多个操作系统的方法相似，在SD卡上安装操作系统也有两种EFI分区方式，一种是在SD卡上创建一个EFI分区，另一种是在安装系统时把硬盘上的EFI分区挂载到/boot/efi。 对于/boot分区，可以选择直接在SD卡上创建，也可以先在硬盘上创建一个大小约为500M的ext4分区，然后在安装系统时把它挂载到/boot。/boot里主要存放内核文件和引导文件，通常200M左右的空间就够了，但是如果你选择备份多个内核，那么就需要更大的空间。Fedora系统会限制/boot分区不小于512M。 如果你选择把/boot分区放在SD卡上，你可能会遇到一些安全方面的限制，例如Secure Boot可能会阻止从SD卡启动。你需要注册注册密钥，或者关闭Secure Boot。 我一般选择把EFI分区和/boot分区都放在硬盘上。 我还没试过在U盘上安装系统，不过原理应该和在SD卡上安装是一样的。\nFedora无法启动\n在SD卡上安装了Fedora系统后，我遇到了系统无法启动的问题。rEFInd可以扫描到Fedora的内核文件vmlinuz-xxx和initramfs-xxx.img，但是启动到一半会卡住，报错：\n1 2 3 4 5 6 7 Failed to switch root: Specified switch root path /sysroot does not seem to be an OS tree. os-release file is missing. initrd-switch-root.service: Main process exited, code=exited, status=1/FAILURE initrd-switch-root.service: Failed with result \u0026#39;exit-code\u0026#39;. Failed to start initrd-switch-root.service: Switch Root. Startup finished in 1.073s (kernel) + 1.000s (initrd) + 1.000s (userspace) = 3.073s. initrd-switch-root.service: Triggering OnFailure= dependencies. Started emergency.service - Emergency Shell. 在Emergency Shell中查看后发现，/sysroot目录为空，没有任何文件。而正常情况下，加载内核和文件系统初始化后，/sysroot目录应该包含操作系统的文件。\n所以这个问题是因为在启动内核和初始化文件系统时，SD卡没有被正确挂载。你可以在Emergency Shell中手动挂载SD卡，然后exit退出Emergency Shell，系统就可以正常启动了。\n1 2 mount /dev/sda1 /sysroot exit 但显然这不是一个好的解决方案，因为每次启动都要手动挂载SD卡。所以正确的解决方案是让rEFInd在启动内核时正确挂载SD卡，这需要给启动选项添加一个root参数，指定根目录。你可以在rEFInd的配置文件/boot/efi/EFI/refind/refind.conf中添加一个带有options参数的菜单项。\n1 2 3 4 5 6 menuentry \u0026#34;Fedora\u0026#34; { volume \u0026#34;SD-boot\u0026#34; loader /vmlinuz-xxx initrd /initramfs-xxx.img options \u0026#34;root=/dev/sda1\u0026#34; } 然而，这个解决方案也不是很好，因为\n在每次更新内核后，你都需要手动修改rEFInd的配置文件。 如果你有多个内核，这个菜单项不能自动生成子菜单，你需要手动添加每个内核的菜单项。 所以，更好的解决方案是让整个boot分区里的内核文件和引导文件都使用整个选项。做法是在/boot分区的根目录下创建一个refind_linux.conf文件，然后在rEFInd的配置文件/boot/efi/EFI/refind/refind.conf中添加一个root参数。\n1 \u0026#34;Boot with standard options\u0026#34; \u0026#34;root=/dev/sda1\u0026#34; 问题终于解决了！\nWindows/Linux双硬盘双系统的坑\n我本来以为经过上面的探索，我已经对多系统启动项非常熟悉了。所以当在一个双硬盘的电脑上安装Windows、Linux双系统时我信心满满，觉得在同一个硬盘上装多系统我都搞定了，俩系统分开装岂不更简单？然而现实却是啪啪打脸，我到最后甚至都没能优雅地解决。\n先说一下我的基本情况：电脑上装了两块固态硬盘A（1TB）和B（500GB），俩硬盘都装在主板上的M.2接口。**A硬盘装的那个M.2接口离CPU更近，B硬盘离CPU稍远一点。**离CPU远近有啥影响呢？我本来以为没啥影响，但最终发现这似乎是一个巨大的影响因素。\n我选择使用Fedora 42作为自己的主力系统，所以选择1T的固态硬盘A来装Fedora 42。Windows 11系统只是辅助系统，装在500G的B硬盘上。按照惯例，Windows/Linux双系统安装时选择优先安装Windows，因为后安装的话Windows会覆盖Linux的引导程序，导致Linux无法启动。但是我点亮机器时已经将Fedora 42先装到了硬盘A上，且已经用了几天了，懒得再把它清理掉了。而且我觉得我已经对启动引导了如指掌了，即使先装Linux再装Windows那自然也就是个调整启动引导的小事。然后我就有了下面这个第一次的失败经历。\n第一次失败的尝试\n失败的过程总结起来是这样的：\na. 先在硬盘A上装Fedora 42，成功引导进入系统。\nb. 再在硬盘B上装Windows 11。最保险的做法自然是将硬盘A拆下来，只留下硬盘B来安装Windows 11。这样Windows 11就会完全安装在硬盘B上，不会影响硬盘A上的Fedora 42。但是由于我懒得拆机，而且硬盘A那个M.2插槽还被显卡挡住了，拆机更麻烦。所以我就想，在安装Windows 11时能不能把硬盘A给屏蔽掉？\nc. 屏蔽硬盘A。首先我想到的是使用BIOS设置来禁用硬盘A，但我发现坑爹的MSI主板居然没有这个功能。我的Z890也算是中高端主板了，居然连这么个功能都没有。于是只能找其他方法屏蔽。经历了一番探索，发现可以通过在Windows安装时使用Diskpart工具来隐藏硬盘A。具体做法是这样的：\n启动Windows安装程序，在进入第一个安装界面（也就是选择语言那里）时按Shift + F10，打开命令提示符。\n在命令提示符中输入以下命令，启动Diskpart工具：\n1 diskpart 然后输入以下命令，列出所有磁盘：\n1 list disk 找到硬盘A的磁盘编号（例如Disk 0），然后输入以下命令，选择硬盘A：\n1 select disk 0 最后输入以下命令，将硬盘A下线：\n1 offline disk 这样，在本次Windows安装过程中，安装程序都无法看到硬盘A。\n退出Diskpart工具：\n1 exit 关闭命令提示符，继续安装Windows 11。\nd. 屏蔽硬盘A后继续运行安装程序。在安装程序运行到末期，需要建立启动引导，然后重启。这时问题来了，Windows 11的引导程序虽然看不到硬盘A，但由于硬盘A的M.2接口离CPU更近，Windows安装程序似乎意识到自己待的这个硬盘B不是第一块启动的硬盘，所以它仍然试图往启动顺序靠前的硬盘写入引导信息。但是硬盘A已经被下线了啊。于是Windows安装程序傲娇地拒绝把引导写在启动顺序靠后的硬盘B上，并报错说\n```sh Windows could not prepare the computer to boot into the next phase of installation. To install Windows, restart the installation. ``` 于是本次尝试以失败告终。\n第二次失败的尝试\n有了第一次的教训，我也懒得管硬盘A上的Fedora系统了，反正才用了几天，做的配置还不多。就按照传统方案，先装Windows，把Fedora的引导项损坏了也无所谓，之后再重装Fedora就是了。于是就有了下面的操作：\na. 在硬盘B上安装Windows 11，安装时不再将硬盘A下线。这倒是很顺利，能正常安装且正常启动进入Windows系统。\nb. Windows 11安装时破坏了Fedora的启动引导，我也懒得修复了，直接重装Fedora。Fedora重装后可以正常进入系统。\nc. 但是我在Fedora里试图用grub2-mkconfig命令重新生成GRUB配置时，发现它并没有自动检测到Windows 11的启动项。这时我才意识到，Windows 11的引导程序并没有将引导信息写入硬盘B，而是仍然试图将其写入硬盘A。而Fedora在安装时将它覆盖掉了！\nd. 行吧，刚才为了嫌修复Fedora的引导麻烦将其重装，但这下又要修复Windows的引导。但反正这活儿我也熟，于是开始修复：\n在Fedora系统里调整硬盘B的分区，在里面重新建立一个300M的FAT32分区，并将其命名为EFI。\n重新从U盘启动Windows 11的安装程序，在进入第一个安装界面时按Shift + F10，打开命令提示符。\n在命令提示符中输入以下命令：\n1 2 diskpart list disk 找到硬盘B的磁盘编号（例如Disk 1），然后输入以下命令，选择硬盘B：\n1 2 select disk 1 list partition 找到刚才新建的EFI分区的分区编号，以及硬盘B上安装Windows 11的那个分区的编号和盘符。可以根据分区的大小来判断，大小为300M的就是EFI分区，大小为400多G的就是Windows系统分区。发现EFI分区编号为4, Windows系统分区编号为3,且没有分配盘符。于是我们给EFI分区分配盘符为S，给Windows系统分区分配盘符为C。\n1 2 3 4 5 select partition 4 assign letter=S select partition 3 assign letter=C exit 这样我们就可以将Windows的启动引导程序从系统写入到EFI分区了\n1 bcdboot C:\\Windows /s S: /f UEFI 这样就完成了Windows的启动引导程序的修复。\ne. 修复完成后，重启电脑，进入BIOS设置，将硬盘B设置为第一启动项。这样就可以正常启动Windows 11了。但是我常用的是Fedora，所以我想把Windows的启动项加到Fedora的Grub里。然后把硬盘A设置为第一启动项，这样默认会进入Fedora，当我想启动Windows时，在Fedora的Grub界面选择Windows就行了。\nf. 于是进入Fedora系统，运行sudo grub2-mkconfig -o /boot/grub2/grub.cfg。然而，Fedora并没有检测到刚才我们修复的Windows启动项。运行sudo os-prober也没有任何输出，表示Fedora就是看不见这个Windows启动项。我百思不得其解，这么明显一EFI分区，就在磁盘B上，我也mount了，它为啥就是视而不见。\ng. 我发现这个EFI分区跟正常装系统时安装程序自己建的EFI分区有些不一样。具体来说，系统自己建的EFI分区和系统分区在其他系统的文件管理器里默认是显示为一整块硬盘的，但现在这个EFI分区跟同一硬盘上的系统分区却是分开的。我也不知道为啥我手动修复建立的分区就是如此不合群。当然，还有解决方法就是手动修改Grub配置，强行把Windows的启动项添加进去。但这样就不优雅了。\n于是，这次尝试又是以失败告终。\n最终解决方案\n我意识到Windows就是如此流氓，就是非得把引导分区建立在它所监测到的最先启动的硬盘上。所以要想老老实实让它把系统和启动引导都放在硬盘B上，只能把硬盘B挪到第一个M.2接口上，或者在安装Windows时把第一个M.2接口上的硬盘拆下来。\n总之，还是逃不过拆机。\n由于Fedora系统本身没问题，所以只需要把硬盘A拆下来。然后在硬盘B上安装Windows 11就行了。装好后再把硬盘A装回主板。\n启动进入Fedora系统，这次再运行os-prober和grub2-mkconfig就可以检测到Windows 11的启动项了。\n我是真的没想到在俩硬盘上装Windows/Linux双系统会有如此多的坑。这里的教训就是：\n如果要在双硬盘上安装双系统，最好把要装Windows的那个硬盘插到启动顺序靠前的那个M.2接口上。 如果你非要把要装Linux的那个硬盘插到启动顺序靠前的M.2接口上，那么要么在安装Windows时把第一个硬盘拆下来，要么用其他电脑在硬盘B上安装好Windows，再把硬盘B插到主板上。 ","date":"2024年5月24日","externalUrl":null,"permalink":"/p/%E5%90%AF%E5%8A%A8%E5%BC%95%E5%AF%BC%E4%B8%80%E5%A4%9A%E7%B3%BB%E7%BB%9F%E5%90%AF%E5%8A%A8%E9%A1%B9%E9%85%8D%E7%BD%AE/","section":"Posts","summary":" 缘起 # 我之前在电脑上装过双系统或者多系统，但对于多个系统的启动项一直云里雾里，经常出现启动项丢失、启动项重复等问题。这次我打算好好研究一下多系统启动项的配置，包括启动加载器的设置、EFI/Grub/rEFInd等，以及如何定制多系统启动器主题。\n","title":"启动引导（一）：多系统启动项配置","type":"post"},{"content":"","date":"8 五月 2024","externalUrl":null,"permalink":"/en/tags/domain-name/","section":"Tags","summary":"","title":"Domain Name","type":"tags"},{"content":"","date":"2024年5月8日","externalUrl":null,"permalink":"/tags/nginx/","section":"Tags","summary":"","title":"Nginx","type":"tags"},{"content":" 缘起 # 在决定用自己的电脑搭建一个私人服务器之后，我购买了一个域名，配置了动态域名解析，于是就可以通过域名来访问自己的服务器了。后来我又在这个服务器上搭建了不少网站，并使用Nginx配置了反向代理，以便从公网访问这些网站。这里我记录一下我是如何配置Nginx反向代理的。\n前置条件 # 运行Linux系统的电脑 (作为私人服务器) nginx (作为HTTP服务器) 私有域名 （详细操作请查看之前的帖子\u0026ldquo;个人网站的建立过程（一）：购买个人域名并配置动态域名解析\u0026rdquo;） 安装并配置nginx # 安装nginx # 安装nginx 1 sudo apt install nginx 开启nginx服务 1 sudo systemctl enable nginx Nginx配置文件 # 配置文件结构 # Nginx的配置文件一般在/etc/nginx目录下，目录结构一般如下：\n1 2 3 4 5 6 7 8 /etc/nginx ├── ... ├── nginx.conf ├── sites-available │ ├── default │ └── ... └── sites-enabled └── ... 其中nginx.conf是nginx的主配置文件，而sites-available目录下存放着所有的网站配置文件。每添加一个新的网站，我们可以把配置文件放在sites-available目录下。\n在nginx.conf文件中，默认会有如下配置：\n1 2 3 4 http { ... include /etc/nginx/sites-enabled/*; } 这表明nginx会自动读取sites-enabled目录下的所有配置文件。因此，我们可以在sites-available目录下创建新的配置文件后，可以在sites-enabled目录下创建一个软链接指向这个配置文件，这样nginx就会读取这个配置文件。\n配置文件示例 # 一般的静态网站\n一般的静态网站指的是只有静态文件的网站，如HTML、CSS、JS等文件。或者前后端分离的网站的前端部分。这种网站的配置文件一般如下：\n1 2 3 4 5 6 7 server { listen 80; server_name www.your.domain.name; root /home/YourUserName/Documents/www/website1/public; location / { } } 其中：\nlisten 80; 表示监听80端口，即HTTP协议的端口。 server_name www.your.domain.name; 表示将域名www.your.domain.name指向这个网站。 root /home/YourUserName/Documents/www/website1/public; 表示网站的根目录。 location / {} 表示所有的请求都会被转发到网站的根目录。 注意，这里我们将域名指向了~/Documents/www/website1目录下的public文件夹，而非整个website1目录。因为我们不想暴露一些私有文件。\n如果需要配置HTTPS，可以使用如下配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server{ listen 80; server_name www.your.domain.name; return 301 https://www.your.domain.name$request_uri; } server{ listen 443 ssl; ssl_certificate /home/YourUserName/Documents/www/website1/private/cert.pem; ssl_certificate_key /home/YourUserName/Documents/www/website1/private/key.pem; root /home/YourUserName/Documents/www/website1/public; server_name www.your.domain.name; location /{ } } 其中ssl_certificate和ssl_certificate_key分别是SSL证书和私钥的路径。\n一般的动态网站\n一般的动态网站指的是有后端代码的网站，如PHP、Python、Node.js等，或者在Docker容器中托管的网站。这种网站一般会通过某个特定的端口来访问，如localhost:3000。这种网站的配置文件一般如下：\n1 2 3 4 5 6 7 server { listen 80; server_name www.your.domain.name; location / { proxy_pass http://localhost:3000; } } 其中：\nproxy_pass http://localhost:3000; 表示将所有的请求转发到localhost:3000，即后端服务的地址。 如果需要配置HTTPS，可以使用如下配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 server{ listen 80; server_name www.your.domain.name; return 301 https://www.your.domain.name$request_uri; } server{ listen 443 ssl http2; listen [::]:443 ssl http2; server_name www.your.domain.name; ssl_certificate /home/YourUserName/Documents/www/website1/private/cert.pem; ssl_certificate_key /home/YourUserName/Documents/www/website1/private/key.pem; location /{ proxy_pass http://localhost:3000; } } 配置文件的启用 # 在sites-available目录下按照上述的配置文件示例创建一个新的配置文件，如website1.conf。\n检查配置文件是否有语法错误\n1 sudo nginx -t 如果没有错误，会显示nginx: configuration file /etc/nginx/nginx.conf test is successful。\n在sites-enabled目录下创建一个软链接指向这个配置文件。\n1 sudo ln -s /etc/nginx/sites-available/website1.conf /etc/nginx/sites-enabled/website1.conf 重启nginx服务\n1 sudo service nginx reload 可以通过下面的命令查看nginx的状态：\n1 sudo nginx status 通过浏览器访问www.your.domain.name，即可看到网站的内容。\n负载均衡 # 如果我们有多个后端服务，我们可以使用Nginx的负载均衡功能。负载均衡可以将请求分发到多个后端服务上，以提高性能和可靠性。由于我暂时只有一台服务器，这里暂时不做详细介绍。等有钱买了多台服务器后再来更新这部分内容。\n","date":"2024年5月8日","externalUrl":null,"permalink":"/p/%E4%BB%8E%E5%85%AC%E7%BD%91%E8%AE%BF%E9%97%AE%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99nginx%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E9%85%8D%E7%BD%AE/","section":"Posts","summary":" 缘起 # 在决定用自己的电脑搭建一个私人服务器之后，我购买了一个域名，配置了动态域名解析，于是就可以通过域名来访问自己的服务器了。后来我又在这个服务器上搭建了不少网站，并使用Nginx配置了反向代理，以便从公网访问这些网站。这里我记录一下我是如何配置Nginx反向代理的。\n","title":"从公网访问个人网站——Nginx反向代理配置","type":"post"},{"content":"","date":"2024年4月24日","externalUrl":null,"permalink":"/tags/latex/","section":"Tags","summary":"","title":"LaTeX","type":"tags"},{"content":"","date":"2024年4月24日","externalUrl":null,"permalink":"/tags/overleaf/","section":"Tags","summary":"","title":"Overleaf","type":"tags"},{"content":"","date":"2024年4月24日","externalUrl":null,"permalink":"/tags/self-host/","section":"Tags","summary":"","title":"Self-Host","type":"tags"},{"content":" 缘起 # 之前在和别人合作写作业或者论文时，一直使用Overleaf来协作编写LaTeX文档。虽然不如在本地编辑器（如VS Code）上可定制性强，但跟别人协作起来还是挺方便的。在写博士论文时，为了使用Git同步功能，我购买了一年的学生订阅。由于毕业后使用频率降低，我就没有续费了。最近我需要重新编译一下之前的博士论文，才发现Overleaf免费用户的编译时间是有限制的，由于博士论文比较大，编译时间超过了限制，导致我无法编译成功。\n我之前也尝试过使用Overleaf的开源社区版搭建在线LaTeX服务，但搭好后还是使用Overleaf的官方服务较多，就没有维护自己搭建的服务。这次由于需要重新编译博士论文，我就试着在自己搭建的Overleaf上编译，但发现缺少了很多宏包，导致无法编译。于是我决定重新搭建一个Overleaf开源社区版服务，并安装完整版的TeX Live，以便能够编译所有的LaTeX文档。\nOverleaf简介 # Overleaf是一个在线的LaTeX编辑器，支持多人协作编辑，提供了丰富的模板和宏包，可以在线编译LaTeX文档。Overleaf有免费版和付费版，免费版有一些限制，如编译时间限制、项目数限制等。Overleaf的付费版提供了更多的功能，如无限编译时间、无限项目数、Git同步等。\nOverleaf的开源社区版是Overleaf的开源版本，可以在自己的服务器上搭建一个类似Overleaf的在线LaTeX编辑器。Overleaf开源社区版提供了与Overleaf官方服务相似的功能，如在线编译LaTeX文档、多人协作编辑、模板和宏包支持等。但是遗憾的是，Overleaf社区版也不支持集成Git同步功能。如果想要使用Git同步以及一些其他功能，需要购买Overleaf Server Pro版。这里我只需要Overleaf社区版就够了。\n前提 # Docker和Docker Compose 域名（二级域名即可） Overleaf社区版安装 # 我们可以使用Docker来方便快捷地搭建Overleaf社区版服务。这里有两种安装方式：一种是直接使用Overleaf社区版仓库里提供的docker-compose.yml文件，另一种是使用Overleaf官方提供的工具箱。Overleaf官方推荐使用第二种方式，但这里两种方式都介绍一下。\n方法一：使用docker-compose.yml文件安装 # 可以在Overleaf社区版仓库中找到docker-compose.yml文件，下载到本地，然后使用Docker Compose启动服务。\n这里可能需要根据自己的实际情况修改docker-compose.yml文件，如修改端口、数据卷路径等。修改完成后，使用以下命令启动服务：\n1 docker-compose up -d 参考：Overleaf社区版快速开始指南。\n方法二：使用Overleaf工具箱安装 # Overleaf官方提供了一个工具箱，包装了一些常用的docker命令，可以初始化、启动、停止、诊断、升级Overleaf服务。虽然我觉得相比直接使用docker-compose.yml，这个工具箱使部署docker的流程更复杂了，但这个工具箱的确提供了更多更灵活的定制选项。\n使用这个工具箱部署Overleaf社区版服务，可以参考Overleaf社区版工具箱文档。简单来讲，有以下几步：\n下载工具箱：\n1 git clone https://github.com/overleaf/toolkit.git 进入工具箱目录：\n1 cd toolkit 初始化安装配置：\n1 bin/init 运行此命令，将在当前目录下生成一个config文件夹，里面包含了三个配置文件：\noverleaf.rc：Overleaf配置文件。用户可以在这个文件中配置Overleaf的一些参数，如端口、数据卷路径等。 variables.env：环境变量配置文件 version：选择Overleaf版本。注意在5.0.0版本之后，Overleaf将原来的ShareLaTeX商标替换为Overleaf商标，所以如果环境变量配置文件中如果使用OVERLEAF前缀的变量，需要选择5.0.0之后的版本。 在修改完配置文件后，运行以下命令启动服务（可能需要sudo提升权限）：\n1 bin/up 这个命令会以命令行debug模式启动服务，在运行后可以看到服务的输出日志。如果想要停止服务，可以按Ctrl+C。\n如果想要以后台模式启动服务，可以使用以下命令（可能需要sudo提升权限）：\n1 bin/up -d 如果想要停止服务，可以使用以下命令（可能需要sudo提升权限）：\n1 bin/stop 配置Nginx反向代理 # Overleaf社区版的docker-compose.yml文件中有一个nginx容器，如果使用Overleaf工具箱，在overleaf.rc配置文件中也可以配置Nginx。如果需要TSL/SSL加密，可以使用在初始化时使用bin/init --tls命令。这样会在config目录中生成一个nginx文件夹，里面包含了Nginx的配置文件和作为示例的SSL证书。\n但是由于我之前在服务器上已经部署了单独的Nginx服务来管理所有的网站，所以我选择使用单独的Nginx服务来反向代理Overleaf服务。具体过程可参见个人网站的建立过程（二）：使用Hugo框架搭建个人网站。\n升级TeX Live # Overleaf的docker镜像中自带了一个基础版本的TeX Live，但是这个版本可能不包含所有的宏包。如果需要编译一些特殊的LaTeX文档，可能需要安装完整版的TeX Live。可参考Overleaf工具箱文档中升级Tex Live的文档。主要步骤如下：\n安装完整版TeX Live # 使用如下命令进入Overleaf容器：\n1 docker exec -it sharelatex bash 查看当前TeX Live版本：\n1 tlmgr --version 更新TeX Live：\n1 tlmgr install scheme-full 我在执行这个命令时遇到了如下问题：\n1 2 3 4 tlmgr: Local TeX Live (2023) is older than remote repository (2024). Cross release updates are only supported with update-tlmgr-latest(.sh/.exe) --update See https://tug.org/texlive/upgrade.html for details. 这是因为TeX Live的版本不同，可以使用如下命令为Tex Live指定合适的版本：\n1 tlmgr option repository https://ftp.math.utah.edu/pub/tex/historic/systems/texlive/2023/tlnet-final 关于TeX Live的历史版本仓库，可以参考这里。\n在指定了合适的版本后，再次执行更新命令：\n1 tlmgr install scheme-full 不出意外的话，这个命令会下载并安装完整版的TeX Live。这将会安装超过4000个宏包，可能需要一段时间。\n添加路径：\n在安装完成后，需要使用如下命令添加路径：\n1 tlmgr path add 如果不执行这个命令，在Overleaf中编译LaTeX文档时，可能会出现无法编译EPS图片等问题。\n保存更改到镜像 # 做完上述更改后，升级的TeX Live只会保存在当前的容器中，如果容器被删除，这些更改也会丢失。如果想要将这些更改保存到镜像中并在之后创建容器时使用，可以执行下面的操作：\n使用如下命令（可能需要sudo提升权限）更新镜像：\n1 docker commit sharelatex sharelatex/sharelatex:with-texlive-full 然后可以在config目录中添加一个docker-compose.override.yml文件，在之后创建容器时指定使用新的镜像：\n1 2 3 4 5 --- version: \u0026#39;2.2\u0026#39; services: sharelatex: image: sharelatex/sharelatex:with-texlive-full 停止并删除之前的容器，然后重新创建容器（可能需要sudo提升权限）：\n1 2 3 bin/stop bin/docker-compose rm -f sharelatex bin/up -d ","date":"2024年4月24日","externalUrl":null,"permalink":"/p/%E8%87%AA%E5%BB%BA%E5%9C%A8%E7%BA%BFlatex%E7%BC%96%E8%AF%91%E9%A2%84%E8%A7%88%E6%9C%8D%E5%8A%A1overleaf%E5%BC%80%E6%BA%90%E7%A4%BE%E5%8C%BA%E7%89%88/","section":"Posts","summary":" 缘起 # 之前在和别人合作写作业或者论文时，一直使用Overleaf来协作编写LaTeX文档。虽然不如在本地编辑器（如VS Code）上可定制性强，但跟别人协作起来还是挺方便的。在写博士论文时，为了使用Git同步功能，我购买了一年的学生订阅。由于毕业后使用频率降低，我就没有续费了。最近我需要重新编译一下之前的博士论文，才发现Overleaf免费用户的编译时间是有限制的，由于博士论文比较大，编译时间超过了限制，导致我无法编译成功。\n","title":"自建在线LaTeX编译预览服务：Overleaf开源社区版","type":"post"},{"content":"","date":"2024年2月12日","externalUrl":null,"permalink":"/tags/development/","section":"Tags","summary":"","title":"Development","type":"tags"},{"content":"","date":"2024年2月12日","externalUrl":null,"permalink":"/tags/git/","section":"Tags","summary":"","title":"Git","type":"tags"},{"content":"","date":"2024年2月12日","externalUrl":null,"permalink":"/tags/github/","section":"Tags","summary":"","title":"GitHub","type":"tags"},{"content":" 背景 # 如果在GitHub上看到了一个有趣的项目，想要参与开发，我们一般需要遵照一定的流程。这里记录一下一般的项目合作开发流程。\n当然这里的流程不仅适用于GitHub，也适用于其他的代码托管平台，如GitLab等。\n前置条件 # 已经安装了Git 已经注册了GitHub账号 Git设置 # 设置用户名和邮箱\n设置用户名和邮箱有两种模式。\n如果你只用一个GitHub账号，可以设置全局用户名和邮箱。\n1 2 git config --global user.name \u0026#34;Your Name\u0026#34; git config --global user.email \u0026#34;Your Email\u0026#34; 如果你有多个GitHub账号，可以给每个Git仓库分别设置用户名和邮箱。\n1 2 3 cd /path/to/your/repo git config user.name \u0026#34;Your Name\u0026#34; git config user.email \u0026#34;Your Email\u0026#34; 登录凭证管理\n在上一步设置好用户名和邮箱后，在克隆仓库以及每次git push时，都需要输入GitHub的用户名和密码，这样很麻烦。可以使用凭证管理器来保存用户名和密码。\nWindows\n如果你安装的Git版本号高于2.29，那么Git中已经集成了对GitHub OAuth的支持。当你第一次通过HTTPS方式克隆一个GitHub私有仓库时，Git会提示你使用浏览器登录GitHub，并授权Git访问你的GitHub账号。之后，Git会自动保存你的GitHub凭证，不需要再次输入用户名和密码。\nmacOS\n方法一（旧方法）：\n你需要安装Git Credential Manager。可以在终端中运行以下命令：\n1 brew install --cask git-credential-manager 安装完成后，当你第一次通过HTTPS方式克隆一个GitHub私有仓库时，Git会提示你使用浏览器登录GitHub，并授权Git访问你的GitHub账号。之后，Git会自动保存你的GitHub凭证，不需要再次输入用户名和密码。\nGit Credential Manager支持各种Git托管平台，包括GitHub、GitLab、Bitbucket等的账号都可以使用这个工具进行凭证管理。\n方法二（推荐方法）：\n对于GitHub账号，可以使用GitHub CLI工具进行凭证管理。可以在终端中运行以下命令安装GitHub CLI工具：\n1 brew install gh 安装完成后，运行以下命令登录GitHub账号：\n1 gh auth login 按照提示选择通过浏览器登录GitHub账号。登录成功后，GitHub CLI工具会自动配置Git的凭证管理，不需要再次输入用户名和密码。\nLinux\nLinux系统和macOS类似，可以选用Git Credential Manager或者GitHub CLI工具进行凭证管理。使用你所用的Linux发行版的包管理器安装即可。\nGitHub合作开发流程 # 总览 # 要参与别人的项目开发，其总体思路是：\n在GitHub网页上将项目fork到自己的GitHub账号下。 在本地将fork过来的项目克隆到本地，并将原始仓库添加为远程仓库。这样： 我们在本地的仓库对应两个远程仓库：一个是Fork仓库（origin），另一个是原始仓库（upstream）。 不要在GitHub上动origin远程仓库的main分支，也不要在本地的main分支上进行开发。时刻注意本地的main分支、origin远程仓库的main分支和upstream远程仓库的main分支要保持同步。 想要添加新功能或者修复bug时，先从main分支上创建一个新的分支，在这个新的分支上进行开发。 在本地开发完成后，将本地的新分支推送到origin远程仓库，GitHub会提醒你x commits ahead of upstream/main, y commits behind upstream/main，其中x表示你在新分支上的提交数，y表示你在开发新功能/修复bug的过程中，upstream远程仓库的main分支上的新提交数。 如果y不为0，说明upstream远程仓库的main分支有新的提交，这时你需要将你的新分支与upstream远程仓库的main分支进行同步。 先将upstream远程仓库的main分支的最新代码拉取到本地的main分支。 然后将本地的main分支合并到你的新分支。这个过程有两种情况： 如果没有冲突，直接merge或rebase即可。 如果有冲突，你需要解决这些冲突，然后再提交。 如果你想将你的新功能/bug修复合并到原始仓库中，你需要在GitHub上创建一个Pull Request，等待原始仓库的作者审核你的Pull Request，如果有需要，你需要根据原始仓库的作者的反馈进行修改。 下面我们来详细介绍一下这个流程。\nGitHub网页上的操作 —— Fork # 打开你想要参与开发的项目的GitHub页面，点击右上角的Fork按钮，将项目fork到自己的GitHub账号下。 我们把原项目称为原始仓库，把自己fork过来的项目称为Fork仓库。\n在自己的GitHub账号下，找到fork过来的项目（即Fork仓库），点击Clone or download按钮，复制项目的URL。\n本地操作 —— Clone # 在本地的终端中，切换到你想要存放项目的目录，运行以下命令，将项目克隆到本地。\n1 git clone https://github.com/your-username/project-name.git 这里的your-username是你的GitHub用户名，project-name是项目的名字。\n进入项目目录，运行以下命令，将原始仓库添加为远程仓库。\n1 git remote add upstream https://github.com/authors-usename/project-name.git 这里的authors-username是原始仓库的作者的GitHub用户名，project-name是项目的名字。\n然后运行以下命令，查看远程仓库的情况。\n1 git remote -v 如果一切正常，你会看到类似以下的输出：\n1 2 3 4 origin https://github.com/your-username/project-name.git (fetch) origin https://github.com/your-username/project-name.git (push) upstream https://github.com/authors-username/project-name.git (fetch) upstream https://github.com/authors-username/project-name.git (push) 这里的origin是你的Fork仓库，upstream是原始仓库。\n本地开发 # 在本地的终端中，切换到项目目录，运行以下命令，创建一个新的分支。\n1 git checkout -b new-branch-name 这里的new-branch-name是你的新分支的名字，它可以是你要添加的新功能的名字，也可以是你要修复的bug的名字。\n在本地的终端中，进行开发，修改代码/添加新功能/修复bug。然后运行以下命令，将修改的文件添加到暂存区以及提交到本地仓库。\n1 2 git add . git commit -m \u0026#34;Your commit message\u0026#34; 也可以把本地仓库的修改推送到Fork仓库。\n1 git push origin new-branch-name 重复第2步的操作，直到你的新功能/bug修复完成。\n在GitHub上打开Fork仓库，GitHub会提醒你x commits ahead of upstream/main, y commits behind upstream/main，其中x表示你在新分支上的提交数，y表示你在开发新功能/修复bug的过程中，upstream远程仓库的main分支有新的提交数。如果y不为0，说明upstream远程仓库的main分支有新的提交，这时你需要将你的新分支与upstream远程仓库的main分支进行同步。\n1 2 git checkout main git pull upstream main 然后可以在本地合并main分支的最新代码到自己的new-branch-name分支。这里的“合并”有两种方式：merge和rebase:\nmerge：将main分支的最新代码合并到new-branch-name分支，new-branch-name分支的提交历史保持不变，形成一个新的提交历史。\n1 2 git checkout new-branch-name git merge main rebase：会将new-branch-name分支的提交历史打包，放在最新的main分支的提交历史之后，形成一个新的提交历史。\n1 2 git checkout new-branch-name git rebase main 无论是merge还是rebase，如果原始仓库的main分支改动了你在new-branch-name分支上改动的文件，那么就会产生冲突。你需要解决这些冲突。 解决冲突后，运行以下命令，将修改的文件添加到暂存区以及提交到本地仓库。\n1 2 git add . git commit -m \u0026#34;Your commit message\u0026#34; 最后再次运行以下命令，将修改的文件推送到Fork仓库。\n1 git push origin new-branch-name 这样，你的new-branch-name分支向原始仓库的main分支合并时就不会产生冲突了。\nGitHub网页上的操作 —— Pull Request # 在创建Pull Request之前，先在GitHub上切换到你的new-branch-name分支，确认GitHub上显示的This branch is x commits ahead of upstream/main, y commits behind upstream/main中的y为0（即只显示This branch is x commits ahead of upstream/main，不显示behind），说明你的new-branch-name分支已经和原始仓库的main分支保持了同步。如果y不为0，那么返回到上一步的第4步，先将你的new-branch-name分支与原始仓库的main分支进行同步。\n打开你的Fork仓库的GitHub页面，点击New pull request按钮，创建一个新的Pull Request。注意如果原始仓库提供了Pull Request模板，你需要按照原始仓库的Pull Request模板填写Pull Request的标题和内容。\n等待原始仓库的作者审核你的Pull Request，如果有需要，你需要根据原始仓库的作者的反馈进行修改。\n如果原始仓库的作者接受了你的Pull Request，恭喜你，你的代码将会被合并到原始仓库中。\n本地操作 —— 更新本地仓库 # 在本地的终端中，切换到main分支，运行以下命令，将原始仓库的main分支的最新代码拉取到本地。\n1 2 git checkout main git pull upstream main 你也可以先使用git fetch upstream main命令，查看原始仓库的main分支的最新代码，然后再使用git merge upstream/main命令，将原始仓库的main分支的最新代码合并到本地的main分支。\n1 2 3 git fetch upstream main git checkout main git merge upstream/main 这样，你本地的main分支就和原始仓库的main分支保持了同步。\n然后你可以把本地的main分支推送到Fork仓库。\n1 git push origin main （可选）如果你的new-branch-name分支已经被合并到原始仓库的main分支，你可以删除new-branch-name分支。\n1 git branch -d new-branch-name 你也可以删除new-branch-name分支的远程分支。\n1 git push origin --delete new-branch-name 这样，你就完成了在Github上和别人合作添加一个新功能/修复一个bug的工作。\n继续开发新功能/修复新bug # 如果需要继续添加新功能/修复新bug，可以先将自己Fork仓库的main分支与原始仓库的main分支同步，确保你从项目最新的版本开始工作：\n```bash git checkout main git pull upstream main git push origin main ``` 然后，你可以在main分支上创建一个新的分支，继续开发新功能/修复新bug。\n```bash git checkout -b another-new-branch-name ``` 后面的操作就和本地开发之后的一样了。\n","date":"2024年2月12日","externalUrl":null,"permalink":"/p/github/gitlab%E4%B8%8A%E7%9A%84%E9%A1%B9%E7%9B%AE%E5%90%88%E4%BD%9C%E5%BC%80%E5%8F%91%E6%B5%81%E7%A8%8B/","section":"Posts","summary":" 背景 # 如果在GitHub上看到了一个有趣的项目，想要参与开发，我们一般需要遵照一定的流程。这里记录一下一般的项目合作开发流程。\n","title":"GitHub/GitLab上的项目合作开发流程","type":"post"},{"content":"","date":"2024年2月12日","externalUrl":null,"permalink":"/tags/gitlab/","section":"Tags","summary":"","title":"GitLab","type":"tags"},{"content":"","date":"2024年1月27日","externalUrl":null,"permalink":"/tags/rnn/","section":"Tags","summary":"","title":"RNN","type":"tags"},{"content":" 背景 # 这是“实例学PyTorch”系列的第4篇文章。在前三篇文章中\n“实例学PyTorch（1）：MNIST手写数字识别（一）——PyTorch基础和神经网络基础” “实例学PyTorch（2）：MNIST手写数字识别（二）——神经网络中的参数选择” “实例学PyTorch（3）：MNIST手写数字识别（三）——卷积神经网络（CNN）” 我们介绍了用PyTorch解决图片分类的问题。机器学习领域还有一个非常重要的问题是序列预测，与图片分类不同，序列预测需要考虑数据的前后关联性。能够处理数据的前后关联性的神经网络是循环神经网络（Recurrent Neural Network，RNN）。\nPyTorch官方的示例代码https://github.com/pytorch/examples/中有一个使用长短期记忆网络（Long Short-Term Memory，LSTM）实现对sin函数预测的示例，我们暂时先不考虑LSTM，而是使用一个简单的循环神经网络（Recurrent Neural Network，RNN）来实现对正弦函数的预测。\n本文的代码可以在我的GitHub仓库https://github.com/jin-li/pytorch-tutorial中的T04_series_rnn文件夹中找到。\n循环神经网络（RNN）简介 # 在之前的文章中，我们介绍了全连接神经网络和卷积神经网络，这两种神经网络都是前馈神经网络（Feedforward Neural Network）。前馈神经网络的一个缺点是不能处理序列数据，因为它没有存储之前的数据信息。循环神经网络（Recurrent Neural Network，RNN）是一种可以处理序列数据的神经网络，它在每个时间步都会保存之前的数据信息，从而可以处理序列数据。需要注意的是，这里的序列数据不一定是时间序列数据，也可以是空间序列数据。例如，自然语言处理中的句子就是一个空间序列数据，声音识别中的音频数据属于序列数据。这里为了方便，不管序列数据是不是时间序列，我们都将其中的每个元素称为时间步（time step）。\n循环神经网络的结构并不复杂，它的核心有两点：\n一是在训练时，需要将训练数据按照时间步展开，然后通过遍历每个时间步来计算损失函数，最后通过反向传播算法来更新权重。 二是对于每个时间步，不止有一个输出，还有一个隐藏状态（hidden state），这个隐藏状态会在下一个时间步被传递到下一个时间步，从而保留了之前的数据信息。 循环神经网络的具体知识可以参考斯坦福大学的CS230课程课件，这里我们用一个动图来简单说明循环神经网络的工作原理：\n这里的$x$是输入，$h$是隐藏状态，$y$是输出，$t$是时间步。可以看到，隐藏状态$h$在每个时间步都会被传递到下一个时间步，从而保留了之前的数据信息。$x$, $h$和$y$各为神经网络的一个层，$h$是隐藏层，$x$是输入层，$y$是输出层，各个层的大小可以根据具体问题来确定。\n正弦函数预测 # 正弦函数可以看作一个时间序列，在某些时刻，正弦函数的值也许是相同的，但其后的值可能不同。例如对于$y = \\sin(x)$，当$x = 0$和$x = \\pi$时，$y$都等于$0$，但在这两个时刻的后一个时间步（假设时间步的大小为0.01，则后两个时间步为$x = 0.01$和$x = \\pi + 0.01$）时，$y$就不相同了。要想预测后一个时间步的值，我们不仅需要知道当前时间步的值，还要知道之前几个时间步的值。这正是循环神经网络的用途所在。\nRNN模型设计 # 我们这个问题比较简单，只需要用一个循环神经网络就可以了。我们的循环神经网络的输入是一个序列，输出是这个序列的下一个值。我们的输入序列是正弦函数的值，输出是正弦函数的下一个值。我们的循环神经网络的结构如下：\n输入层：输入层的大小为1，即每个时间步只有一个输入。 隐藏层：隐藏层的大小可以随意选择。考虑到我们这个问题比较简单，我们选择10个神经元作为隐藏层。 输出层：输出层的大小为1，即每个时间步只有一个输出。 这样我们的循环神经网络的结构就确定了。我们可以使用PyTorch的nn.RNN类来实现这个循环神经网络：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class SimpleRNN(nn.Module): def __init__(self, rnn_type, input_size, hidden_size, num_layers): super(SimpleRNN, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers self.rnn = nn.RNN(input_size=input_size, hidden_size=hidden_size, dropout=(0 if num_layers == 1 else 0.05), num_layers=num_layers, batch_first=True) self.out = nn.Linear(hidden_size, 1) # Linear layer is output of model def forward(self, x, h_state): # Define our forward pass, we take some input sequence and an initial hidden state. r_out, h_state = self.rnn(x, h_state) final_y = self.out(r_out[:, -1, :]) # Return only the last output of RNN. return final_y, h_state 这里我们定义了一个SimpleRNN类，它继承自PyTorch的nn.Module类。在__init__函数中，我们定义了一个nn.RNN类的实例，这个实例就是我们的循环神经网络。在forward函数中，我们定义了循环神经网络的前向传播过程，即我们如何计算输出。这里我们只需要返回最后一个时间步的输出即可。\n数据准备 # 在本系列之前的三篇文章中，我们在训练神经网络时都是使用的别人已经准备好的数据集。但这里，我们需要自己来准备数据。对于本问题，数据集的准备还是非常简单的，我们只需要生成一些正弦函数的数据即可。注意，在生成了数据之后，我们需要将其封装成PyTorch的Dataset类，这样才能方便地使用PyTorch的DataLoader类来加载数据。\n生成数据集的完整代码在本文对应的GitHub仓库https://github.com/jin-li/pytorch-tutorial中的T04_series_rnn文件夹中的SineWaveDataset.py文件中，具体代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import numpy as np import torch from torch.utils.data import Dataset class RNNDataset(Dataset): def __init__(self, x, y=None): self.data = x self.labels = y def __len__(self): return self.data.shape[0] def __getitem__(self, idx): if self.labels is not None: return self.data[idx], self.labels[idx] else: return self.data[idx] def create_dataset(sequence_length, train_percent=0.8): # Create sin wave at discrete time steps. num_time_steps = 2000 time_steps = np.linspace(start=0, stop=1000, num=num_time_steps, dtype=np.float32) discrete_sin_wave = (np.sin(time_steps * 2 * np.pi / 20)).reshape(-1, 1) # Take (sequence_length + 1) elements \u0026amp; put as a row in sequence_data, extra element is value we want to predict. # Move one time step and keep grabbing till we reach the end of our sampled sin wave. sequence_data = [] for i in range(num_time_steps - sequence_length): sequence_data.append(discrete_sin_wave[i: i + sequence_length + 1, 0]) sequence_data = np.array(sequence_data) # Split for train/val. num_total_samples = sequence_data.shape[0] num_train_samples = int(train_percent * num_total_samples) train_set = sequence_data[:num_train_samples, :] test_set = sequence_data[num_train_samples:, :] print(\u0026#39;{} total sequence samples, {} used for training\u0026#39;.format(num_total_samples, num_train_samples)) # Take off the last element of each row and this will be our target value to predict. x_train = train_set[:, :-1][:, :, np.newaxis] y_train = train_set[:, -1][:, np.newaxis] x_test = test_set[:, :-1][:, :, np.newaxis] y_test = test_set[:, -1][:, np.newaxis] train_data = RNNDataset(x_train, y_train) test_data = RNNDataset(x_test, y_test) torch.save(train_data, \u0026#39;train_data.pt\u0026#39;) torch.save(test_data, \u0026#39;test_data.pt\u0026#39;) if __name__ == \u0026#39;__main__\u0026#39;: create_dataset(sequence_length=80) 首先我们定义了一个RNNDataset类来存放训练数据，它继承自PyTorch的Dataset类，这样我们就可以使用PyTorch的DataLoader类来加载数据。 然后我们定义了一个create_dataset函数来生成正弦函数的数据。这个函数的输入参数有两个，一个是sequence_length，表示我们要用多少个时间步的数据来预测下一个时间步的数据；另一个是train_percent，表示我们将多少比例的数据用于训练，剩下的用于测试。这个函数的主要工作是生成正弦函数的数据，并将其封装成RNNDataset类的实例，最后将训练数据和测试数据保存到文件中。 在create_dataset函数中，我们首先生成了2000个时间步的正弦函数数据，然后我们从这些正弦数据中生成一些序列数据用于训练。生成训练数据的方法是： 从第1个时间步开始，取连续50个时间步的数据作为一个序列，即$x_1, x_2, \\cdots, x_{80}$。 这50个时间步的下一个时间步即为要预测的值$y = x_{81}$。 重复上述过程，直到取完所有的时间步。这里我们有2000个时间步，所以我们一共可以生成$2000 - 80 = 1920$个序列。 我们将这1950个序列数据分为两部分，其中80%用于训练（$1920 \\times 0.8 = 1536$，20%用于测试。 最后我们将训练数据和测试数据封装成RNNDataset类的实例，并保存到文件train_data.pt和test_data.pt中。 x_train是一个三维张量，其形状为$1536 \\times 80 \\times 1$，其中$1536$是训练数据的个数，$80$是一个序列的长度，1是输入的维度。 y_train是一个二维张量，其形状为$1536 \\times 1$，其中$1950$是训练数据的个数，1是输出的维度。 y_train是x_train中每组训练数据的最后一个时间步之后那个时间步的值。 模型训练 # 和之前的文章一样，我们需要定义一个训练函数来训练我们的模型。这个训练函数的代码也很简单：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def train(model, device, train_dataloader, loss_function, optimizer, epoch_idx, log_interval): model.train() trained_cnt = 0 for batch_idx, (x_batch, y_batch) in enumerate(train_dataloader): x_batch, y_batch = x_batch.to(device), y_batch.to(device) h_state = torch.zeros([model.num_layers, x_batch.size()[0], model.hidden_size]).to(device) optimizer.zero_grad() output, _ = model(x_batch, h_state) loss = loss_function(output, y_batch) loss.backward() optimizer.step() trained_cnt += len(x_batch) if batch_idx % log_interval == 0: print(\u0026#39;Train Epoch: {:5d} [{:5d} / {:5d} ({:3.0f}%)]\\tLoss: {:.3e}\u0026#39;.format( epoch_idx, trained_cnt, len(train_dataloader.dataset), 100. * (batch_idx + 1) / len(train_dataloader), loss.item())) 这个训练函数的输入参数也和之前的训练函数类似，分别是模型、训练数据加载器、损失函数和优化器，这里不再赘述。\n模型测试 # 除了训练函数，我们再定义一个测试函数，在训练完一个epoch之后，我们需要测试我们的模型在测试数据上的表现。测试函数的代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 def test(model, device, test_dataloader, loss_function): model.eval() loss_all = [] for x_batch, y_batch in test_dataloader: x_batch, y_batch = x_batch.to(device), y_batch.to(device) h_state = torch.zeros([model.num_layers, x_batch.size()[0], model.hidden_size]).to(device) output, _ = model(x_batch, h_state) loss = loss_function(output, y_batch) loss_all.append(loss.cpu().data.numpy()) print(\u0026#39;Test loss: \u0026#39;, np.mean(loss_all)) return np.mean(loss_all) 模型预测 # 最后，我们定义一个预测函数，用于测试我们的模型能否根据一个正弦函数的序列数据预测出之后一段的值。预测函数的代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def predict(model, device, dataloader, prediction_steps): model.eval() h_state = torch.zeros([model.num_layers, 1, model.hidden_size]).to(device) # Adjusted to 3-D with batch size 1 initial_input = next(iter(dataloader))[1].to(device) # Grab one initial sequence of data for use in prediction. if initial_input.dim() == 2: initial_input = initial_input.unsqueeze(0) initial = initial_input.squeeze().cpu().numpy().tolist() predictions = [] for _ in range(prediction_steps): # Predict prediction_steps steps ahead pred, h_state = model(initial_input, h_state) predictions.append(pred.item()) initial_input = pred.unsqueeze(0) # Ensure pred has the same dimensions as test_input[:, 1:, :] return initial, predictions 运行模型 # 我们把上述代码整合到一个文件中，在main()函数中调用上述函数。和之前类似地，我们添加一些命令行参数来控制模型的训练和测试。完整的代码参考我的GitHub仓库https://github.com/jin-li/pytorch-tutorial中T04_series_rnn文件夹里的time_series_rnn.py。\n首先我们生成正弦函数的数据集：\n1 python SineWaveDataset.py 然后我们训练模型，用模型来预测一个正弦函数的序列，并把预测结果和真实结果画在一起：\n1 python time_series_rnn.py --plot 这个代码在GPU上运行了约20秒，显存占用约208M；如果使用CPU，则运行时间增加到约1分30秒。运行完这个命令之后，我们可以看到模型预测的结果如下图：\n其中前80个数据点是一个初始序列，后面的150个数据点是模型预测的结果。可以看到，模型的预测结果和真实结果非常接近。\n总结 # 在本文中，我们介绍了如何使用PyTorch实现一个简单的循环神经网络（RNN）来预测正弦函数的序列。\n本文中的例子我使用不同的随机数种子运行了多次，在给定的参数下，上面展示的结果是其中预测得比较好的一次，随机数种子是18（在GitHub上的代码中已将其设为默认值）。读者可以根据我们在本系列的第二篇文章“实例学PyTorch（2）：MNIST手写数字识别（二）——神经网络中的参数选择”中介绍的参数选择方法来调整模型的参数，看看能否得到更好的结果。\n有了本文这个简单的例子作为基础，我们将在下一篇文章中介绍如何使用其他的神经网络，例如长短期记忆网络（Long Short-Term Memory，LSTM）、门控循环单元（Gated Recurrent Unit，GRU）等来预测正弦函数的序列。\n","date":"2024年1月27日","externalUrl":null,"permalink":"/p/%E5%AE%9E%E4%BE%8B%E5%AD%A6pytorch4%E5%BA%8F%E5%88%97%E9%A2%84%E6%B5%8B%E4%B8%80%E5%BE%AA%E7%8E%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9Crnn/","section":"Posts","summary":" 背景 # 这是“实例学PyTorch”系列的第4篇文章。在前三篇文章中\n“实例学PyTorch（1）：MNIST手写数字识别（一）——PyTorch基础和神经网络基础” “实例学PyTorch（2）：MNIST手写数字识别（二）——神经网络中的参数选择” “实例学PyTorch（3）：MNIST手写数字识别（三）——卷积神经网络（CNN）” 我们介绍了用PyTorch解决图片分类的问题。机器学习领域还有一个非常重要的问题是序列预测，与图片分类不同，序列预测需要考虑数据的前后关联性。能够处理数据的前后关联性的神经网络是循环神经网络（Recurrent Neural Network，RNN）。\n","title":"实例学PyTorch（4）：序列预测（一）——循环神经网络（RNN）","type":"post"},{"content":"","date":"2024年1月23日","externalUrl":null,"permalink":"/tags/cnn/","section":"Tags","summary":"","title":"CNN","type":"tags"},{"content":"","date":"23 一月 2024","externalUrl":null,"permalink":"/en/tags/convolutional-neural-networks/","section":"Tags","summary":"","title":"Convolutional Neural Networks","type":"tags"},{"content":"","date":"23 一月 2024","externalUrl":null,"permalink":"/en/tags/handwritten-digit-recognition/","section":"Tags","summary":"","title":"Handwritten Digit Recognition","type":"tags"},{"content":"","date":"2024年1月23日","externalUrl":null,"permalink":"/tags/mnist/","section":"Tags","summary":"","title":"MNIST","type":"tags"},{"content":"","date":"23 一月 2024","externalUrl":null,"permalink":"/en/tags/neural-networks/","section":"Tags","summary":"","title":"Neural Networks","type":"tags"},{"content":"","date":"2024年1月23日","externalUrl":null,"permalink":"/tags/%E5%8D%B7%E7%A7%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/","section":"Tags","summary":"","title":"卷积神经网络","type":"tags"},{"content":" 背景 # 这是“实例学PyTorch”系列的第3篇文章。在前两篇文章“实例学PyTorch（1）：MNIST手写数字识别（一）——PyTorch基础和神经网络基础”和“实例学PyTorch（2）：MNIST手写数字识别（二）——神经网络中的参数选择”中，我们介绍了PyTorch的基本概念和使用方法，使用一个简单的三层全连接神经网络实现了MNIST手写数字识别，并简单讨论了一下神经网络中参数选择的问题，这算是深度学习或计算机视觉领域的“Hello World\u0026quot;。在这篇文章中，我们将使用一个卷积神经网络实现MNIST手写数字识别，算是对之前两篇文章的一个扩展。\n本文的代码可以在我的GitHub仓库https://github.com/jin-li/pytorch-tutorial中的T03_mnist_cnn文件夹中找到。该代码是基于PyTorch官方的示例代码https://github.com/pytorch/examples改编的。\n卷积神经网络（CNN）简介 # 在前两篇文章中，我们用了一个简单的全连接神经网络来解决MNIST手写数字识别问题。全连接神经网络的效果已经不错了，但细想一下，全连接神经网络有一个很大的缺点，就是没有考虑到图像的局部特征。在图像识别中，图像的局部特征是非常重要的，例如图像的边缘、纹理等。卷积神经网络（Convolutional Neural Network，CNN）是一种专门用于处理图像的神经网络，它可以有效地提取图像的局部特征，从而提高图像识别的准确度。\n卷积神经网络的关键点在于卷积层（Convolutional Layer）和池化层（Pooling Layer）。卷积层是用一个卷积核（Convolutional Kernel）对输入图像进行卷积操作，从而提取图像的局部特征。池化层是用一个池化核（Pooling Kernel）对卷积层的输出进行池化操作，从而减少特征图的大小，提高计算效率。卷积层和池化层交替出现，最后通过全连接层得到输出。接下来我们将设计一个简单的卷积神经网络来实现MNIST手写数字识别。\n卷积核是卷积神经网络的核心，它是一个小的矩阵，用来提取图像的局部特征。卷积核的大小、步长、填充等参数都是需要调整的超参数。卷积核的大小决定了卷积核能感受的区域的大小，即局部感受野。步长是卷积核每次移动的距离，填充是在图像周围填充一圈0，可以保持图像的大小不变。池化核的大小、步长等参数也是需要调整的超参数。关于卷积核的更多信息可以参考这篇文章。这里我们引用这篇文章中的几个图来说明卷积核的工作原理：\n卷积计算 普通卷积 空洞卷积 反卷积 MNIST手写数字识别 # CNN模型设计 # 我们可以先确定输入和输出。显然这个神经网络的输入是28x28的灰度图像，输出是0到9之间的一个数字。\n我们需要选择一个神经网络类型，例如全连接神经网络、卷积神经网络、循环神经网络等。这里我们选择使用一个简单的卷积神经网络。\n我们需要确定网络的结构，包括网络的层数、每层的神经元数、激活函数等。这里我们选择一个简单的卷积神经网络，包括两个卷积层和两个全连接层。\n代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import torch import torch.nn as nn import torch.nn.functional as F class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 32, 3, 1) self.conv2 = nn.Conv2d(32, 64, 3, 1) self.dropout1 = nn.Dropout(0.25) self.dropout2 = nn.Dropout(0.5) self.fc1 = nn.Linear(9216, 128) self.fc2 = nn.Linear(128, 10) def forward(self, x): x = self.conv1(x) x = F.relu(x) x = self.conv2(x) x = F.relu(x) x = F.max_pool2d(x, 2) x = self.dropout1(x) x = torch.flatten(x, 1) x = self.fc1(x) x = F.relu(x) x = self.dropout2(x) x = self.fc2(x) output = F.log_softmax(x, dim=1) return output 这里我们定义了一个名为Net的类，继承自nn.Module。在__init__方法中，我们定义了两个卷积层conv1和conv2，和两个全连接层fc1和fc2。网络的结构如下图所示：\n由于MNIST数据集是灰度图像，所以输入通道数是1。如果是RGB彩色图像，输入通道数是3。输出通道数表示卷积核的个数，即每个卷积层想要提取的特征数，这里假设第一个卷积层提取32个特征，第二个卷积层提取64个特征。卷积核的大小是每个卷积核能感受的区域的大小，即局部感受野，这里假设卷积核大小是3x3。步长是卷积核每次移动的距离，这里假设步长是1。 28x28的输入经过步长为1的3x3卷积核，输出是26x26；再经过第二个卷积层，输出是24x24；再经过最大池化层，输出是12x12。两个dropout层是为了防止过拟合，不影响输出参数的形状。因此，第一个全连接层的输入神经元数是12x12x64=9216。 中间的Dropout层是为了防止过拟合，Dropout是一种正则化方法，可以随机地将一些神经元的输出设置为0，从而减少神经元之间的依赖关系。 经过两个全连接层，最后输出10个神经元，分别表示0到9之间的数字。最后使用F.log_softmax()函数将输出转换为概率。 在forward方法中，我们定义了网络的前向传播过程，即输入数据经过每一层的计算，最后输出预测结果。其中： 两个卷积层都使用ReLU激活函数，第二个卷积层后面跟了一个最大池化层。 然后执行一次Dropout操作，将输出展平为一维向量，输入到两个全连接层中。 在第一个全连接层后面又执行了一次ReLU激活函数，然后再执行一次Dropout操作。最后输出10个神经元，分别表示0到9之间的数字。 数据加载、预处理、训练、测试 # 这部分内容其实和上一篇文章中介绍的简单全连接神经网络是一样的，只是需要把上一篇文章中定义的SimpleNet类换成这里定义的Net类。\n这里的代码实际上就是PyTorch官方给的示例，可以参考这里，或者在我的GitHub仓库https://github.com/jin-li/pytorch-tutorial中的T03_mnist_cnn文件夹中找到。\n这里我们创建了一个Python脚本来运行这个CNN模型，并绘制模型的性能曲线。\n本文代码所需的Python环境和之前两篇文章是一样的，可以通过conda activate pytorch-mnist激活环境，然后使用如下命令运行代码：\n1 python plot_performance.py 运行结果 # 我分别使用CPU（Intel i5-9600K）和GPU（NVIDIA GeForce RTX 4060 Ti）运行了这个CNN模型，运行时间分别为12分56秒和2分25秒。可以看到，使用GPU运行速度快了很多。运行所需的GPU资源实际并不高，GPU占用率约12%，显存占用率约740MB。\n模型的表现如下图所示：\n可见，这个CNN模型在MNIST数据集上的准确率约为99.2%，比之前的全连接神经网络模型要好。\n使用模型进行数字识别 # 本文已经是介绍MNIST手写数字识别的第三篇文章了，但是我们一直都只是在训练模型，没有实际使用我们训练好的模型。现在我们就来使用我们训练好的CNN模型来识别一些手写数字。\n首先我们在训练模型时需要把模型的参数保存下来，这样我们在待会儿使用模型时就可以直接加载这些参数，而不必重新训练。保存模型参数的代码如下：\n1 torch.save(model.state_dict(), \u0026#34;mnist_cnn.pt\u0026#34;) 在mnist_cnn.py的代码中已经有这个功能了，我们需要在运行mnist_cnn.py时指定--save-model参数，这样模型参数就会被保存到mnist_cnn.pt文件中：\n1 python mnist_cnn.py --save-model 然后我们可以使用保存的模型参数来识别手写数字。我们可以使用PIL库来读取图片，然后使用torchvision库来对图片进行预处理，最后使用我们训练好的CNN模型来识别图片中的数字。\n需要注意的是：\n我们这里使用的MNIST训练集中的图片是黑底白字的28x28像素的图片，所以我们在创建手写图片时也要保持黑底白字。 我们自己手写的数字不一定是28x28像素的，所以我们需要对图片进行缩放，使其变成28x28像素的图片。 MNIST训练集中的图片满足均值为0.1307，标准差为0.3081的正态分布，所以我们在对图片进行预处理时需要对图片进行归一化。 做这些处理的代码如下：\n1 2 3 4 5 6 7 8 from torchvision import transforms transform = transforms.Compose([ transforms.Grayscale(num_output_channels=1), transforms.Resize((28, 28)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) 完整的代码在classify_image文件中。\n这里我在电脑上使用鼠标手写了几个数字，对每个数字分别截图保存到T03_mnist_cnn文件夹中numbers目录下：\n最后我们可以使用classify_image.py来识别一个手写数字：\n1 python classify_image.py numbers/number1.png 我分别识别了这10个手写数字，发现模型正确识别出了8个数字，没识别出的两个是数字2和9，模型将它们分别识别成了4和8，虽然有些令我费解，但这个模型的表现也称得上是差强人意了。\n总结 # 在这篇文章中，我们使用了一个卷积神经网络来实现MNIST手写数字识别，相比之前的全连接神经网络，CNN模型的准确率有了明显的提升。我们还使用了训练好的CNN模型来识别一些手写数字，模型的表现也还算不错。\nMNIST作为计算机视觉领域的“Hello World”，是一个非常经典的数据集，也是一个非常好的入门数据集。我们已经写了三篇文章来讨论它，这里不妨先告一段落，先来研究一些其他机器学习或深度学习的问题，等以后有机会我们再来使用MNIST数据集来研究一些其他的深度学习算法。\n","date":"2024年1月23日","externalUrl":null,"permalink":"/p/%E5%AE%9E%E4%BE%8B%E5%AD%A6pytorch3mnist%E6%89%8B%E5%86%99%E6%95%B0%E5%AD%97%E8%AF%86%E5%88%AB%E4%B8%89%E5%8D%B7%E7%A7%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9Ccnn/","section":"Posts","summary":" 背景 # 这是“实例学PyTorch”系列的第3篇文章。在前两篇文章“实例学PyTorch（1）：MNIST手写数字识别（一）——PyTorch基础和神经网络基础”和“实例学PyTorch（2）：MNIST手写数字识别（二）——神经网络中的参数选择”中，我们介绍了PyTorch的基本概念和使用方法，使用一个简单的三层全连接神经网络实现了MNIST手写数字识别，并简单讨论了一下神经网络中参数选择的问题，这算是深度学习或计算机视觉领域的“Hello World\"。在这篇文章中，我们将使用一个卷积神经网络实现MNIST手写数字识别，算是对之前两篇文章的一个扩展。\n","title":"实例学PyTorch（3）：MNIST手写数字识别（三）——卷积神经网络（CNN）","type":"post"},{"content":"","date":"2024年1月23日","externalUrl":null,"permalink":"/tags/%E6%89%8B%E5%86%99%E6%95%B0%E5%AD%97%E8%AF%86%E5%88%AB/","section":"Tags","summary":"","title":"手写数字识别","type":"tags"},{"content":"","date":"20 一月 2024","externalUrl":null,"permalink":"/en/tags/hyperparameter/","section":"Tags","summary":"","title":"Hyperparameter\"","type":"tags"},{"content":"","date":"20 一月 2024","externalUrl":null,"permalink":"/en/tags/parameter-selection/","section":"Tags","summary":"","title":"Parameter Selection","type":"tags"},{"content":"","date":"2024年1月20日","externalUrl":null,"permalink":"/tags/%E5%8F%82%E6%95%B0%E9%80%89%E6%8B%A9/","section":"Tags","summary":"","title":"参数选择","type":"tags"},{"content":"","date":"2024年1月20日","externalUrl":null,"permalink":"/tags/%E8%B6%85%E5%8F%82%E6%95%B0/","section":"Tags","summary":"","title":"超参数","type":"tags"},{"content":" 背景 # 这是“实例学PyTorch”系列的第2篇文章。在上一篇文章“实例学PyTorch（1）：MNIST手写数字识别（一）——PyTorch基础和神经网络基础”中，我们介绍了PyTorch的基本概念和使用方法，并使用一个简单的三层全连接神经网络实现了MNIST手写数字识别，这算是深度学习领域的“Hello World\u0026quot;。在这篇文章中，我们将简单讨论一下这个简单的三层神经网络中参数的选择，对比不同的参数选择对模型性能的影响。\n本文的代码可以在我的GitHub仓库https://github.com/jin-li/pytorch-tutorial中的T02_mnist_cnn文件夹中找到。该代码是基于PyTorch官方的示例代码https://github.com/pytorch/examples得到的。\n参数选择 # 基于上一篇文章中的三层全连接神经网络，我们可以对其中的参数进行调整来观察模型性能的变化。\n机器学习模型中的参数主要可以分为两类：超参数和模型结构。超参数是在训练模型之前设置的参数，例如学习率、迭代次数、批次大小等。模型结构是指模型的网络结构，例如网络的层数、每层的神经元数、激活函数、损失函数、正则化方法等。\n模型结构 # 我们在上一篇文章中使用的是一个简单的三层全连接神经网络，我们可以尝试增加或减少网络的层数、每层的神经元数、激活函数等来观察模型性能的变化。也可以尝试使用不同的损失函数和正则化方法。\n神经网络的层数和每层的神经元数\n在机器学习中，神经网络的层数和每层的神经元数是非常重要的超参数。增加神经网络的层数和每层的神经元数可以增加模型的表达能力，但也会增加模型的复杂度，可能会导致过拟合。反之，减少神经网络的层数和每层的神经元数可以减少模型的复杂度，但也可能会导致模型的准确度下降。因此，我们需要在二者之间进行权衡。\n这里我们尝试几个不同的网络结构，例如分别使用1层、3层、5层隐藏层，每层的神经元数也分别尝试不同的值，例如64、128、256等。\n激活函数\n激活函数是神经网络中非常重要的概念，它也是神经网络可以拟合各种模型的关键。我们可以这样理解激活函数：无论什么模型，其本质都是在做一些判断，根据不同的输入来判断输出该是什么，这样的判断可能是单次的，也可能是很多次判断的综合。激活函数就是将非线性因素引入神经元，为神经元提供一种判断能力。\n常见的激活函数有ReLU、Sigmoid、Tanh等。可以看到，不同的激活函数实际上是很不同的，但其本质都是为了引入一个非线性因素。这里我们也尝试一些不同的激活函数。\n损失函数\n损失函数是用来衡量模型预测值与真实值之间的差异的函数，即评价模型好坏的指标。损失函数很重要，因为它决定了模型的优化方向。\n常见的损失函数有交叉熵损失函数、均方误差损失函数等。这里我们也尝试一些不同的损失函数。需要注意的是，均方差损失函数一般用于回归问题，交叉熵损失函数一般用于分类问题。但我们这里仍然可以尝试使用均方差损失函数。由于均方误差损失函数参数中的目标值（target）需要输入一个one-hot编码的向量，而原来的MNIST数据集中的标签是一个整数，因此我们需要对标签进行one-hot编码。本文对应的代码中对损失函数做了一次判断，如果损失函数是均方误差损失函数，则对标签进行one-hot编码。\n1 2 if loss_function == F.mse_loss: target = F.one_hot(target, num_classes=10).float() 所谓one-hot编码就是将一个整数转换为一个向量，向量的长度等于类别的个数，其中只有一个元素为1，其余元素为0。例如，对于MNIST数据集，共有10个类别，我们可以将标签0转换为[1, 0, 0, 0, 0, 0, 0, 0, 0, 0]，标签1转换为[0, 1, 0, 0, 0, 0, 0, 0, 0, 0]，以此类推。\n正则化方法\n正则化主要是用来防止过拟合的。为什么正则化可以防止过拟合呢？\n首先我们来分析过拟合的原因。过拟合是指模型在训练集上表现很好，但在测试集上表现很差。过拟合的原因是模型在训练集上学习到了训练集的噪声，导致模型在测试集上泛化能力很差。正则化就是将模型的参数加入到损失函数中，使得模型的参数尽量小，从而减少模型的复杂度，防止模型在训练集上学习到噪声。\n常见的正则化方法有L1正则化、L2正则化、Dropout等。这里我们也尝试一些不同的正则化方法。\n超参数 # 超参数是在训练模型之前设置的参数，例如学习率、迭代次数、批次大小等。这些参数对模型的性能有很大的影响，我们需要仔细选择这些参数。\n学习率\n学习率是模型在更新参数时的步长，它决定了模型参数的更新速度。学习率太小会导致模型收敛速度慢，学习率太大则可能导致模型不收敛。\n迭代次数\n迭代次数是指模型在训练集上迭代的次数。迭代次数太少可能导致模型欠拟合，迭代次数太多可能导致模型过拟合。\n批次大小\n我们在训练模型时通常会将训练集分成若干个批次，每个批次包含若干个样本，这样可以减少内存的占用，加快模型的训练速度。批次大小太小可能导致模型收敛速度慢，批次大小太大可能导致模型过拟合。\n修改代码使参数可定制 # 在上一篇文章中，我们使用的模型结构是固定的，超参数可以通过命令行参数来指定。现在我们修改代码，把main函数独立出来，把这些参数全部作为main函数的参数。这样，我们可以再写一个脚本来调用main函数，传入不同的参数，从而实现不同的模型结构和超参数的选择。\n这里我们就不贴完整的代码了，只给出修改后的main函数原型：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def main(batch_size = 64, epochs = 14, lr = 1.0, test_batch_size = 1000, no_cuda = False, no_mps = False, dry_run = False, seed = 1, log_interval = 10, save_model = False, hidden_layers = [128], activation_functions = [F.relu], loss_function = F.nll_loss, regularizations = [None] ): 这里前3个参数是超参数，后4个参数是模型结构的参数，中间几个参数是一些辅助性的参数。\n最后，因为我们想要对比不同参数下的模型性能，我们让主函数返回训练中的损失值和准确率，这样我们就可以在调用主函数的脚本中绘图对比不同参数下的模型性能。为此我们定义一个存储训练性能的类：\n1 2 3 4 5 6 7 8 class PerformanceMetrics: def __init__(self): self.train_count = [] self.train_loss = [] self.test_count = [] self.test_loss = [] self.test_accuracy = [] self.run_time = 0.0 在训练过程中，我们将训练和测试的损失值和准确率存储到这个类中，最后由主函数返回。具体代码参见GitHub仓库中本文对应的代码。\n性能对比 # 我们这里关注了3个超参数和4个模型参数，共7个参数，而每个参数都有多种选择。假设我们每个参数只选择3个值，那么总共有$3^7=2187$种组合，这可不是一个小数目！\n正是由于参数组合多种多样，寻找一个最优的参数组合就像是通过不停的组合试错来寻找一个配方，因此很多人把机器学习形象地称为“炼丹”。的确，这跟古代炼丹术士的活儿很像。\n按照我们在上一篇文章中测试的结果，每次训练用GPU需要约2分15秒，如果我们要测试2187种组合，那么总共需要$2187\\times 2.25/60=82$小时，也就是约3天半的时间。虽然这并不是做不到，但这样的时间成本还是有点高。如果之后训练更复杂的模型，需要的时间会几倍、几十倍地增加，因此我们必须有所取舍，不能测试所有的参数组合。\n在实际应用中，我们可以通过一些启发式的方法来选择参数，例如网格搜索、随机搜索、贝叶斯优化等。这些方法可以帮助我们更快地找到一个较优的参数组合。\n这里我们采用控制变量的方法，将上一篇文章中的模型结构和超参数作为基准，每次只调整一个参数，观察模型性能的变化。这样可以更好地理解每个参数对模型性能的影响。具体代码参见GitHub仓库https://github.com/jin-li/pytorch-tutorial中T02_mnist_parameters文件夹中的parametric_study.py文件。\n每组参数训练完后，我们绘制出训练和测试的损失值和准确率：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 fig, axs = plt.subplots(2, 1, figsize=(10, 10)) fig.suptitle(f\u0026#39;Parameter Study: {param_names[idx]}\u0026#39;) axs[0].plot(metrics_ref.test_count, metrics_ref.test_loss, label=ref_labels[idx]) for i, metric in enumerate(metrics): axs[0].plot(metric.test_count, metric.test_loss, label=param_labels[idx][i]) axs[0].legend() axs[0].set_title(\u0026#39;Training Loss\u0026#39;) axs[0].set_xlabel(\u0026#39;data count\u0026#39;) axs[0].set_ylabel(\u0026#39;loss\u0026#39;) axs[1].plot(metrics_ref.test_count, metrics_ref.test_accuracy, label=ref_labels[idx]) for i, metric in enumerate(metrics): axs[1].plot(metric.test_count, metric.test_accuracy, label=param_labels[idx][i]) axs[1].legend() axs[1].set_title(\u0026#39;Test Accuracy\u0026#39;) axs[1].set_xlabel(\u0026#39;data count\u0026#39;) axs[1].set_ylabel(\u0026#39;accuracy\u0026#39;) plt.savefig(f\u0026#39;test{idx}_{param_names[idx]}.png\u0026#39;) 这里需要的Python虚拟环境和上一篇文章中的一样，可以通过conda activate pytorch-mnist来激活虚拟环境。然后可以使用如下命令来运行所有的测试：\n1 python parametric_study.py 模型结构参数 # 神经网络层数\n这里我尝试了1层、3层、5层隐藏层，中间层的神经元数目在64、128、256之间变化。下图是测试的结果：\n可见，1个隐藏层的模型性能已经很不错了，增加隐藏层的数目并没有明显提升模型性能。当然，这可能也与模型中的其他参数有关，也许增加隐藏层数目的同时，还需要调整神经网络的其他参数才能发挥隐藏层的作用。但从这个实验结果来看，由于本问题比较简单，1个隐藏层的模型已经足够。\n激活函数\n这里我尝试了ReLU、Sigmoid、Tanh等激活函数。下图是测试的结果：\n可见，这三种激活函数的性能差不多，但ReLU的效果相对更好一点儿，Sigmoid和Tanh的模型性能相对差了一点儿。这也符合我们的预期，因为ReLU激活函数是目前最常用的激活函数，它的优点是计算简单，收敛速度快，不容易出现梯度消失的问题。\n损失函数\n除了之前模型中使用的负对数似然损失函数nll_loss()，这里我尝试了交叉熵损失函数cross_entropy()和均方误差损失函数mse_loss()。下图是测试的结果：\n可见，交叉熵损失函数和负对数似然损失函数的表现差不多，性能比较好，均方误差损失函数的模型性能最差。这也符合我们的预期，因为均方误差损失函数一般用于回归问题，交叉熵损失函数一般用于分类问题。\n正则化方法\n正则化方法比较特殊，在本文章的代码中使用正则化需要对代码做过多修改，这里我暂时没有测试正则化的效果，留到以后再来研究。\n超参数 # 批次大小\n这里我尝试了16、64、256三个批次大小。下图是测试的结果：\n可见，批次大小为64和256时模型性能最好，批次大小为16时模型性能最差。这里我也统计了训练时间，批次大小为16时训练时间最长，批次大小为64和256时训练时间相对较短：\n1 2 3 batch size 16: 164.6s batch size 64: 133.9s batch size 256: 125.0s 在内存和计算资源允许的情况下，我们可以选择较大的批次大小，这样可以加快模型的训练速度。\n迭代次数\n迭代次数是指模型在训练集上迭代的次数。这里我最多尝试了25次迭代。下图是测试的结果：\n可见，随着迭代次数增加，损失值逐渐减小，准确率逐渐增加。但是，当迭代次数超过一定值之后，模型的性能不再提升，甚至可能出现过拟合。因此，我们需要根据模型的性能来选择合适的迭代次数。对于本问题，14次迭代已经足够。\n学习率\n这里我尝试了0.1、1.0、10.0三个学习率。下图是测试的结果：\n可见，学习率为1.0时模型性能最好，学习率为10时模型性能最差。这说明学习率过大，模型可能会发散；学习率过小，模型可能会收敛缓慢。因此，我们需要根据模型的性能来选择合适的学习率。对于本问题，学习率为1.0已经足够。\n计算资源 # 上面的实验是在我的个人电脑上运行的，使用的是NVIDIA GeForce GTX 4060 Ti显卡。每个例子用的计算资源都差不多，基本不怎么消耗显卡资源，计算时显卡的占用率在5%左右，显存大约占用了180 MB，运行时间大约是2分15秒。如果使用CPU，运行时间大约是3分钟左右。\n总结 # 在本文中，我们讨论了神经网络中的参数选择问题，包括模型结构和超参数。我们通过修改代码，使得模型结构和超参数可以通过命令行参数来指定，然后通过控制变量的方法，逐一调整每个参数，观察模型性能的变化。\n由于计算资源有限，我们只测试了一部分参数组合，但这已经足够说明了参数选择对模型性能的影响。在实际应用中，我们可以通过一些启发式的方法来选择参数，例如网格搜索、随机搜索、贝叶斯优化等。这些方法可以帮助我们更快地找到一个较优的参数组合。我们将在后续文章中继续讨论这些方法。\n感兴趣的读者可以利用GitHub仓库https://github.com/jin-li/pytorch-tutorial中本文对应的代码（在T02_mnist_cnn文件夹中），尝试不同的参数组合，观察模型性能的变化。\n","date":"2024年1月20日","externalUrl":null,"permalink":"/p/%E5%AE%9E%E4%BE%8B%E5%AD%A6pytorch2mnist%E6%89%8B%E5%86%99%E6%95%B0%E5%AD%97%E8%AF%86%E5%88%AB%E4%BA%8C%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E4%B8%AD%E7%9A%84%E5%8F%82%E6%95%B0%E9%80%89%E6%8B%A9/","section":"Posts","summary":" 背景 # 这是“实例学PyTorch”系列的第2篇文章。在上一篇文章“实例学PyTorch（1）：MNIST手写数字识别（一）——PyTorch基础和神经网络基础”中，我们介绍了PyTorch的基本概念和使用方法，并使用一个简单的三层全连接神经网络实现了MNIST手写数字识别，这算是深度学习领域的“Hello World\"。在这篇文章中，我们将简单讨论一下这个简单的三层神经网络中参数的选择，对比不同的参数选择对模型性能的影响。\n","title":"实例学PyTorch（2）：MNIST手写数字识别（二）——神经网络中的参数选择","type":"post"},{"content":" 缘起 # 我自建的网站几乎全部是用Docker部署的，在跟朋友聊起我是如何搭建这些网站时，我发现很多人都表示听说过Docker或者容器，但并不知道它们到底是什么。\n其实，我在最开始使用Docker时，接触到很多容器相关的概念，但由于没有实践过，对这些概念也是一头雾水。如今已经用了很长时间的容器技术，就想总结一下相关的知识，希望能够帮助到其他人。\n这是关于docker容器的第一篇文章，这个系列的其他文章链接如下：\n容器（2）：docker最佳实践指南——docker-compose和Portainer 容器（3）：docker最佳实践指南——数据卷volume的管理 容器（4）：docker最佳实践指南——容器更新、升级和迁移 容器（5）：docker最佳实践指南——容器更新监测工具WUD（What\u0026rsquo;s Up Docker） 容器（6）：使用Docker时的一些误区、坏习惯和问题 容器化 # 什么是容器化 # 容器化是一种虚拟化技术，它可以将应用程序及其依赖打包在一起，以便在不同的环境中运行。容器化的应用程序可以在任何地方运行，包括开发人员的笔记本电脑、物理服务器、虚拟机、容器集群和公有云等。\n为什么需要容器化 # 直接看容器化的概念，可能还是不好理解。我们用一个例子来说明为什么需要容器化，这样容器化的概念就会很清晰了。\n首先，如果你只是一个程序或者服务的使用者，不关心这个程序或服务是怎样在机器上运行的，那么你根本不必了解容器化的概念。\n其次，如果你运行的程序或服务只是一个简简单单的，只要能在你的电脑上运行就行了的程序，那么你也不必了解容器化的概念。\n所以，这里假设你是一个开发者（当然，你不必真的在开发程序，因为对于大多数新手而言，他们并没有开发什么程序，他们只是在使用从开源网站上或其他地方搞到的程序），你有一个想运行的程序，这个程序可以提供一些服务，你想在不同的地方都能获取这些服务。举个例子，你用某个流行的网站框架自建了一个网站，你希望自己能够在不同地方的不同设备上访问这个网站，也希望别人能访问你的网站。这时，你将这个网站部署到了自己的电脑上，假设你的网络运营商给你分配了静态IP、开放了端口，你就可以将你的网站在公网上开放，这样你就可以在任何能连接到公网的地方访问这个网站了。\n过了一段时间，你购买了一个域名，并用网页服务器（如Nginx或者Apache）将这个域名绑定到了你的网站上，这样你就可以通过域名访问你的网站了。\n又过了一段时间，你发现你在你的网站上部署的服务越来越多，你的网站也越来越受欢迎，访问量越来越大，你电脑孱弱的性能已经无法满足这么大的访问量了，你决定购买一台更强的电脑或者服务器，将你的网站迁移过去。\n于是你组装了一台服务器，装上了Linux系统，开始迁移网站。你把原来电脑上的网站文件复制到了新的电脑上，然后开始运行，发现网站根本无法运行。因为你的网站有很多依赖项，例如Python库和一些乱七八糟的库，你只能一个一个安装这些库。然后会发现很多库的默认版本都已经比你在原来的电脑上安装的版本新了，新库根本不兼容你原来的旧代码，你只能上网找旧版本的库，一个个安装。幸运的话，你可以顺利安装上旧版本的库；要是不幸运的话，你可能发现你的操作系统也依赖某些库，你安装旧版本后操作系统直接崩溃。\n这还没完，即使你成功解决了依赖库的问题，你在原来电脑上为你的网站配置的数据库、网页服务器等等，全部需要重新配置。\n这时你可能会想，要是能把原来电脑上的网站、数据库、网页服务器，以及它们的依赖项和运行环境，全部打包起来，然后在新电脑上可以直接运行就好了。如果你之前用过虚拟机的话，你可能会想，这不就是创建一个和原来的电脑相同的虚拟机，然后将虚拟机迁移到新电脑上就行了吗？\n但是虚拟机太笨重了，因为虚拟机包含了一个完整的操作系统，一般都会占用很多资源，启动速度也很慢。所以我们不想创建整个操作系统，我们只想创建一个和原来的程序相同的运行环境。这，其实就是容器化。\n如何容器化 # 现在我们的目标已经很明确了，我们要将一个程序及其依赖项和运行环境打包起来，然后在不同的地方运行这个打包好的程序。所以，简单来说，容器化分两步：1. 打包程序及其依赖项和运行环境；2. 在机器上隔离出独立的运行环境，运行打包好的程序。\n打包 # 这一步比较好说，只需要将程序及其依赖项和运行环境打包起来就行了。打包好的程序及其依赖项和运行环境，我们称之为镜像（Image）。\n隔离环境 # 隔离环境的技术依赖于操作系统的特性，在Linux系统中，隔离环境依赖于三项技术：命名空间（Namespace）、控制组（Control Group）和chroot：\n命名空间：命名空间可以隔离进程的视图，使得进程只能看到自己的视图，而看不到其他进程的视图。 控制组：控制组可以限制进程的资源使用，例如CPU、内存、磁盘IO等。 chroot：chroot可以更改进程的根目录。 这三项技术的详细介绍超出了本文的范围，有兴趣的读者可以自行搜索。\nWindows系统也有容器技术，但和Linux系统有所不同。在Windows系统的Docker Desktop中，容器化的技术依赖于Hyper-V虚拟机。\n容器与虚拟机的区别 # 从上面的描述可以看出，容器和虚拟机都提供了隔离环境的功能，二者都可以称为沙盒（Sandbox）技术。但二者又有很大区别：\n容器是在操作系统层面上实现的，提供的隔离是进程级别的，必须和宿主机共享操作系统内核 虚拟机是在硬件层面上实现的，提供的隔离是操作系统级的，可以有自己的操作系统内核 由于容器和宿主机共享操作系统内核，所以容器的启动速度（秒级）比虚拟机（分钟级）快得多，容器的资源占用也比虚拟机少得多。\nDocker # 容器化需要的三项技术分别在1979年（chroot）、2002年（namespace）和2007年（cgroup）就已经出现了，但是这三项技术的初衷并不是容器化。直到2013年，Docker横空出世，将这三项技术结合起来，容器化开始流行起来。\nDocker安装 # 在使用Docker之前，首先需要在你的系统上安装Docker。Docker的安装方式因操作系统而异，下面分别介绍在Ubuntu、Fedora、macOS和Windows上的安装方法。对于docker-compose的安装，将在后文中单独介绍。\nUbuntu # 在Ubuntu上推荐使用Docker官方的APT源来安装Docker，这样可以获得最新的版本。以下是安装步骤：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 1. 卸载旧版本（如果有的话） sudo apt-get remove docker docker-engine docker.io containerd runc # 2. 安装依赖 sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg # 3. 添加Docker官方GPG密钥 sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg # 4. 添加Docker官方APT源 echo \u0026#34;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release \u0026amp;\u0026amp; echo \u0026#34;$VERSION_CODENAME\u0026#34;) stable\u0026#34; | sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null # 5. 安装Docker sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # 6. 将当前用户添加到docker组（免sudo运行docker） sudo usermod -aG docker $USER # group变更需要重新登录才能生效，退出并重新登录后即可免sudo运行docker # 7. 验证安装 docker run hello-world 安装完成后，建议配置Docker镜像加速（国内用户）：\n1 2 3 4 5 6 7 8 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json \u0026lt;\u0026lt;-\u0026#39;EOF\u0026#39; { \u0026#34;registry-mirrors\u0026#34;: [\u0026#34;https://mirror.ccs.tencentyun.com\u0026#34;] } EOF sudo systemctl daemon-reload sudo systemctl restart docker Fedora # Fedora上同样使用Docker官方源安装。从Fedora 42开始，dnf config-manager的用法有所变化（旧版--add-repo已不再支持），需要根据你的Fedora版本选择对应的命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 1. 卸载旧版本 sudo dnf remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-selinux docker-engine-selinux docker-engine # 2. 添加Docker官方源 sudo dnf -y install dnf-plugins-core # Fedora 42+（新语法） sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo # Fedora 41及以下（旧语法） # sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo # 3. 安装Docker sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # 4. 启动Docker并设置开机自启 sudo systemctl start docker sudo systemctl enable docker # 5. 将当前用户添加到docker组 sudo usermod -aG docker $USER # 注意：group变更需要重新登录才能生效，退出并重新登录，或者重启系统，或者使用下面的命令 newgrp docker # 6. 验证安装 docker run hello-world 注意：\nFedora默认启用了SELinux，可能会导致容器访问宿主机文件时权限不足。如果遇到权限问题，可以临时关闭SELinux（sudo setenforce 0），或者为容器添加正确的SELinux标签（例如在挂载卷时添加:z或:Z后缀）。 newgrp docker虽然能在当前shell临时加载新group，但它会启动一个子shell，实际操作中建议直接退出登录再重新登录。 macOS # macOS上最简单的方式是安装Docker Desktop：\n前往 Docker Desktop官网 下载macOS版本的安装包 双击.dmg文件，将Docker拖入Applications文件夹 启动Docker Desktop，等待Docker Engine启动完成 在终端中验证安装： 1 docker run hello-world 注意：\nDocker Desktop for Mac需要macOS 13 Ventura或更高版本（2025年起的版本已不再支持macOS 12 Monterey） Intel芯片和Apple Silicon（M1/M2/M3/M4）芯片的安装包不同，请选择正确的版本 Docker Desktop免费版仅限个人使用、教育和非商业开源项目；商业用途需要购买付费订阅 也可以使用Homebrew安装：\n1 brew install --cask docker Windows # Windows上同样使用Docker Desktop：\n前往 Docker Desktop官网 下载Windows版本的安装包 双击.exe安装文件，按照安装向导完成安装 安装过程中会提示启用WSL 2（Windows Subsystem for Linux 2）功能，建议选择启用 安装完成后重启电脑 启动Docker Desktop，等待Docker Engine启动 在PowerShell或CMD中验证安装： 1 docker run hello-world 注意：\nWindows 10/11 64位：Home版需要WSL 2后端；Pro/Enterprise版可以选择Hyper-V或WSL 2后端，推荐使用WSL 2 需要在BIOS中启用虚拟化（Intel VT-x或AMD-V） WSL 2安装方法：以管理员身份运行PowerShell，执行 wsl --install，然后重启电脑 Docker Desktop免费版的商业使用限制同macOS 验证Docker安装 # 在任何平台上，安装完成后都可以通过以下命令验证Docker是否正确安装：\n1 2 3 4 5 6 7 8 9 # 查看Docker版本 docker --version # 示例输出: Docker version 27.5.1, build 9f9e405 # 查看Docker详细信息 docker info # 运行测试容器 docker run hello-world 如果hello-world容器成功运行并输出欢迎信息，说明Docker已经正确安装并可以正常使用了。\nDocker的组成和使用都很简单，如下图所示：\nDocker的组成 # 图中包含了Docker的三个核心概念：镜像（Image）、容器（Container）和仓库（Repository）。\n镜像：镜像是一个只读的模板，它包含了运行程序所需要的所有东西，包括代码、运行环境、库、环境变量和配置文件等。镜像是容器的基础。\n仓库：仓库是用来存放镜像的地方，可以理解为镜像的集合。仓库分为公有仓库和私有仓库，公有仓库是开放的，任何人都可以上传和下载镜像；私有仓库是私有的，只有拥有者可以上传和下载镜像。\n容器：容器是镜像的运行实例，它包含了镜像以及运行时所需要的东西，包括文件系统、系统环境、网络配置等。容器是镜像的运行时状态。\nDocker的使用（单个容器） # Docker的使用有两种情况：一种是使用别人的镜像，另一种是自己制作镜像。\n使用别人的镜像 # 使用别人的镜像非常简单，只需要两步：\n下载镜像：使用docker pull命令下载镜像，例如：\n1 docker pull ubuntu:latest 这个命令会从Docker Hub下载一个名为ubuntu的镜像，标签latest表示拉取的镜像是最新的版本。\n运行容器：使用docker run命令运行容器，例如：\n1 docker run -it --rm ubuntu:latest /bin/bash 上面的命令会运行一个名为ubuntu的容器，标签为latest，并且进入容器的bash终端。\n使用自己的镜像 # 使用自己的镜像需要三步：\n编写Dockerfile：Dockerfile是一个文本文件，它包含了一系列命令，这些命令用来构建镜像。例如：\n1 2 3 4 5 FROM python:3.12 WORKDIR /app COPY . /app RUN pip install -r requirements.txt CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] 上面的Dockerfile中，FROM命令表示基础镜像是python:3.12，WORKDIR命令表示工作目录是/app，COPY命令表示将当前目录下的所有文件复制到/app目录下，RUN命令表示安装requirements.txt中的依赖，CMD命令表示容器启动时运行的命令。\n构建镜像：使用docker build命令构建镜像，例如：\n1 docker build -t myapp . 上面的命令会在当前目录下构建一个名为myapp的镜像。\n运行容器：使用docker run命令运行容器，例如：\n1 docker run -d -p 5000:5000 myapp 上面的命令会运行一个名为myapp的容器，并且将容器的5000端口映射到宿主机的5000端口。\nDocker的使用（多个容器） # 通常情况下，我们在部署一个服务时往往需要用到不止一个容器，例如我们要自建一个网盘服务，我们可能需要一个容器用来运行网盘服务端，还一个容器用来运行数据库服务。而一些复杂的服务可能需要更多的容器。\n同时部署和管理多个容器的技术称为容器编排（Container Orchestration），Docker提供了一个名为docker-compose的工具来实现容器编排。\ndocker-compose安装 # 在Docker客户端发布之初，Docker并不具备编排容器的功能，于是有开发者开发了docker-compose工具来实现容器编排，这个版本的docker-compose被称为docker-compose的第一版（v1）。\n后来Docker公司发布了Docker客户端的新版本，这个版本的Docker客户端具备了编排容器的功能，称为第二版（v2）。\n在Ubuntu 22中，docker-compose可以用apt包管理器安装，使用apt search docker-compose命令可以在apt的源中可以看到有三个版本的docker-compose：\n1 2 3 4 5 6 7 8 9 10 11 $ apt search docker-compose Sorting... Done Full Text Search... Done docker-compose/jammy,jammy,now 1.29.2-1 all [installed] define and run multi-container Docker applications with YAML docker-compose-plugin/jammy 2.24.5-1~ubuntu.22.04~jammy amd64 [upgradable from: 2.24.1-1~ubuntu.22.04~jammy] Docker Compose (V2) plugin for the Docker CLI. docker-compose-v2/jammy-updates 2.20.2+ds1-0ubuntu1~22.04.1 amd64 tool for running multi-container applications on Docker 第1个是docker-compose的第一版，第2个和第3个都是docker-compose的第二版。\n第1个是第一版，我们就不考虑了 第2个是作为docker-cli的插件，使用时的命令是docker compose 第3个是独立的工具，使用时的命令是docker-compose 我们一般选择安装第3个，因为网上很多教程里都用docker-compose命令，而不是docker compose命令。\n现在我推荐使用作为docker插件的docker compose命令，因为它由官方维护，更新速度更快，支持的功能也更多。详情请参见容器（6）：使用Docker时的一些误区、坏习惯和问题中的3. docker-compose 和 docker compose部分。\ndocker-compose使用 # docker-compose使用一个名为docker-compose.yml的文件来定义多个容器的配置，例如我们用Nextcloud部署个人云网盘服务时，使用的docker-compose.yml文件如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 version: ‘3’ services: db: image: mariadb container_name: nextcloud-mariadb networks: - nextcloud_network volumes: - ./db:/var/lib/mysql - /etc/localtime:/etc/localtime:ro environment: - MYSQL_ROOT_PASSWORD=PASSWORD1 - MYSQL_PASSWORD=PASSSWORD2 - MYSQL_DATABASE=nextcloud - MYSQL_USER=nextcloud restart: unless-stopped app: image: nextcloud:latest container_name: nextcloud-app networks: - nextcloud_network ports: - 7080:80 depends_on: - db volumes: - ./nextcloud:/var/www/html - ./app/config:/var/www/html/config - ./app/custom_apps:/var/www/html/custom_apps - ./app/data:/var/www/html/data - ./app/themes:/var/www/html/themes - /etc/localtime:/etc/localtime:ro environment: - VIRTUAL_HOST=your.cloud.domain.name restart: unless-stopped networks: nextcloud_network: 这个文件定义了两个服务：db和app，db服务使用mariadb镜像，app服务使用nextcloud镜像。db服务和app服务都使用了nextcloud_network网络。\n我们可以使用docker-compose命令来启动这两个服务：\n1 docker-compose up -d 如果要停止这两个服务，可以使用docker-compose命令来停止：\n1 docker-compose down Kubernetes / K8s # 上面介绍的docker-compose工具是Docker公司推出的，它的功能比较简单，适合只在单个电脑或者服务器上使用的小型项目。对于包含数个计算机集群的大型项目，docker-compose的功能就显得有些不足了，这时就需要使用Kubernetes（简称K8s）。\nKubernetes（简称K8s，因为K和s之间有8个字母）是一个开源的容器编排引擎，它可以自动化地部署、扩展和管理容器化的应用程序，功能非常强大。\n需要指出的是，容器化的标准由Open Container Initiative（OCI）制定，Docker只是OCI的一种实现。Kubernetes也是OCI的一种实现，它之前使用Docker作为容器运行时，但现在已经不再依赖Docker，而是使用containerd作为容器运行时。\n我暂时也还没有用到Kubernetes，所以这里就不再详细介绍了。\n","date":"2024年1月18日","externalUrl":null,"permalink":"/p/%E5%AE%B9%E5%99%A81%E5%AE%B9%E5%99%A8%E7%9B%B8%E5%85%B3%E7%9F%A5%E8%AF%86%E7%AE%80%E4%BB%8B%E5%AE%B9%E5%99%A8%E5%8C%96dockerdocker-composekubernetes-/-k8s%E7%AD%89/","section":"Posts","summary":" 缘起 # 我自建的网站几乎全部是用Docker部署的，在跟朋友聊起我是如何搭建这些网站时，我发现很多人都表示听说过Docker或者容器，但并不知道它们到底是什么。\n","title":"容器（1）：容器相关知识简介——容器化、docker、docker-compose、Kubernetes / K8s等","type":"post"},{"content":"","date":"2024年1月17日","externalUrl":null,"permalink":"/tags/aws/","section":"Tags","summary":"","title":"AWS","type":"tags"},{"content":"","date":"2024年1月17日","externalUrl":null,"permalink":"/tags/flask/","section":"Tags","summary":"","title":"Flask","type":"tags"},{"content":"","date":"17 一月 2024","externalUrl":null,"permalink":"/en/series/python-web-development-series/","section":"Series","summary":"","title":"Python Web Development Series","type":"series"},{"content":"","date":"2024年1月17日","externalUrl":null,"permalink":"/series/python-web%E5%BC%80%E5%8F%91%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"Python Web开发系列","type":"series"},{"content":" 简介 # 本系列的前四篇文章中，我们使用Python的Flask框架开发了一个简单的天气预报网站，并且可以用Docker把这个网站部署到了私有服务器上，使得我们的网站可以在公网上访问。\n但是并不是所有人都有自己的服务器，为了使网站的部署更加简单，我们可以使用AWS或者Google Cloud这样的云服务提供商提供的云服务器来部署我们的网站。\n前置条件 # 已经会用Flask框架开发简单的Web应用程序（参见\u0026ldquo;Python Web开发学习（一）：使用Flask框架\u0026rdquo;） 已经安装好PostgreSQL数据库，并且会使用SQLAlchemy操作数据库（参见\u0026ldquo;Python Web开发学习（二）：使用PostgreSQL和SQLAlchemy\u0026rdquo; 已经有AWS的账号，并且会使用它们提供的云服务器 已经有一个域名 扩展 # 若想了解如何通过输入框获取用户输入，以及如何使用API获取网络上的信息，请参见\u0026ldquo;Python Web开发学习（三）：使用输入框和API\u0026rdquo; 若想了解如何使用Docker部署网站，请参见\u0026ldquo;Python Web开发学习（四）：使用Docker、Gunicorn和Nginx部署网站\u0026rdquo; 部署到AWS # 总的来说，部署到AWS有以下几个步骤：\n创建AWS账号并获取Access Key和Secret Key 配置AWS命令行工具 创建安全组 创建数据库 创建App的容器镜像 创建App Runner 创建AWS账号并获取Access Key和Secret Key # 首先，我们需要在AWS官网上注册一个账号，然后在控制台中创建一个新的IAM用户。可以在顶端的搜索框中搜索“IAM”找到IAM服务。\n在IAM服务中，选择“用户”，然后点击“添加用户”。\n用户名可以任取，不需要勾选“Enable console access“ 勾选”AdministratorAccess“权限 检查之后选择创建即可 创建成功后进入用户详情页，选择“安全凭证”选项卡，点击“创建访问密钥”。\n创建时勾选“Command Line Interface”选项 点击“Next“按钮创建 找个安全的地方记录Access Key和Secret Key，这两个Key只会显示一次，如果忘记了Secret Key，只能重新创建新的Access Key 配置AWS命令行工具 # 为了方便使用AWS的服务，避免在网页上做那些繁琐的操作，我们可以使用AWS提供的命令行工具来管理我们的云服务器。\n安装AWS命令行工具\n可参见AWS官方文档安装AWS命令行工具，注意选择适合自己操作系统的安装方式。\n安装完成后，可以在命令行中输入aws --version来检查是否安装成功。\n配置AWS命令行工具\n在命令行中输入aws configure，然后输入刚才创建的Access Key和Secret Key，以及默认的区域和输出格式。\n1 2 3 4 5 $ aws configure AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY Default region name [None]: us-west-2 Default output format [None]: json 配置完成后，可以在命令行中输入aws configure list来查看配置信息。\n1 aws configure list 创建安全组 # AWS对于云服务的安全性有很高的要求，我们需要创建一个安全组来控制访问我们的云服务器的规则。这里主要是为了允许从外部通过3306端口访问我们接下来要创建的MySQL数据库。\n在AWS控制台中搜索“security group”，进入安全组页面。\n点击“创建安全组”。\n安全组名称：可以任取，比如“flask-weather” 描述：可以不填 VPC：选择默认的VPC 添加规则：添加规则，允许TCP协议，端口3306，来源为“Anywhere” 最后点击“创建安全组” 创建数据库 # 接下来我们需要在AWS上创建一个数据库，用来存储我们的网站的数据。AWS提供的数据库托管服务叫做RDS。\n在AWS控制台中搜索“RDS”，进入RDS服务页面。\n点击“创建数据库”。\n选择数据库创建方式：选择“标准创建” 选择数据库引擎：选择MySQL 选择数据库实例大小：选择“Free tier” 设置数据库实例标识符、主用户名和密码 设置数据库实例类别、存储、VPC、子网组、安全组等，这里选择刚才创建的安全组 最后点击“创建数据库” 创建数据库需要一些时间，等待数据库创建完成后，可以在数据库的详情页中找到数据库的端点，然后可以使用一些数据库连接工具连接数据库。\n我常用的数据库连接工具是VS Code的插件MySQL，可以在VS Code中直接连接数据库，非常方便。\n图片来源：database-client.com\n如果你使用JetBrains的IDE（如IntelliJ IDEA），也可以使用IDE自带的数据库工具连接数据库。\n创建App的容器镜像 # 现代的Web应用程序一般都是使用容器技术部署的，我们可以使用Docker来创建一个容器镜像，然后把这个镜像部署到AWS提供的App Runner上。\n首先，我们需要在AWS上创建一个ECR（Elastic Container Registry）来存储我们的容器镜像。\n在AWS控制台中搜索“ECR”，进入ECR服务页面。\n点击“创建存储库”。\n存储库名称：可以任取，比如“flask-weather” 标签：可以不填 加密：可以不填 最后点击“创建存储库” 创建完成后记录存储库的URI，后面会用到。\n在本地的项目目录中创建一个Dockerfile文件，用来构建容器镜像。\n1 2 3 4 5 6 7 8 9 10 FROM python:3.9-slim WORKDIR /app COPY requirements.txt requirements.txt RUN pip install -r requirements.txt COPY . . CMD [\u0026#34;gunicorn\u0026#34;, \u0026#34;-b\u0026#34;, \u0026#34;0.0.0.0:5001\u0026#34;, \u0026#34;app:app\u0026#34;] 这个Dockerfile文件的内容和之前使用Docker部署网站的Dockerfile文件内容基本一样，只是这里使用了Python 3.9的slim版本作为基础镜像。\n在项目目录中创建一个requirements.txt文件，用来记录项目的依赖。\n1 2 3 4 5 Flask==2.0.1 Flask-SQLAlchemy==2.5.1 gunicorn==20.1.0 psycopg2-binary==2.9.1 requests==2.26.0 这个文件的内容和之前使用Docker部署网站的requirements.txt文件内容基本一样，只是这里使用了Flask 2.0.1版本。\n在项目目录中创建一个.dockerignore文件，用来忽略一些不需要的文件。\n1 2 3 4 5 6 7 8 9 10 11 __pycache__ *.pyc *.pyo *.pyd .DS_Store .env .venv .git .gitignore .dockerignore .vscode 构建容器镜像\n在项目目录中执行以下命令构建容器镜像。\n1 docker build -t flask-weather . 这个命令会在本地构建一个名为flask-weather的容器镜像。\n推送容器镜像到ECR\n首先，我们需要登录到ECR。\n1 aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-west-2.amazonaws.com 这个命令会获取ECR的登录密码，然后使用Docker登录到ECR。\n接下来，我们需要给容器镜像打上标签。\n1 docker tag flask-weather:latest 123456789012.dkr.ecr.us-west-2.amazonaws.com/flask-weather:latest 这个命令会给本地的flask-weather容器镜像打上标签，然后推送到ECR。\n1 docker push 123456789012.dkr.ecr.us-west-2.amazonaws.com/flask-weather:latest 这个命令会把本地的flask-weather容器镜像推送到ECR。\n推送完成后，可以在ECR的存储库中看到刚才推送的容器镜像。\n创建App Runner # 我们使用AWS提供的App Runner来部署我们的网站。\n在AWS控制台中搜索“App Runner”，进入App Runner服务页面。\n点击“创建服务”。\n选择部署方式：选择“容器” 选择容器镜像：选择刚才推送到ECR的容器镜像 选择端口：选择5001 点击“下一步” 配置服务\n服务名称：可以任取，比如“flask-weather” 环境变量：添加数据库的连接信息，如DATABASE_URL、DATABASE_USER、DATABASE_PASSWORD等 点击“下一步” 配置网络\nVPC：选择默认的VPC 子网：选择默认的子网 安全组：选择刚才创建的安全组 点击“下一步” 配置域名\n域名：可以任取，比如“flask-weather” 点击“下一步” 部署服务\n点击“部署服务” 部署完成后，可以在App Runner的服务页面中看到刚才创建的服务。\n点击服务名称，可以看到服务的详情，包括服务的域名。\n点击服务的域名，可以在浏览器中访问我们的网站。\n定制域名 # 在完成上面的部署之后，我们可以通过AWS App Runner提供的域名来访问我们的网站，但是AWS提供的域名一般比较长，而且会包含一段随机的字符串，不太好记忆，我们可以将在App Runner上部署的网站绑定到我们自己的域名上。\n直接在App Runner上绑定自己的域名会报错“Create Failed”，原因是App Runner需要为我们的域名颁发证书以使用HTTPS，但是我们的域名并没有把Amazon列为受信任的证书颁发机构。我们需要在域名的DNS服务器上添加CAA记录，以便App Runner可以为我们的域名颁发证书。\n证书颁发机构授权（CAA）记录\n在域名的DNS服务器上添加CAA记录，CAA记录的名称选择@，值为amazon.com。这表示Amazon可以为我们的域名下任意的子域名颁发证书。\n在App Runner上绑定域名\n在App Runner的服务页面中，点击服务名称，然后点击“域名”选项卡，点击“绑定域名”。\n域名：输入我们自己的域名 点击“绑定域名” 绑定完成后，可以在浏览器中访问我们的网站。\n","date":"2024年1月17日","externalUrl":null,"permalink":"/p/python-web%E5%BC%80%E5%8F%91%E5%AD%A6%E4%B9%A0%E4%BA%94%E5%9C%A8aws%E4%B8%8A%E9%83%A8%E7%BD%B2%E7%BD%91%E7%AB%99/","section":"Posts","summary":" 简介 # 本系列的前四篇文章中，我们使用Python的Flask框架开发了一个简单的天气预报网站，并且可以用Docker把这个网站部署到了私有服务器上，使得我们的网站可以在公网上访问。\n","title":"Python Web开发学习（五）：在AWS上部署网站","type":"post"},{"content":" 缘起 # 我在多年前上机器学习的课时，使用过TensorFlow，但是当时对深度学习的理解还不够深入，只是简单地使用了一下。后来我也再没有上过机器学习的课，但研究工作中还是用到了一些机器学习的知识，所以我断断续续地学习了一些机器学习的知识。\n现在深度学习已经成为机器学习的主流方法，而PyTorch是一个非常流行的深度学习框架。我之前也了解过PyTorch，但是没有系统地学习过，所以我最近决定系统地学习一下PyTorch。\n尽管网上已经有很多关于PyTorch的教程了，但是我还是想自己再开一个系列“实例学PyTorch”，主要是为了加深自己的理解。这个系列的目标是从PyTorch的基础开始，用PyTorch官方给的实例，来学习用PyTorch实现一些经典的机器学习模型。\nPyTorch官方给了很多实例，包括MNIST手写数字识别、CIFAR-10图像分类、IMDB情感分析等，GitHub仓库地址是https://github.com/pytorch/examples。这些实例都是深度学习的Hello World，非常适合初学者学习。但是PyTorch官方基本只给了代码，没有讲解，对于初学者来说可能不够友好。所以我打算用这个系列来讲解这些实例，希望能帮助初学者更好地学习PyTorch。\n这里我重新建了一个GitHub仓库，其主体是PyTorch官方的示例，但是我会给每个示例添加一些辅助性的代码和文档，方便初学者学习。这个仓库的地址是https://github.com/jin-li/pytorch-tutorial，欢迎大家Star和Fork。\nPyTorch基础 # PyTorch简介 # PyTorch是一个开源的深度学习框架，由Facebook的人工智能研究团队开发。PyTorch提供了两个主要的功能：\n一个多维张量库，类似于NumPy，但是可以在GPU上运行。 一个自动微分引擎，用于构建和训练神经网络。 PyTorch的优点：\nPyTorch是一个动态图框架，可以更灵活地定义神经网络。 PyTorch的API更加Pythonic，更容易学习和使用。 PyTorch的社区更加活跃，有更多的教程和示例。 PyTorch运行环境 # PyTorch支持多种操作系统，包括Linux、Windows和macOS。PyTorch支持多种硬件设备，包括CPU、GPU和TPU。PyTorch支持多种编程语言，包括Python、C++和Java。\n这里我本人的运行环境是一台装有Ubuntu 22.04的台式机，配备了Intel Core i5-9600K处理器和NVIDIA GeForce GTX 4060 Ti显卡。CPU内存为64GB，GPU内存为8GB。但本系列教程中，我会尽量既使用CPU，又使用GPU，既作为性能对比，又方便读者可以在不同的硬件设备上运行。\nPyTorch安装 # PyTorch的安装非常简单，只需要使用pip命令即可。但如果直接用pip安装，可能会弄乱自己的Python环境，所以我们每次都会使用conda创建一个虚拟环境，然后在虚拟环境中运行PyTorch实例。\n关于Python环境的管理，我之前写过一篇文章\u0026ldquo;Python环境管理方式总结\u0026rdquo;，有兴趣的读者可以参考。\nMNIST手写数字识别 # MNIST是一个非常经典的手写数字识别数据集，包含了60000张训练图片和10000张测试图片。每张图片都是28x28像素的灰度图像，标签是0到9之间的一个数字。MNIST已经成为了深度学习，尤其是计算机视觉（Computer Vision, CV）领域的Hello World，几乎所有的深度学习框架都有MNIST的示例。这里我们就从MNIST开始，来开启我们的PyTorch学习之旅。\n流程概述 # 使用PyTorch实现深度学习模型的一般流程如下：\n准备数据集：下载数据集，将数据集转换为PyTorch的数据集。 定义模型：定义神经网络模型，包括网络结构和参数。 训练模型：使用训练数据集训练模型，调整模型参数。 测试模型：使用测试数据集测试模型，评估模型性能。 这里我们就按照这个流程来实现MNIST手写数字识别。\n准备数据集 # 对于MNIST手写数字识别问题，数据集的准备很简单，因为PyTorch已经内置了MNIST数据集。我们只需要使用torchvision.datasets.MNIST类即可，这里我们可以指定train=True表示训练数据集，train=False表示测试数据集。\n1 2 3 4 5 6 7 from torchvision import datasets dataset1 = datasets.MNIST(root=\u0026#39;data\u0026#39;, train=True, download=True) # 训练数据集 dataset2 = datasets.MNIST(root=\u0026#39;data\u0026#39;, train=False, download=True) # 测试数据集 train_loader = torch.utils.data.DataLoader(dataset1, batch_size=64, shuffle=True) test_loader = torch.utils.data.DataLoader(dataset2, batch_size=64, shuffle=False) MNIST数据集是一系列的图片和标签，每张图片是一个28x28的灰度图像，每个标签是一个0到9之间的数字。\n这里我们在用datasets.MNIST()拿到数据集之后，又使用torch.utils.data.DataLoader()将数据集转换为PyTorch的数据集。DataLoader是一个迭代器，可以方便地对数据集进行批处理，这里我们指定batch_size=64表示每次取64个样本，shuffle=True表示每次取样本时打乱顺序。\n定义模型 # 这里我们选择使用神经网络来实现手写数字识别。确定了模型的类型之后，我们需要考虑模型的具体结构，包括网络的层数、每层的神经元数、激活函数等。这些参数的选择跟具体的问题有关，并且非常依赖于经验。\n我们可以先确定输入和输出。显然这个神经网络的输入是28x28的灰度图像，输出是0到9之间的一个数字。\n我们需要选择一个神经网络类型，例如全连接神经网络、卷积神经网络、循环神经网络等。这里我们选择使用全连接神经网络。\n我们需要确定网络的结构，包括网络的层数、每层的神经元数、激活函数等。这里我们选择一个简单的神经网络，包括一个输入层、一个隐藏层和一个输出层。\n创建这个神经网络的代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import torch import torch.nn as nn import torch.nn.functional as F class SimpleNN(nn.Module): def __init__(self): super(SimpleNN, self).__init__() self.fc1 = nn.Linear(28 * 28, 128) # Input layer to hidden layer self.fc2 = nn.Linear(128, 10) # Hidden layer to output layer def forward(self, x): x = x.view(-1, 28 * 28) # Flatten the input image x = F.relu(self.fc1(x)) # Apply ReLU activation x = self.fc2(x) # Output layer return F.log_softmax(x, dim=1) # Apply log-softmax for classification 这里我们定义了一个名为Simple的类，继承自nn.Module。在PyTorch中，所有的神经网络模型都需要继承自nn.Module类，并实现__init__和forward方法。\n在__init__方法中，我们定义了两个全连接层fc1和fc2，分别表示输入层到隐藏层和隐藏层到输出层。nn.Linear表示全连接层，第一个参数表示输入神经元数，第二个参数表示输出神经元数。 在forward方法中，我们定义了网络的前向传播过程，即输入数据经过每一层的计算，最后输出预测结果。其中： x.view(-1, 28 * 28)表示将输入数据展平为一维向量，即将28x28的图像展平为784维向量。 F.relu(self.fc1(x))表示将输入数据输入到第一个全连接层中，然后应用ReLU激活函数。 self.fc2(x)表示将ReLU激活后的数据输入到第二个全连接层中，得到输出结果。 F.log_softmax(x, dim=1)表示将输出结果转换为概率，即对每个类别的输出取对数概率。 训练模型 # 有了数据集和模型之后，我们就可以开始训练模型了。训练模型的原理是通过梯度下降算法，不断调整模型参数，使得模型的预测结果和真实结果之间的误差最小。\n训练模型的一般流程如下：\n初始化模型参数。 从数据集中取出一个批次的数据。 将数据输入到模型中，得到模型的预测结果。 计算模型的预测结果和真实结果之间的误差。 使用梯度下降算法更新模型参数。 重复步骤2到步骤5，直到模型收敛。 其中最为关键的是第4步和第5步，即计算误差和更新参数。PyTorch提供了torch.optim模块来实现梯度下降算法，提供了torch.nn.functional模块来实现损失函数。更新参数的过程是通过反向传播算法实现的，PyTorch提供了loss.backward()方法来计算梯度，提供了optimizer.step()方法来更新参数。\n损失函数和反向传播算法是深度学习的核心，因为损失函数决定了模型的优化目标，反向传播算法决定了如何调整模型参数。\n损失函数 # 损失函数是用来衡量模型的预测结果和真实结果之间的差异，即模型的误差。PyTorch提供了很多常用的损失函数：\n分类问题：交叉熵损失函数torch.nn.CrossEntropyLoss()，负对数似然损失函数torch.nn.NLLLoss()等。 回归问题：均方误差损失函数torch.nn.MSELoss()。 二分类问题：二元交叉熵损失函数torch.nn.BCELoss()。 多标签分类问题：多标签交叉熵损失函数torch.nn.BCEWithLogitsLoss()。 反向传播 # 反向传播算法是用来计算模型参数的梯度，即模型的误差对参数的导数。PyTorch提供了loss.backward()方法来计算梯度，然后使用optimizer.step()方法来更新参数。\n训练代码 # 我们把这个过程封装成一个train函数：\n1 2 3 4 5 6 7 8 9 10 import torch.nn.functional as F def train(args, model, device, train_loader, optimizer, epoch): model.train() for batch_idx, (data, target) in enumerate(train_loader): data, target = data.to(device), target.to(device) optimizer.zero_grad() output = model(data) loss = F.nll_loss(output, target) loss.backward() optimizer.step() model.train()表示将模型设置为训练模式，这样模型中的Dropout层和BatchNorm层会起作用。 optimizer.zero_grad()表示将优化器的梯度清零，因为PyTorch默认会累积梯度。 output = model(data)表示将数据输入到模型中，得到模型的预测结果。 loss = F.nll_loss(output, target)表示计算模型的预测结果和真实结果之间的误差，这里使用负对数似然损失函数。 loss.backward()表示使用反向传播算法计算模型参数的梯度。 optimizer.step()表示使用梯度下降算法更新模型参数。 注意，由于我们事先把数据集转换为PyTorch的数据集，所以每次取出的数据是一个批次的数据，即data是一个张量，对MNIST数据集来说，data的形状是(batch_size, 1, 28, 28)，target的形状是(batch_size,)。\n训练完所有的数据被称为完成一个epoch，我们可以多次迭代训练数据集。但需要注意的是，训练数据集的多次迭代并不一定能提高模型的性能，因为可能会导致过拟合。因此我们需要在训练过程中监控模型的性能，及时停止训练。\n为什么在训练时我们要分批次训练呢？因为一次性训练所有数据可能会导致内存不足，而且分批次训练可以加速训练过程。\n分批次为什么能加速训练呢？因为分批次训练可以利用矩阵乘法的并行性，同时计算多个样本的预测结果和误差，从而加速计算。\n如何选择批次大小呢？批次大小的选择是一个超参数，需要根据具体的问题和硬件设备来选择。一般来说，批次大小越大，训练速度越快，但是内存消耗也越大。批次大小的选择也会影响模型的收敛速度和泛化能力。\n测试模型 # 训练模型之后，我们需要测试模型的性能。测试模型的一般流程如下：\n从测试数据集中取出一个批次的数据。 将数据输入到模型中，得到模型的预测结果。 计算模型的预测结果和真实结果之间的误差。 重复步骤1到步骤3，直到测试数据集遍历完毕。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def test(model, device, test_loader): model.eval() test_loss = 0 correct = 0 with torch.no_grad(): for data, target in test_loader: data, target = data.to(device), target.to(device) output = model(data) test_loss += F.nll_loss(output, target, reduction=\u0026#39;sum\u0026#39;).item() # sum up batch loss pred = output.argmax(dim=1, keepdim=True) # get the index of the max log-probability correct += pred.eq(target.view_as(pred)).sum().item() test_loss /= len(test_loader.dataset) print(\u0026#39;\\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\\n\u0026#39;.format( test_loss, correct, len(test_loader.dataset), 100. * correct / len(test_loader.dataset))) model.eval()表示将模型设置为评估模式，这样模型中的Dropout层和BatchNorm层不会起作用。 with torch.no_grad():表示不需要计算梯度，因为在测试阶段我们只需要计算模型的预测结果，不需要更新模型参数。 这里的误差是整个测试数据集的平均误差。 pred = output.argmax(dim=1, keepdim=True)表示取出预测结果中概率最大的那个类别。 correct += pred.eq(target.view_as(pred)).sum().item()表示计算预测正确的样本数。 主程序 # 有了上面的准备工作，我们就可以开始训练和测试模型了。这里我们定义一个main函数，用来调用train和test函数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def main(): transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) dataset1 = datasets.MNIST(\u0026#39;../data\u0026#39;, train=True, download=True, transform=transform) dataset2 = datasets.MNIST(\u0026#39;../data\u0026#39;, train=False, transform=transform) train_loader = torch.utils.data.DataLoader(dataset1,**train_kwargs) test_loader = torch.utils.data.DataLoader(dataset2, **test_kwargs) model = SimpleNN().to(device) optimizer = optim.Adadelta(model.parameters(), lr=args.lr) for epoch in range(1, args.epochs + 1): train(args, model, device, train_loader, optimizer, epoch) test(model, device, test_loader) transforms.Compose()表示将多个数据转换操作组合在一起。\ntransforms.ToTensor()表示将数据转换为张量。\ntransforms.Normalize()表示对数据进行标准化，即减去均值除以标准差。这里的均值和标准差是MNIST数据集的均值和标准差，可以通过计算得到，具体代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 import torch from torchvision import datasets, transforms # Load the MNIST dataset without any transformations dataset = datasets.MNIST(\u0026#39;../data\u0026#39;, train=True, download=True, transform=transforms.ToTensor()) # Compute the mean and standard deviation loader = torch.utils.data.DataLoader(dataset, batch_size=60000, shuffle=False) data = next(iter(loader))[0] # Get all the images in a single batch mean = data.mean().item() std = data.std().item() print(f\u0026#39;Mean: {mean}, Std: {std}\u0026#39;) 我把这段代码放在了get_mnist_statistics.py文件中，可以直接运行。\nmodel = SimpleNN().to(device)表示将模型移动到指定的设备上，这里我们可以指定CPU或GPU。\n完整代码 # 上面就是实现MNIST手写数字识别所需要的全部主要代码，我把这些代码整合到了一个文件mnist_nn.py中。上面提到的所有代码参见我的GitHub仓库https://github.com/jin-li/pytorch-tutorial中的T01_mnist_nn文件夹。\n除了上面的代码，我还仿照PyTorch官方的示例，定义了一些解析命令行参数的代码，这样我们可以通过命令行来指定一些参数，例如学习率、批次大小、迭代次数等。输入下面的命令，可以查看所有的参数：\n1 python mnist_nn.py --help Python环境 # 在运行代码之前，我们需要创建一个Python虚拟环境，并安装PyTorch和其他依赖包。Python的环境管理工具有很多，可参见我之前写的文章\u0026ldquo;Python环境管理方式总结\u0026rdquo;。这里我使用conda来创建一个给本系列教程用的虚拟环境，并安装PyTorch和其他依赖包。\n1 2 3 conda create -n pytorch-mnist conda activate pytorch-mnist pip install -r requirements.txt 运行代码 # 我们运行这个代码，可以看到模型在测试数据集上的性能：\n1 python mnist_nn.py 上面的代码默认使用GPU来运行，在我的机器上（NVIDIA GeForce GTX 4060 Ti）需要2分15秒左右。如果没有GPU，可以指定--no-cuda参数只使用CPU，在我的机器上（Intel Core i5-9600K）需要3分1秒左右，比使用GPU略慢，但没有慢很多，这是因为我们的模型比较简单，而且MNIST数据集也比较小。\n在运行14个epoch之后，可以得到如下的输出：\n1 Test set: Average loss: 0.1077, Accuracy: 9790/10000 (98%) 即我们这个简单的三层神经网络模型在MNIST数据集上的准确率为98%，这个效果已经非常不错了。\n总结 # 在本篇文章中，我们介绍了PyTorch的基本概念和使用方法，然后使用一个简单的三层全连接神经网络实现了MNIST手写数字识别。这算是深度学习领域的“Hello World\u0026quot;。然而，这个神经网络中的参数都是直接给出的，我们并没有讨论这些参数是怎么来的，也没有讨论这些参数的选择对模型性能的影响。在下一篇文章中，我们将尝试不同的参数选择对模型性能的影响，以及如何调整这些参数来提高模型的性能。\n","date":"2024年1月15日","externalUrl":null,"permalink":"/p/%E5%AE%9E%E4%BE%8B%E5%AD%A6pytorch1mnist%E6%89%8B%E5%86%99%E6%95%B0%E5%AD%97%E8%AF%86%E5%88%AB%E4%B8%80pytorch%E5%9F%BA%E7%A1%80%E5%92%8C%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E7%A1%80/","section":"Posts","summary":" 缘起 # 我在多年前上机器学习的课时，使用过TensorFlow，但是当时对深度学习的理解还不够深入，只是简单地使用了一下。后来我也再没有上过机器学习的课，但研究工作中还是用到了一些机器学习的知识，所以我断断续续地学习了一些机器学习的知识。\n","title":"实例学PyTorch（1）：MNIST手写数字识别（一）——PyTorch基础和神经网络基础","type":"post"},{"content":"","date":"2023年12月9日","externalUrl":null,"permalink":"/tags/shadowsocks/","section":"Tags","summary":"","title":"Shadowsocks","type":"tags"},{"content":" 缘起 # 由于版权问题，在国外无法使用国内很多视频、音乐资源，因此就想搭建一个VPN，以便翻墙回国。于是我在国内的云服务商那里租了一台服务器，然后在上面搭建了Shadowsocks服务，实现了翻墙回国。在这个过程中遇到了不少问题，这里记录一下。\n本文是《私有虚拟网络VPN》系列的第1篇，后续文章会介绍VPN的基础概念，以及如何使用Tailscale来搭建一个私有虚拟网络。本系列的其他文章参见：\n私有虚拟网络VPN（二）：基础概念和使用Tailscale搭建指南 前提 # 一台位于国内的云服务器 安装Shadowsocks # 安装Shadowsocks的过程我主要参考了这篇文章：给小白的Shadowsocks翻墙教程。但依照这篇文章的步骤，我搞了好多次都没有成功，原因是这篇文章里有不少坑，这里主要记录一下是如何解决这些问题的。\n云服务器网络 # 上面安装Shadowsocks的教程需要下载一些位于GitHub上的资源，而由于众所周知的原因，GitHub在国内是无法访问的。因此，我们需要先解决这个问题。即我们想在国外翻墙回国，首先需要让国内的云服务器翻墙出去。\n如果是在国内的普通电脑上，那只需要用一些通用的翻墙软件就行了。但我们的云服务器以命令行的形式运行，因此我们需要在命令行中翻墙。这里有两种方案：\n方案（一）：使用ssh代理 # 首先，因为我们本身就在国外，不需要再为我们在国内的云服务器专门购买一个VPN，只需要利用我们在国外的电脑就行。\n从国外的电脑（以下简称“电脑”）ssh到国内的云服务器（以下简称“云服务器”）上：\n1 ssh username@cloud_server_ip 从云服务器ssh到电脑上，并使用代理端口（端口号可以在1到65535中任选一个，这里我选了11111）：\n1 ssh -D 11111 username@foreign_computer_ip 设置云服务器的网络代理，将代理端口设置为11111：\n1 export ALL_PROXY=socks5://127.0.0.1:11111 这样，经过云服务器的网络请求都会被转发到端口11111，然后通过ssh隧道转发到电脑上，这样当云服务器需要访问国外的网络时，就会通过电脑的网络访问，从而实现了翻墙。\n查看一下云服务器此时的ip地址：\n1 curl ipinfo.io 如果显示的ip地址是电脑的ip地址，那就说明我们成功了。\n然后按照上面的教程中的步骤安装Shadowsocks，但是我发现依然有问题。问题似乎是，教程中安装Shadowsocks的脚本会使用wget下载一些文件，但是wget似乎不会走我们设置的代理，因此依然会提示连接超时。但是curl是可以走我们的代理网络的，因此我们可以把脚本中的wget请求全部换为curl请求，这样就可以解决这个问题了。\n方案（二）：先在本地下载，再上传到云服务器 # 上面的方案设置起来比较复杂，而且可能会出各种问题。其实我之后可能并不会再频繁使用云服务器连接国外的网络，如果只是为了这次的需求，完全可以把需要的文件事先下载下来，然后再上传到云服务器上就行了。\n使用这个方案就无法使用上面教程中的单个命令完成了，我们需要拆分一下这个命令：\n在电脑上下载Shadowsocks安装脚本：\n1 wget --no-check-certificate -O shadowsocks-all.sh https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks-all.sh 将下载好的脚本上传到云服务器上：\n1 scp shadowsocks-all.sh root@cloud_server_ip:/root 在云服务器上安装Shadowsocks：\n1 bash shadowsocks-all.sh 2\u0026gt;\u0026amp;1 | tee shadowsocks-all.log 需要注意的是，现在运行这个安装脚本依然会失败，有两个原因：\n无法连接GitHub下载一些文件 无法找到Python的安装包 Python版本 # 上面的安装脚本需要使用python（python2），但是Ubuntu 22中的apt源中并没有python，而需要显式地指定Python版本，例如明确是python2还是python3。\n因此这里需要做两件事：\n将脚本中apt_depends里的python改为python2、python-dev改为python2-dev 在/usr/bin目录下创建一个软链接，将python指向python2 1 ln -s /usr/bin/python2 /usr/bin/python 然后再按照上面教程中的安装步骤就行了。这里我选择安装的Shadowsocks版本是Shadowsocks-Python。\n完成Shadowsocks安装 # 确认Shadowsocks在正常运行 # 安装完成后，我们可以使用如下命令查看Shadowsocks的运行状态：\n1 /etc/init.d/shadowsocks status 如果显示的是Shadowsocks (pid xxxxx) is running...，那就说明Shadowsocks已经在正常运行了。\n云服务器防火墙开启端口 # 需要在云服务器的防火墙中开启Shadowsocks的端口（默认是8388），否则无法连接到Shadowsocks服务。运行下面的命令：\n1 2 ufw allow 8388 ufw enable 然后使用下面的命令查看防火墙的状态：\n1 ufw status 如果显示的是Status: active，并且Shadowsocks使用的端口处于ALLOW状态，那就说明防火墙已经开启了。\n在云服务器管理界面开启端口 # 对很多云服务器来说，仅在云服务器命令行中开启防火墙是不够的，还需要在云服务器的管理界面中开启端口。在管理界面找到安全组，然后添加一个入方向的规则，将端口设置为Shadowsocks使用的端口（默认是8388），协议设置为TCP， 源地址设置为0.0.0.0/0。\n使用Shadowsocks翻墙回国 # 下载Shadowsocks客户端，然后配置Shadowsocks客户端，将服务器地址设置为云服务器的ip地址，端口设置为Shadowsocks使用的端口（默认是8388），密码设置为Shadowsocks服务的密码，确保加密方式与Shadowsocks服务端的一致。 如果之前的设置都正常的话，现在就可以连接到国内的网络了。\n","date":"2023年12月9日","externalUrl":null,"permalink":"/p/%E7%A7%81%E6%9C%89%E8%99%9A%E6%8B%9F%E7%BD%91%E7%BB%9Cvpn%E4%B8%80%E5%9C%A8%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B8%8A%E7%94%A8shadowsocks%E6%90%AD%E5%BB%BAvpn/","section":"Posts","summary":" 缘起 # 由于版权问题，在国外无法使用国内很多视频、音乐资源，因此就想搭建一个VPN，以便翻墙回国。于是我在国内的云服务商那里租了一台服务器，然后在上面搭建了Shadowsocks服务，实现了翻墙回国。在这个过程中遇到了不少问题，这里记录一下。\n","title":"私有虚拟网络VPN（一）：在云服务器上用Shadowsocks搭建VPN","type":"post"},{"content":"","date":"2023年12月3日","externalUrl":null,"permalink":"/tags/dmarc/","section":"Tags","summary":"","title":"DMARC","type":"tags"},{"content":"","date":"2023年12月3日","externalUrl":null,"permalink":"/tags/email-relay/","section":"Tags","summary":"","title":"Email Relay","type":"tags"},{"content":"","date":"2023年12月3日","externalUrl":null,"permalink":"/tags/mail-server/","section":"Tags","summary":"","title":"Mail Server","type":"tags"},{"content":"","date":"3 十二月 2023","externalUrl":null,"permalink":"/en/series/mail-server-series/","section":"Series","summary":"","title":"Mail Server Series","type":"series"},{"content":"","date":"2023年12月3日","externalUrl":null,"permalink":"/tags/mailcow/","section":"Tags","summary":"","title":"Mailcow","type":"tags"},{"content":" 缘起 # 在本系列的第一篇文章\u0026ldquo;使用mailcow和docker自建私人邮箱服务器\u0026rdquo;中，我们用mailcow和docker在自己的电脑或服务器上搭建了私人邮箱服务器。由于很多运营商都会屏蔽25端口，导致邮件无法从服务器发送出去，因此我们使用了邮箱中继作为替代方案。\n最近我发现邮箱又无法向外界发送邮件了，经过一番测试，发现是AT\u0026amp;T邮箱的邮件中继服务不工作了，似乎是他们的邮件系统出了点问题，我甚至都没法登录到在他们那注册的邮箱att.net邮箱里。每次登录总会跳转到AT\u0026amp;T的页面，然后经过半天加载，弹出一个错误的页面。真是忍不住吐槽，作为一个知名的网络运营商，网站竟可以做得如此之烂，这也是一大奇观了。\n总之，多次尝试之后，我还是决定放弃AT\u0026amp;T的邮件中继服务，转而使用SendGrid 的邮件中继服务。这里记录一下其中遇到的问题以及解决方案。\n前提 # 已使用mailcow和docker搭建好私人邮箱服务器（如果还没有，可参见\u0026ldquo;使用mailcow和docker自建私人邮箱服务器\u0026rdquo;） SendGrid设置 # 注册SendGrid账户 # 在SendGrid官网注册账户，除了注册网站通用的那些信息，SendGrid还需要填写公司信息，可以随意填一下。最后需要使用手机号设置两步验证。 套餐我选择的免费版，每天最多可以发送100封邮件，对于我来说已经足够了。\n生成API Key # 注册完成后，登录进SendGrid的管理界面，点击左侧菜单栏的Email API，选择Integration Guide，然后选择SMTP Relay，点击Get Started，\n然后填写API Key的名字，点击Create API Key，最后勾选I've updated my settings.， 并点击Next: Verify Integration。\n邮箱中继（邮箱转发） # 邮箱中继的设置可参见上一篇文章\u0026ldquo;使用mailcow和docker自建私人邮箱服务器\u0026rdquo;中介绍的步骤，这里不再赘述。\n这里需要注意的是，如果在其他网络应用中使用SendGrid的邮件中继服务，需要将Relayhost设置为smtp.sendgrid.net:587，而不是smtp.mail.your_domain_name，用户名就是\u0026quot;apikey\u0026quot;，密码就是上一步中生成的API Key。\n邮件被屏蔽的问题 # 被屏蔽的原因 # 当我在自己的一个网络应用中使用SendGrid的邮件中继服务时，发现等了好久也没有收到邮件，然后我在SendGrid的管理界面查看最近的活动记录：\n点击未能送达的这条记录，可以看到邮件被屏蔽的原因是550 DMARC check failed：\n在QQ邮箱中可以查看被屏蔽的邮件，只需右上角自己的头像，然后选择自助查询，然后点击收信查询这一栏，就可以看到被屏蔽的邮件：\n在网上查询后得知，出现这个问题的原因是，我的邮件是用SendGrid的邮件中继服务发送的，但邮件的发件人却是我的自建邮箱服务器，也就是说邮件的实际发件人和号称的发件人不一致。接收邮件的服务器查询发件人的DMARC记录时，发现不一致，觉得有“挂羊头卖狗肉”的嫌疑，因此很大概率会拒绝接收邮件。\n其实这点在接收邮件时也可以看出来，点击查看发件人的详细，可以看到实际发件人的域名是sendgrid.net，而号称的发件人的域名是jinli.io：\n解决方案 # 上面问题的解决方案就是在SendGrid的管理系统中认证自己的域名。点击左上角自己的用户名，选择Setup Guide，然后完成其中的第二步Authenticate your sending domain。SendGrid会要求你添加3条CNAME记录在自己域名的DNS服务器上，添加完成后点击验证就行了。\n完成验证后可以再用你的邮箱发邮件测试一下，这次SendGrid会使用你的域名生成一个发件人地址，然后用这个用户来发送邮件，这样实际发件人的域名和名义发件人的域名就一致了，可以顺利通过收件服务器的DMARC检查。\n总结 # 自建邮件服务器的过程其实并不复杂，接受邮件一般问题也不大，真正的问题是如何发送邮件。这里使用SendGrid的邮件中继服务来解决发送邮件的问题，虽然有点麻烦，但总算是也是一个免费解决问题的方案。\n自建邮件服务器其实是个挺花精力的事情，尤其是邮件中继的问题。网上其实也有挺多讨论，有些网友也提出了一些免费的或者较为廉价的解决方案（约$10每年），可参见Reddit的讨论。目前我使用mailcow和SendGrid似乎还没啥问题，将来如果出了问题，我也可能会转向这个帖子里提到的方案。\n","date":"2023年12月3日","externalUrl":null,"permalink":"/p/%E4%BD%BF%E7%94%A8mailcow%E5%92%8Cdocker%E8%87%AA%E5%BB%BA%E7%A7%81%E4%BA%BA%E9%82%AE%E7%AE%B1%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%BA%8C%E4%BD%BF%E7%94%A8%E9%82%AE%E7%AE%B1%E4%B8%AD%E7%BB%A7%E4%B8%8E%E9%99%8D%E4%BD%8E%E8%A2%AB%E5%B1%8F%E8%94%BD%E7%9A%84%E6%A6%82%E7%8E%87/","section":"Posts","summary":" 缘起 # 在本系列的第一篇文章“使用mailcow和docker自建私人邮箱服务器”中，我们用mailcow和docker在自己的电脑或服务器上搭建了私人邮箱服务器。由于很多运营商都会屏蔽25端口，导致邮件无法从服务器发送出去，因此我们使用了邮箱中继作为替代方案。\n","title":"使用mailcow和docker自建私人邮箱服务器（二）——使用邮箱中继与降低被屏蔽的概率","type":"post"},{"content":"","date":"2023年12月3日","externalUrl":null,"permalink":"/series/%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%B3%BB%E5%88%97/","section":"Series","summary":"","title":"邮件服务器系列","type":"series"},{"content":"","date":"2023年11月21日","externalUrl":null,"permalink":"/tags/api/","section":"Tags","summary":"","title":"API","type":"tags"},{"content":"","date":"21 十一月 2023","externalUrl":null,"permalink":"/en/categories/programming/","section":"Categories","summary":"","title":"Programming","type":"categories"},{"content":" 简介 # 在本系列的前三篇文章中，我们使用Python的Flask框架开发了一个简单的天气预报网站，到目前为止，我们的网站还仅限于在自己的电脑上访问。在这篇文章中，我们将使用Docker、Gunicorn和Nginx部署这个网站，使得我们的网站可以在公网上访问。\n前置条件 # 已经会用Flask框架开发简单的Web应用程序（参见\u0026ldquo;Python Web开发学习（一）：使用Flask框架\u0026rdquo;） 已经安装好PostgreSQL数据库，并且会使用SQLAlchemy操作数据库（参见\u0026ldquo;Python Web开发学习（二）：使用PostgreSQL和SQLAlchemy\u0026rdquo; 已经安装好Docker、docker-compose 已经有一个域名 项目结构（开发版） # 到上一篇文章\u0026ldquo;Python Web开发学习（三）：使用输入框和API\u0026rdquo;为止，我们的项目结构如下所示：\n1 2 3 4 5 6 learn_flask ├── app.py ├── static │ └── style.css └── templates └── weather.html 在这篇文章中，我们将使用Docker和Gunicorn部署这个网站，因此我们需要添加一些文件。为了方便起见，我们将项目调整为如下所示的结构：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 learn_flask ├── docker-compose.yml ├── .env ├── .env.db └── learn_flask ├── app │ ├── config.py │ ├── __init__.py │ ├── static │ │ └── style.css │ └── templates │ └── weather.html ├── Dockerfile ├── manage.py └── requirements.txt 接下来我们将逐步创建这些文件，并介绍这些文件的作用。\n添加依赖 # 我们在开发这个网络应用前，已经定义了一个Python虚拟环境，其中包含了我们需要的依赖。我们可以使用如下命令将当前环境中的依赖导出到requirements.txt文件中：\n1 pip freeze \u0026gt; requirements.txt 不知道为什么里面会包含一些不需要的依赖，例如click、itsdangerous、Jinja2、MarkupSafe、Werkzeug，我们可以手动删除这些依赖。最后，我们的requirements.txt文件如下所示：\n1 2 3 4 5 Flask==3.0.0 Flask-SQLAlchemy==3.1.1 Jinja2==3.1.2 gunicorn==20.1.0 psycopg2-binary==2.9.9 将应用包装为Python包 # 我们将原来的app.py文件重命名为__init__.py，并将其移动到learn_flask/app目录下。\n然后我们可以在learn_flask/app目录下创建一个config.py文件，用来存储我们的数据库配置信息：\n1 2 3 4 5 6 7 import os basedir = os.path.abspath(os.path.dirname(__file__)) class Config(object): SQLALCHEMY_DATABASE_URI = os.getenv(\u0026#34;DATABASE_URL\u0026#34;, \u0026#34;sqlite://\u0026#34;) SQLALCHEMY_TRACK_MODIFICATIONS = False 这样我们可以删去__init__.py中的数据库配置信息：\n1 2 app.config[\u0026#39;SQLALCHEMY_DATABASE_URI\u0026#39;] = \u0026#39;postgresql://test:test_password@localhost:5432/weather_db\u0026#39; app.config[\u0026#39;SQLALCHEMY_TRACK_MODIFICATIONS\u0026#39;] = False 我们在app目录的外面创建一个manage.py文件，用来管理我们的应用程序。我们可以在manage.py中添加如下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from flask.cli import FlaskGroup from sqlalchemy.exc import ProgrammingError from app import app cli = FlaskGroup(app) @cli.command(\u0026#34;create_db\u0026#34;) def create_db(): from app import db from app import Weather with app.app_context(): try: db.create_all() db.session.commit() except ProgrammingError: pass @cli.command(\u0026#34;drop_db\u0026#34;) def drop_db(): from app import db from app import Weather with app.app_context(): try: db.drop_all() db.session.commit() except ProgrammingError: pass if __name__ == \u0026#34;__main__\u0026#34;: cli() 这里我们使用FlaskGroup来管理我们的应用程序，其中create_db和drop_db命令用来创建和删除数据库。这里我们使用ProgrammingError来判断数据库是否存在，如果数据库不存在，则不会删除数据库。\n然后我们可以删除__init__.py中的数据库创建和删除的代码：\n1 2 3 4 5 6 7 8 if __name__ == \u0026#39;__main__\u0026#39;: with app.app_context(): try: db.create_all() db.session.commit() except ProgrammingError: pass app.run() 使用Docker部署应用 # 创建Dockerfile # 我们在learn_flask目录下创建一个Dockerfile文件，用来构建我们的应用程序。我们可以在Dockerfile中添加如下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # pull official base image FROM python:3.11.4-slim-buster as builder # set work directory WORKDIR /app # Copy the current directory contents into the container at /app COPY . /app # Install any needed packages specified in requirements.txt COPY ./requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Run app.py when the container launches CMD [\u0026#34;gunicorn\u0026#34;, \u0026#34;-b\u0026#34;, \u0026#34;0.0.0.0:5001\u0026#34;, \u0026#34;app:app\u0026#34;] 其中各个命令的作用已经标注在了注释中。\n创建docker-compose.yml # 我们在最外层的learn_flask目录下创建一个docker-compose.yml文件，用来管理我们的应用程序。我们可以在docker-compose.yml中添加如下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 version: \u0026#34;3.8\u0026#34; services: learn_flask: build: ./learn_flask command: python manage.py run -h 0.0.0.0 volumes: - ./learn_flask:/app/ ports: - 5001:5000 env_file: - ./.env depends_on: - db db: image: postgres:13 volumes: - ./postgres_data_prod:/var/lib/postgresql/data/ env_file: - ./.env.db volumes: postgres_data: 在这个docker-compose.yml文件中，我们定义了两个服务，一个是应用程序learn_flask，另一个是数据库db。\n创建.env文件 # 上面的docker-compose.yml文件中，我们使用了.env文件和.env.db文件，我们可以在最外层的learn_flask目录下创建这两个文件，用来存储我们的环境变量。我们可以在.env文件中添加如下环境变量：\n1 2 3 4 5 6 FLASK_APP=app/__init__.py FLASK_DEBUG=1 DATABASE_URL=postgresql://your_postgre_user_name:your_postgres_password@db:5432/weather_db SQL_HOST=db SQL_PORT=5432 DATABASE=postgres 在.env.db文件中添加数据库的环境变量：\n1 2 3 POSTGRES_USER=your_postgre_user_name POSTGRES_PASSWORD=your_postgres_password POSTGRES_DB=weather_db 部署应用（开发版） # 构建镜像 # 第一次构建镜像时，我们的应用程序还没有创建数据库，因此我们需要先创建数据库。需要将docker-compose.yml中的command改为如下代码：\n1 2 3 4 command: \u0026gt; sh -c \u0026#34;python manage.py drop_db \u0026amp;\u0026amp; python manage.py create_db \u0026amp;\u0026amp; python manage.py run -h 0.0.0.0\u0026#34; 然后我们可以使用如下命令构建镜像：\n1 docker-compose up -d --build 如果一切正常，在浏览器中输入http://localhost:5001，就可以看到我们的网站了。\n部署应用 # 在第一次构建成功后，我们可以暂时停止应用程序，然后将docker-compose.yml中的command改回原来的命令：\n1 command: python manage.py run -h 0.0.0.0 然后我们可以使用如下命令部署应用：\n1 docker-compose up -d 部署应用（生产版） # 根据上面的步骤完成部署后，我们的程序仍然是在开发模式下运行的，查看docker里面的日志，我们可以看到如下的提示信息：\n1 WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. 这提示我们上面的部署对于生产环境来说是不够的，因此我们还需要再做一些调整，使得我们的应用程序可以在生产环境下运行。\ndocker-compose # 首先我们为生产环境的部署创建一个新的docker-compose.yml文件，我们可以在最外层的learn_flask目录下创建一个docker-compose.prod.yml文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 version: \u0026#34;3.8\u0026#34; services: learn_flask: build: context: ./learn_flask dockerfile: Dockerfile.prod command: gunicorn --bind 0.0.0.0:5000 manage:app ports: - 5001:5000 env_file: - ./.env.prod depends_on: - db db: image: postgres:13 volumes: - postgres_data_prod:/var/lib/postgresql/data/ env_file: - ./.env.prod.db volumes: postgres_data_prod: 注意，这里我们不再为应用程序指定volumes，因为我们不再需要将应用程序的代码挂载到容器中，而是将应用程序打包到镜像中。\nDockerfile # 上面的docker-compose.prod.yml文件中，我们使用了一个新的Dockerfile.prod文件，我们可以在learn_flask/learn_flask目录下创建一个Dockerfile.prod文件，用来构建我们的应用程序镜像：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 ########### # BUILDER # ########### # pull official base image FROM python:3.11.3-slim-buster as builder # set work directory WORKDIR /usr/src/app # set environment variables ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 # install system dependencies RUN apt-get update \u0026amp;\u0026amp; \\ apt-get install -y --no-install-recommends gcc # lint RUN pip install --upgrade pip # RUN pip install flake8==6.0.0 COPY . /usr/src/app/ # RUN flake8 --ignore=E501,F401 . # install python dependencies COPY ./requirements.txt . RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt ######### # FINAL # ######### # pull official base image FROM python:3.11.3-slim-buster # create directory for the app user RUN mkdir -p /home/app # create the app user RUN addgroup --system app \u0026amp;\u0026amp; adduser --system --group app # create the appropriate directories ENV HOME=/home/app ENV APP_HOME=/home/app/learn_flask RUN mkdir $APP_HOME WORKDIR $APP_HOME # install dependencies RUN apt-get update \u0026amp;\u0026amp; apt-get install -y --no-install-recommends netcat COPY --from=builder /usr/src/app/wheels /wheels COPY --from=builder /usr/src/app/requirements.txt . RUN pip install --upgrade pip RUN pip install --no-cache /wheels/* # copy entrypoint-prod.sh COPY ./entrypoint.prod.sh $APP_HOME # copy project COPY . $APP_HOME # chown all the files to the app user RUN chown -R app:app $APP_HOME # change to the app user USER app # run entrypoint.prod.sh ENTRYPOINT [\u0026#34;/home/app/learn_flask/entrypoint.prod.sh\u0026#34;] entrypoint # 上面的Dockerfile.prod文件中，我们使用了一个entrypoint.prod.sh文件，我们可以在learn_flask/learn_flask目录下创建一个entrypoint.prod.sh文件，用来运行我们的应用程序：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #!/bin/sh if [ \u0026#34;$DATABASE\u0026#34; = \u0026#34;postgres\u0026#34; ] then echo \u0026#34;Waiting for postgres...\u0026#34; while ! nc -z $SQL_HOST $SQL_PORT; do sleep 0.1 done echo \u0026#34;PostgreSQL started\u0026#34; fi if [ \u0026#34;$FLASK_DEBUG\u0026#34; = \u0026#34;1\u0026#34; ] then echo \u0026#34;Creating the database tables...\u0026#34; python manage.py create_db echo \u0026#34;Tables created\u0026#34; fi exec \u0026#34;$@\u0026#34; 注意：在创建好entrypoint.prod.sh文件后，我们需要将其权限改为可执行，否则之后运行docker时会报错：\n1 chmod +x entrypoint.prod.sh 环境变量 # 和在开发环境中部署类似，我们需要创建.env.prod和.env.prod.db文件，用来存储用于生产环境的环境变量。 我们可以在.env.prod文件中添加如下环境变量：\n1 2 3 4 5 6 FLASK_APP=app/__init__.py FLASK_DEBUG=0 DATABASE_URL=postgresql://your_postgre_user_name:your_postgres_password@db:5432/weather_db_prod SQL_HOST=db SQL_PORT=5432 DATABASE=postgres 在.env.prod.db文件中添加数据库的环境变量：\n1 2 3 POSTGRES_USER=your_postgre_user_name POSTGRES_PASSWORD=your_postgres_password POSTGRES_DB=weather_db_prod 构建镜像 # 我们可以使用如下命令构建镜像：\n1 sudo docker-compose -f docker-compose.prod.yml up -d --build 这时我们只构建了镜像，数据库还是空的，因此我们需要创建数据库。我们可以使用如下命令在容器中创建数据库：\n1 sudo docker-compose -f docker-compose.prod.yml exec learn_flask python manage.py create_db 然后我们可以在浏览器中输入http://localhost:5001，就可以看到我们的网站了。\n生产版项目结构 # 最终我们用于生产环境的目录结构如下所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 learn_flask ├── docker-compose.prod.yml ├── env.prod ├── env.prod.db └── learn_flask ├── app │ ├── config.py │ ├── __init__.py │ ├── static │ │ └── style.css │ └── templates │ └── weather.html ├── Dockerfile.prod ├── entrypoint.prod.sh ├── manage.py └── requirements.txt 使用Nginx反向代理 # 完成上面的部署后，我们仍然只能在本地访问我们的网站，如果我们想要在公网上访问，我们需要使用Nginx反向代理。由于我的服务器上已经安装了Nginx，因此我只需要在服务器上添加一个配置文件即可。我们可以在服务器上的/etc/nginx/sites-available目录下创建一个learn_flask.conf文件，用来存储我们的配置信息。我们可以在learn_flask.conf文件中添加如下代码：\n1 2 3 4 5 6 7 8 server { listen 80; server_name your_domain_name; location / { proxy_pass http://localhost:5001; } } 然后我们还可以用acme.sh生成一个SSL证书，用来支持HTTPS。acme.sh的安装和使用可参见个人网站的建立过程（二）：使用Hugo框架搭建个人网站。\n上面的nginx配置文件也需要做一些修改，我们可以在learn_flask文件中添加如下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 upstream learn_flask.jinli.io { server 127.0.0.1:5001; } server { listen 80; server_name learn_flask.jinli.io; return 301 https://learn_flask.jinli.io$request_uri; } server { listen 443 ssl; server_name learn_flask.jinli.io; ssl_certificate /media/lijin/learn_flask/cert/cert.pem; ssl_certificate_key /media/lijin/learn_flask/cert/key.pem; location / { proxy_redirect off; proxy_pass http://learn_flask.jinli.io; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Ssl on; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Frame-Options SAMEORIGIN; client_max_body_size 100m; } } 然后我们可以使用如下命令激活这个配置文件：\n1 sudo service nginx reload ","date":"2023年11月21日","externalUrl":null,"permalink":"/p/python-web%E5%BC%80%E5%8F%91%E5%AD%A6%E4%B9%A0%E5%9B%9B%E4%BD%BF%E7%94%A8dockergunicorn%E5%92%8Cnginx%E9%83%A8%E7%BD%B2%E7%BD%91%E7%AB%99/","section":"Posts","summary":" 简介 # 在本系列的前三篇文章中，我们使用Python的Flask框架开发了一个简单的天气预报网站，到目前为止，我们的网站还仅限于在自己的电脑上访问。在这篇文章中，我们将使用Docker、Gunicorn和Nginx部署这个网站，使得我们的网站可以在公网上访问。\n","title":"Python Web开发学习（四）：使用Docker、Gunicorn和Nginx部署网站","type":"post"},{"content":"","date":"21 十一月 2023","externalUrl":null,"permalink":"/en/categories/web/","section":"Categories","summary":"","title":"Web","type":"categories"},{"content":"","date":"2023年11月21日","externalUrl":null,"permalink":"/categories/%E7%BC%96%E7%A8%8B/","section":"Categories","summary":"","title":"编程","type":"categories"},{"content":" 简介 # 这是本系列文章的第三篇，主要介绍如何使用输入框获取用户输入，以及如何使用API获取网络上的信息。\n前置条件 # 已经会用Flask框架开发简单的Web应用程序（参见\u0026ldquo;Python Web开发学习（一）：使用Flask框架\u0026rdquo;） 已经安装好PostgreSQL数据库，并且会使用SQLAlchemy操作数据库（参见\u0026ldquo;Python Web开发学习（二）：使用PostgreSQL和SQLAlchemy\u0026rdquo; 获取用户输入 # 添加输入框 # 首先，我们可以在HTML页面中使用\u0026lt;input\u0026gt;标签来创建输入框。例如，我们可以在templates/index.html中添加一个表单，让用户输入一个城市：\n1 2 3 4 5 6 7 8 9 \u0026lt;section class=\u0026#34;top-banner\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;h1 class=\u0026#34;heading\u0026#34;\u0026gt;Weather App\u0026lt;/h1\u0026gt; \u0026lt;form method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;city\u0026#34; placeholder=\u0026#34;Enter a city\u0026#34; autocomplete=\u0026#34;off\u0026#34; required\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;Search\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/section\u0026gt; 其中输入框的样式来自style.css文件。这个样式文件来自于https://webdesign.tutsplus.com/build-a-simple-weather-app-with-vanilla-javascript\u0026ndash;cms-33893t，其中输入框的效果如下所示：\n获取用户输入 # 当用户在输入框中输入城市名称并点击“Search”按钮或敲击回车时，用户的输入会传递到\u0026lt;input\u0026gt;标签中name定义的名称中。我们可以在app.py中添加一个POST请求的路由，用来处理用户的输入。由于我们之前已经为/路径添加了路由，这里只需要为/路径添加一个POST的请求：\n1 2 3 4 5 6 7 @app.route(\u0026#39;/\u0026#39;, methods=[\u0026#39;POST\u0026#39;, \u0026#39;GET\u0026#39;]) def index(): if request.method == \u0026#39;POST\u0026#39;: city_name = request.form.get(\u0026#39;city\u0026#39;) else: #for default name city_name = \u0026#39;Beijing\u0026#39; 这里我们使用request.form.get('city')来获取用户输入的城市名称，将其赋值给变量city_name。\n使用API获取天气信息 # OpenWeather API # OpenWeather是一个提供天气信息的网站，用户在注册账户后，可以使用OpenWeather提供的API来获取天气信息。我们可以在https://openweathermap.org/注册一个账户，然后在https://home.openweathermap.org/api_keys页面获取API Key。这里我们使用的是免费版的API Key，每分钟最多可以发送60个请求，每天最多可以发送1000个请求。免费版对我来说已经够用了。\n通过API获取天气信息 # 在通过输入框获取了city_name之后，我们就可以使用OpenWeather提供的API来获取天气信息了。我们可以在https://openweathermap.org/current页面查看API的使用方法。这里我们使用的是By city name的API，即通过城市名称来获取天气信息。我们可以在app.py中添加一个函数，用来获取天气信息：\n1 2 3 4 def get_weather(city_name): url = \u0026#39;http://api.openweathermap.org/data/2.5/weather?q={}\u0026amp;units=metric\u0026amp;appid={}\u0026#39;.format(city_name, API_KEY) response = urlopen(url).read() return json.loads(response) 注意：\n这里的API_KEY是我们在OpenWeather网站上获取的API Key，需要将其赋值给变量API_KEY。\n在之前获取的城市名称中，可能包含空格，但是OpenWeather的API不支持空格，OpenWeather API中的空格都是用+替代的。因此我们需要将城市名称中的空格替换为+，并将城市名称转换为小写。例如New York会被转换为new+york。我们可以使用如下的正则表达式来实现这个功能：\n1 re.sub(r\u0026#34;\\s+\u0026#34;, \u0026#39;+\u0026#39;, str(city_name).lower()) 处理获取的天气信息 # 上一步中，我们通过OpenWeather的API获取了天气信息，但是这些信息是以JSON格式返回的。在Python中，我们可以以字典的形式来取用JSON中的数据，并将其存储到数据库中。至于JSON中的天气信息内容，可以在https://openweathermap.org/current#current_JSON\n1 2 3 4 5 6 7 8 9 10 11 data = get_weather(city_name) db.session.add(Weather( name = data[\u0026#39;name\u0026#39;], country = data[\u0026#39;sys\u0026#39;][\u0026#39;country\u0026#39;], temp = round(data[\u0026#39;main\u0026#39;][\u0026#39;temp\u0026#39;]), feels_like = round(data[\u0026#39;main\u0026#39;][\u0026#39;feels_like\u0026#39;]), icon = data[\u0026#39;weather\u0026#39;][0][\u0026#39;icon\u0026#39;], description = data[\u0026#39;weather\u0026#39;][0][\u0026#39;description\u0026#39;], )) db.session.commit() 其他部分跟之前的代码一样，从数据库获取想要展示的天气信息，然后传递给模板就行了。效果如下： 展示最新的天气信息 # 最后我们再加入一点儿稍微复杂的数据库操作，例如展示最新的天气信息。我们可以在app.py中添加一段代码，在数据库中选择最新的4条天气信息，然后在页面中展示出来：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 subquery = ( db.session.query( Weather.name, func.max(Weather.time).label(\u0026#39;max_time\u0026#39;) ) .group_by(Weather.name) .order_by(func.max(Weather.time).desc()) .limit(4) .subquery() ) query = ( db.session.query(Weather) .join(subquery, and_( Weather.name == subquery.c.name, Weather.time == subquery.c.max_time )) .order_by(Weather.time.desc()) ) wd = query.all() 注意，这里我们用了SQLAlchemy的子查询，因此需要导入from sqlalchemy.sql import func, and_。最后，我们在运行整个应用程序时，先创建数据库，如果数据库已存在，则不会创建：\n1 2 3 4 5 6 7 if __name__ == \u0026#39;__main__\u0026#39;: with app.app_context(): try: db.create_all() except ProgrammingError: pass app.run() 这里同样需要导入相关的模块from sqlalchemy.exc import ProgrammingError。\n","date":"2023年11月18日","externalUrl":null,"permalink":"/p/python-web%E5%BC%80%E5%8F%91%E5%AD%A6%E4%B9%A0%E4%B8%89%E4%BD%BF%E7%94%A8%E8%BE%93%E5%85%A5%E6%A1%86%E5%92%8Capi/","section":"Posts","summary":" 简介 # 这是本系列文章的第三篇，主要介绍如何使用输入框获取用户输入，以及如何使用API获取网络上的信息。\n","title":"Python Web开发学习（三）：使用输入框和API","type":"post"},{"content":"","date":"2023年11月17日","externalUrl":null,"permalink":"/tags/database/","section":"Tags","summary":"","title":"Database","type":"tags"},{"content":"","date":"2023年11月17日","externalUrl":null,"permalink":"/tags/postgresql/","section":"Tags","summary":"","title":"PostgreSQL","type":"tags"},{"content":" 缘起 # 在本系列的上一篇文章\u0026ldquo;Python Web开发学习（一）：使用Flask框架\u0026rdquo;中，我们使用Flask框架开发了一个简单的Web应用程序，来显示一些城市的天气信息。但是这些天气信息都是写死在代码中的，如果我们有许多的天气信息，再写死在代码中就不太方便了。因此我们需要一个数据库来存储这些天气信息，然后从数据库中读取这些天气信息，再显示在网页上。\n本文将介绍如何使用PostgreSQL数据库和SQLAlchemy操作数据库，来实现从数据库中读取天气信息。\n前置条件 # 在开始本文之前，确保你已经清楚了如何用Flask框架开发一个简单的Web应用程序，如果不清楚，可以参考本系列的上一篇文章\u0026ldquo;Python Web开发学习（一）：使用Flask框架\u0026rdquo;。\nPostgreSQL # PostgreSQL简介 # PostgreSQL是一个开源的关系型数据库管理系统，它的特点是功能强大、可扩展性好、支持SQL标准、支持事务、支持复杂查询、支持多种编程语言、支持多种操作系统。PostgreSQL是最流行的关系型数据库管理系统之一，因此这里我们选择使用PostgreSQL数据库来作为Web应用后端的数据库。\n安装配置PostgreSQL # 在Ubuntu 22中安装PostgreSQL非常简单，只需要在终端中运行以下命令即可：\n1 sudo apt install postgresql 使用下面的命令可以查看安装的PostgreSQL的版本：\n1 psql --version 安装好之后，PostgreSQL会自动创建一个名为postgres的用户，这个用户是超级用户，可以用来管理数据库。我们可以使用下面的命令来切换到postgres用户：\n1 sudo su postgres 登录后我们以postgres用户的身份登录到系统的命令后中。然后运行下面的命令来登录到PostgreSQL数据库：\n1 psql 下面是PostgreSQL中常用的命令：\n\\l：列出所有的数据库 \\?：列出所有的命令 \\c \u0026lt;database_name\u0026gt;：连接到指定的数据库 \\d：列出当前数据库中的所有表 \\d \u0026lt;table_name\u0026gt;：列出指定表的结构 \\q：退出PostgreSQL \\du：列出所有的用户 \\du+：列出所有的用户和用户的详细信息 \\password \u0026lt;user_name\u0026gt;：修改指定用户的密码 \\conninfo：显示当前连接的数据库信息 使用\\du列出所有用户，可以发现目前只有一个用户postgres。我们需要创建一个新的用户，用来管理我们的数据库。使用下面的命令创建一个新的用户：\n1 CREATE USER test WITH PASSWORD \u0026#39;test_password\u0026#39;; 这里的test是用户名，test是密码。然后使用下面的命令给这个用户授予超级用户权限：\n1 ALTER USER test WITH SUPERUSER; 然后使用下面的命令退出PostgreSQL：\n1 \\q 然后使用下面的命令退出postgres用户：\n1 exit 然后我们重新用新建的用户test登录到PostgreSQL数据库：\n1 psql -U test 这时可能会遇到下面的报错信息：\n1 FATAL: Peer authentication failed for user \u0026#34;test\u0026#34; 解决这个问题需要修改一下PostgreSQL的配置文件（参考https://zhuanlan.zhihu.com/p/467644334），使用下面的命令打开PostgreSQL的配置文件：\n1 sudo vim /etc/postgresql/14/main/pg_hba.conf 其中14是PostgreSQL的版本号，如果你的版本号不是14，请修改为你的版本号。在配置文件的最后可以看到这样的几行信息：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # Database administrative login by Unix domain socket local all postgres peer # TYPE DATABASE USER ADDRESS METHOD # \u0026#34;local\u0026#34; is for Unix domain socket connections only local all all peer # IPv4 local connections: host all all 127.0.0.1/32 scram-sha-256 # IPv6 local connections: host all all ::1/128 scram-sha-256 # Allow replication connections from localhost, by a user with the # replication privilege. local replication all peer host replication all 127.0.0.1/32 scram-sha-256 host replication all ::1/128 scram-sha-256 我们需要把local all all peer这一行的peer改为md5，然后保存退出。最后使用下面的命令重启PostgreSQL：\n1 sudo service postgresql restart 这时再重新尝试登录到PostgreSQL，就不会报错了：\n1 psql -U test 最后为我们的Web应用创建一个新的数据库：\n1 CREATE DATABASE weather_db; 这里的weather_db是数据库的名称。然后使用下面的命令退出PostgreSQL：\n1 \\q 在Flask中使用PostgreSQL # 安装psycopg2和SQLAlchemy # psycopg2是一个用于连接PostgreSQL数据库的Python库，SQLAlchemy是一个用于操作数据库的Python库。我们需要安装这两个库。先确保我们已经激活了Python虚拟环境，然后使用下面的命令：\n1 2 pip install psycopg2-binary pip install SQLAlchemy 创建数据库模型 # 在Flask应用中连接PostgreSQL数据库，使用下面的代码：\n1 2 3 4 5 6 7 from flask import Flask from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) app.config[\u0026#39;SQLALCHEMY_DATABASE_URI\u0026#39;] = \u0026#39;postgresql://test:test_password@localhost:5432/weather_db\u0026#39; app.config[\u0026#39;SQLALCHEMY_TRACK_MODIFICATIONS\u0026#39;] = False db = SQLAlchemy(app) 上面的代码中，我们首先导入Flask类和SQLAlchemy类，然后创建一个Flask应用程序，然后使用app.config来配置数据库的连接信息，其中SQLALCHEMY_DATABASE_URI是数据库的连接地址，SQLALCHEMY_TRACK_MODIFICATIONS是用来关闭对模型修改的监控，因为这个功能会消耗额外的内存，但是我们并不需要这个功能。最后我们创建一个db对象，用来操作数据库。\n注意：这里的数据库使用端口号5432，这是PostgreSQL的默认端口号，如果你的PostgreSQL使用的端口号不是5432，请修改为你的端口号。在PostgreSQL的配置文件中可以查看PostgreSQL使用的端口号。配置文件的路径是/etc/postgresql/14/main/postgresql.conf，其中14是PostgreSQL的版本号，如果你的版本号不是14，请修改为你的版本号。\n然后需要创建数据库模型，以在Flask中使用SQLAlchemy操作数据库。我们先导入SQLAlchemy库，然后创建一个db对象，用来操作数据库。然后我们需要定义一个Weather类，用来表示城市的天气信息。Weather类继承自db.Model类，这样Weather类就可以使用db对象来操作数据库。Weather类有数个属性，和我们之前用字典定义的差不多。最后我们需要使用db.create_all()方法来创建数据库表，这个方法会根据我们定义的数据库模型来创建数据库表。代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 db = SQLAlchemy(app) class Weather(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), nullable=False) country = db.Column(db.String(50), nullable=False) temp = db.Column(db.Integer, nullable=False) feels_like = db.Column(db.Integer, nullable=False) icon = db.Column(db.String(50), nullable=False) description = db.Column(db.String(50), nullable=False) db.create_all() 在应用中使用数据库 # 之前我们用字典来保存天气信息，现在我们改为用数据库来保存。代码如下： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 db.session.add(Weather( name = \u0026#39;London\u0026#39;, country = \u0026#39;UK\u0026#39;, temp = \u0026#39;12\u0026#39;, feels_like = \u0026#39;11\u0026#39;, icon = \u0026#39;10d\u0026#39;, description = \u0026#39;Moderate rain\u0026#39; )) db.session.add(Weather( name = \u0026#39;New York\u0026#39;, country = \u0026#39;US\u0026#39;, temp = \u0026#39;20\u0026#39;, feels_like = \u0026#39;19\u0026#39;, icon = \u0026#39;01d\u0026#39;, description = \u0026#39;Sunny\u0026#39; )) db.session.commit() 然后我们需要从数据库中读取天气信息，然后传递给模板。注意，之前的HTML模板接收一个字典数组作为参数，现在我们需要传递一个Weather对象的数组，因此我们需要修改一下模板： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Weather App\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;{{ url_for(\u0026#39;static\u0026#39;, filename=\u0026#39;style.css\u0026#39;) }}\u0026#34; type=\u0026#34;text/css\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;section class=\u0026#34;ajax-section\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;ul class=\u0026#34;cities\u0026#34;\u0026gt; {% for wd in weather_data %} \u0026lt;li class=\u0026#34;city\u0026#34;\u0026gt; \u0026lt;h2 class=\u0026#34;city-name\u0026#34;\u0026gt;{{ wd.name }}, \u0026lt;sup\u0026gt;{{ wd.country }}\u0026lt;/sup\u0026gt;\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;city-temp\u0026#34;\u0026gt;{{ wd.temp }}\u0026lt;sup\u0026gt;°C\u0026lt;/sup\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;figcaption\u0026gt;{{ wd.description }}\u0026lt;/figcaption\u0026gt; \u0026lt;img class=\u0026#34;city-icon\u0026#34; src=\u0026#34;https://s3-us-west-2.amazonaws.com/s.cdpn.io/162656/{{ wd.icon }}.svg\u0026#34; alt=\u0026#34;{{ wd.description }}\u0026#34;\u0026gt; \u0026lt;/li\u0026gt; {% endfor %} \u0026lt;/ul\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/section\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Python中获取数据库数据并传递给模板的代码如下： 1 2 cities_data = Weather.query.all() return render_template(\u0026#39;weather.html\u0026#39;, cities_data=cities_data) 这里的cities_data是一个列表，列表中的元素是一个Weather对象，我们可以在模板中使用这个对象的属性来获取天气信息。 运行Web应用程序 # 用下面的命令尝试运行我们的Web应用程序：\n1 python app.py 这时可能会遇到下面的报错信息：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Traceback (most recent call last): File \u0026#34;/home/lijin/Documents/www/learn_flask/app.py\u0026#34;, line 21, in \u0026lt;module\u0026gt; db.create_all() File \u0026#34;/home/lijin/Documents/www/learn_flask/env/lib/python3.11/site-packages/flask_sqlalchemy/extension.py\u0026#34;, line 900, in create_all self._call_for_binds(bind_key, \u0026#34;create_all\u0026#34;) File \u0026#34;/home/lijin/Documents/www/learn_flask/env/lib/python3.11/site-packages/flask_sqlalchemy/extension.py\u0026#34;, line 871, in _call_for_binds engine = self.engines[key] ^^^^^^^^^^^^ File \u0026#34;/home/lijin/Documents/www/learn_flask/env/lib/python3.11/site-packages/flask_sqlalchemy/extension.py\u0026#34;, line 687, in engines app = current_app._get_current_object() # type: ignore[attr-defined] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File \u0026#34;/home/lijin/Documents/www/learn_flask/env/lib/python3.11/site-packages/werkzeug/local.py\u0026#34;, line 508, in _get_current_object raise RuntimeError(unbound_message) from None RuntimeError: Working outside of application context. This typically means that you attempted to use functionality that needed the current application. To solve this, set up an application context with app.app_context(). See the documentation for more information. 这是因为我们的应用程序需要一个应用上下文，我们需要在app.py中添加下面的代码：\n1 2 with app.app_context(): db.create_all() 最终的app.py文件的代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 from flask import Flask, render_template from flask_sqlalchemy import SQLAlchemy from datetime import datetime app = Flask(__name__) app.config[\u0026#39;SQLALCHEMY_DATABASE_URI\u0026#39;] = \u0026#39;postgresql://test:test_password@localhost:5432/weather_db\u0026#39; app.config[\u0026#39;SQLALCHEMY_TRACK_MODIFICATIONS\u0026#39;] = False db = SQLAlchemy(app) class Weather(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), nullable=False) country = db.Column(db.String(50), nullable=False) temp = db.Column(db.Integer, nullable=False) feels_like = db.Column(db.Integer, nullable=False) icon = db.Column(db.String(50), nullable=False) description = db.Column(db.String(50), nullable=False) @app.route(\u0026#39;/\u0026#39;) def index(): db.session.add(Weather( name = \u0026#39;London\u0026#39;, country = \u0026#39;UK\u0026#39;, temp = \u0026#39;12\u0026#39;, feels_like = \u0026#39;11\u0026#39;, icon = \u0026#39;10d\u0026#39;, description = \u0026#39;Moderate rain\u0026#39; )) db.session.add(Weather( name = \u0026#39;New York\u0026#39;, country = \u0026#39;US\u0026#39;, temp = \u0026#39;20\u0026#39;, feels_like = \u0026#39;19\u0026#39;, icon = \u0026#39;01d\u0026#39;, description = \u0026#39;Sunny\u0026#39; )) db.session.commit() wd = Weather.query.all() print(len(wd)) return render_template(\u0026#39;weather.html\u0026#39;, weather_data=wd) if __name__ == \u0026#34;__main__\u0026#34;: with app.app_context(): db.create_all() app.run() 然后我们再次运行应用程序：\n1 python app.py 不出意外的话，我们可以在浏览器中看到我们的网站在两个卡片里展示了两个城市的天气信息，和上一篇文章中的效果是一样的： 注意：每次运行应用程序时，都会向数据库中添加两条数据，因此如果多次运行应用程序，可能会导致数据库中有多条重复的数据，网页上也会出现多个重复的卡片。这时我们可以使用下面的代码来删除重复的数据：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from sqlalchemy import text sql_text = text( \u0026#34;\u0026#34;\u0026#34;DELETE FROM weather WHERE id IN ( SELECT id FROM ( SELECT id, ROW_NUMBER() OVER (PARTITION BY name) AS row_num FROM weather ) AS numbered_rows WHERE row_num \u0026gt; 1 ); \u0026#34;\u0026#34;\u0026#34;) db.session.execute(sql_text) db.session.commit() 总结 # 至此，我们成功创建了一个PostgreSQL数据库，然后在Flask应用程序中使用SQLAlchemy操作数据库。但是数据库中的数据仍然是我们一条一条手动添加的，这样是很不灵活也很不方便的。下一篇文章我们将介绍如何获取用户的输入，然后根据用户输入的城市使用API来获取天气信息，然后把获取到的天气信息保存到数据库中。\n","date":"2023年11月17日","externalUrl":null,"permalink":"/p/python-web%E5%BC%80%E5%8F%91%E5%AD%A6%E4%B9%A0%E4%BA%8C%E4%BD%BF%E7%94%A8postgresql%E5%92%8Csqlalchemy/","section":"Posts","summary":" 缘起 # 在本系列的上一篇文章“Python Web开发学习（一）：使用Flask框架”中，我们使用Flask框架开发了一个简单的Web应用程序，来显示一些城市的天气信息。但是这些天气信息都是写死在代码中的，如果我们有许多的天气信息，再写死在代码中就不太方便了。因此我们需要一个数据库来存储这些天气信息，然后从数据库中读取这些天气信息，再显示在网页上。\n","title":"Python Web开发学习（二）：使用PostgreSQL和SQLAlchemy","type":"post"},{"content":" 缘起 # 主流的Web开发语言有很多，比如Java、PHP、Python、Ruby等，其中我对Python了解最多，因此我想用Python来开发Web应用程序。Python有很多Web框架，比如Django、Flask、Tornado等，这些框架都有各自的特点，其中Flask是一个轻量级的Web框架，因此我选择从Flask框架开始来学习开发Web应用程序。\n本系列文章将从零开始，一步一步地介绍如何使用Flask框架来开发Web应用程序。本文将介绍如何使用Flask框架来开发一个简单的Web应用程序，包括URL解析和模版渲染。 本系列文章计划包括：\n使用Flask框架开发一个简单的Web应用程序：URL路由、模版渲染（本文） 使用PostgreSQL数据库：创建PostgreSQL数据库、使用SQLAlchemy操作数据库（参见\u0026ldquo;Python Web开发学习（二）：使用PostgreSQL和SQLAlchemy\u0026rdquo; 数据接收和数据请求：表单、从第三方Web API获取数据（参见\u0026ldquo;Python Web开发学习（三）：使用输入框和API\u0026rdquo; 使用Docker部署Web应用程序：docker-compose、Nginx、Gunicorn（参见\u0026ldquo;Python Web开发学习（四）：使用Docker、Gunicorn和Nginx部署网站\u0026rdquo; Flask简介 # Flask是一个轻量级的Web框架，它的核心是WSGI工具包Werkzeug和模版引擎Jinja。Flask框架的核心是WSGI工具包Werkzeug，它是一个WSGI工具集，可以用来处理HTTP请求和响应，以及其他与Web应用程序相关的任务。模版引擎Jinja是一个现代的、设计优雅的模版引擎，它可以用来生成HTML页面。\n第一个简单的Web应用程序 # 我们需要创建一个Flask环境，然后编写一个简单的Web应用程序。\n创建一个目录，用来学习Flask开发，比如learn_flask，然后进入该目录：\n1 2 mkdir learn_flask cd learn_flask 创建一个Python虚拟环境，用来安装Flask框架和其他依赖包：\n1 2 3 python3 -m venv env # 创建Python虚拟环境 source env/bin/activate # 激活Python虚拟环境 pip install flask # 安装Flask框架 创建一个Python文件，用来编写Flask应用程序，比如app.py：\n1 2 3 4 5 6 7 8 9 10 from flask import Flask app = Flask(__name__) @app.route(\u0026#39;/\u0026#39;) def index(): return \u0026#39;Hello, World!\u0026#39; if __name__ == \u0026#34;__main__\u0026#34;: app.run() 上面的代码创建了一个Flask应用程序，当访问根URL时，会返回Hello, World!。下面我们来分析一下这段代码：\nfrom flask import Flask：导入Flask框架 app = Flask(__name__)：创建一个Flask应用程序，__name__是当前模块的名称，如果当前模块是主模块，则__name__的值为__main__，否则__name__的值为当前模块的名称。 @app.route('/')：使用app.route()装饰器来注册一个URL，这里注册了根URL，即/。route()是一个路由函数，当用户访问某个URL时，路由函数会根据URL请求来调用相应的Python函数。 def index():：定义一个Python函数，用来处理根URL的请求。 return 'Hello, World!'：当用户访问根URL时，会调用index()函数，该函数会返回Hello, World!。 if __name__ == \u0026quot;__main__\u0026quot;:：如果当前模块是主模块，则执行app.run()，启动Flask应用程序。 运行Flask应用程序：\n1 python app.py Flask应用默认运行在5000端口，因此可以在浏览器中访问http://localhost:5000/，就可以看到Hello, World!： 至此，我们已经创建了一个简单的Flask应用程序。但现在我们的应用程序只能返回一个字符串，这显然是不够的，实际应用中，我们往往需要返回一个HTML页面，这就需要使用模版引擎Jinja。\n使用模板 # 接下来的例子中，我们用这个应用来展示一些实际的信息，比如一个城市的天气。我们需要创建一个HTML页面，用来展示天气信息，然后将天气信息传递给HTML页面，最后将HTML页面返回给用户。\n模板文件 # 在learn_flask目录下创建一个templates目录，然后在templates目录下创建weather.html文件。 weather.html文件的内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Weather App\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;{{ url_for(\u0026#39;static\u0026#39;, filename=\u0026#39;style.css\u0026#39;) }}\u0026#34; type=\u0026#34;text/css\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;section class=\u0026#34;ajax-section\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;ul class=\u0026#34;cities\u0026#34;\u0026gt; {% for city in cities_data %} \u0026lt;li class=\u0026#34;city\u0026#34;\u0026gt; \u0026lt;h2 class=\u0026#34;city-name\u0026#34;\u0026gt;{{ city.name }}, \u0026lt;sup\u0026gt;{{ city.country }}\u0026lt;/sup\u0026gt;\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;city-temp\u0026#34;\u0026gt;{{ city.temp }}\u0026lt;sup\u0026gt;°C\u0026lt;/sup\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;figcaption\u0026gt;{{ city.description }}\u0026lt;/figcaption\u0026gt; \u0026lt;img class=\u0026#34;city-icon\u0026#34; src=\u0026#34;{{ city.icon }}\u0026#34; alt=\u0026#34;{{ city.description }}\u0026#34;\u0026gt; \u0026lt;/li\u0026gt; {% endfor %} \u0026lt;/ul\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/section\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 在learn_flask目录下创建一个static目录，然后在static目录下创建style.css文件。 展示天气信息的卡片的样式style.css文件来自https://webdesign.tutsplus.com/build-a-simple-weather-app-with-vanilla-javascript\u0026ndash;cms-33893t，其效果如下所示：\n向模板传递参数 # 在weather.html文件中，我们准备接收一个cities_data的变量，然后分城市展示天气信息。cities_data变量是一个字典，字典的键是城市名称，字典的值是一个列表，列表中的元素是一个字典，字典中包含了天气信息。 改动后的app.py文件如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 from flask import Flask, render_template app = Flask(__name__) class WeatherData: def __init__(self, data): self.name = data[\u0026#39;name\u0026#39;] self.country = data[\u0026#39;country\u0026#39;] self.temp = data[\u0026#39;temp\u0026#39;] self.feels_like = data[\u0026#39;feels_like\u0026#39;] self.icon = f\u0026#39;https://s3-us-west-2.amazonaws.com/s.cdpn.io/162656/{data[\u0026#34;icon\u0026#34;]}.svg\u0026#39; self.description = data[\u0026#39;description\u0026#39;] @app.route(\u0026#39;/\u0026#39;) def index(): cities_data = [] wd1 = WeatherData({ \u0026#39;name\u0026#39;: \u0026#39;London\u0026#39;, \u0026#39;country\u0026#39;: \u0026#39;UK\u0026#39;, \u0026#39;temp\u0026#39;: \u0026#39;12\u0026#39;, \u0026#39;feels_like\u0026#39;: \u0026#39;11\u0026#39;, \u0026#39;icon\u0026#39;: \u0026#39;10d\u0026#39;, \u0026#39;description\u0026#39;: \u0026#39;Moderate rain\u0026#39; }) cities_data.append(wd1) wd2 = WeatherData({ \u0026#39;name\u0026#39;: \u0026#39;New York\u0026#39;, \u0026#39;country\u0026#39;: \u0026#39;US\u0026#39;, \u0026#39;temp\u0026#39;: \u0026#39;20\u0026#39;, \u0026#39;feels_like\u0026#39;: \u0026#39;19\u0026#39;, \u0026#39;icon\u0026#39;: \u0026#39;01d\u0026#39;, \u0026#39;description\u0026#39;: \u0026#39;Sunny\u0026#39; }) cities_data.append(wd2) return render_template(\u0026#39;weather.html\u0026#39;, cities_data=cities_data) if __name__ == \u0026#34;__main__\u0026#34;: app.run() 运行应用 # 再次运行应用程序：\n1 python app.py 在浏览器中访问http://localhost:5000/，就可以看到我们的网站在两个卡片里展示了两个城市的天气信息： ","date":"2023年11月17日","externalUrl":null,"permalink":"/p/python-web%E5%BC%80%E5%8F%91%E5%AD%A6%E4%B9%A0%E4%B8%80%E4%BD%BF%E7%94%A8flask%E6%A1%86%E6%9E%B6/","section":"Posts","summary":" 缘起 # 主流的Web开发语言有很多，比如Java、PHP、Python、Ruby等，其中我对Python了解最多，因此我想用Python来开发Web应用程序。Python有很多Web框架，比如Django、Flask、Tornado等，这些框架都有各自的特点，其中Flask是一个轻量级的Web框架，因此我选择从Flask框架开始来学习开发Web应用程序。\n","title":"Python Web开发学习（一）：使用Flask框架","type":"post"},{"content":"","date":"2023年11月17日","externalUrl":null,"permalink":"/tags/sqlalchemy/","section":"Tags","summary":"","title":"SQLAlchemy","type":"tags"},{"content":"","date":"2023年10月12日","externalUrl":null,"permalink":"/tags/sql/","section":"Tags","summary":"","title":"SQL","type":"tags"},{"content":" 缘起 # 最近在学习使用SQL语言，主要是做LeetCode上的题目。但由于我是刚入门，对SQL数据库还不太熟悉，因此经常需要给自己写的SQL语句调试，这就需要一个可以连接的MySQL数据库。网上的教程大多都是在Windows系统下安装MySQL，但我平时的开发环境是在MacBook上用VSCode连接远程的Ubuntu服务器，因此我想在Ubuntu系统下安装MySQL，并且能够用VSCode remote连接MySQL数据库。\n我在网上也看到了一些在Ubuntu下安装使用MySQL的教程，但大都比较陈旧，试了很多都有各种问题，最后经过踩坑和摸索，终于配置好了一个可以让自己用得比较舒服的MySQL开发测试环境，这里记录一下。\nSQL简介 # SQL（Structured Query Language）是结构化查询语言的缩写，是一种用于数据库操作的语言，是一种标准的数据库语言，用于访问和操作数据库系统。SQL语言可以用于访问和操作数据库中的数据，也可以用于创建和修改数据库中的表和视图。SQL语言是一种标准的数据库语言，不同的数据库系统都支持SQL语言，但是不同的数据库系统对SQL语言的支持程度不同，因此在使用SQL语言时需要注意数据库系统的差异。目前常用的数据库系统有MySQL、Oracle、SQL Server、PostgreSQL、SQLite等。\nMySQL # MySQL是一个开源的关系型数据库管理系统，由瑞典MySQL AB公司开发，目前属于Oracle公司。MySQL是最流行的关系型数据库管理系统之一，它的主要特点是速度快、安装方便、体积小、总体拥有成本低。MySQL有收费的商业版，但这里我们只是为了学习数据库知识，使用免费版就行了。如果需要商用且数据量极大，建议使用Oracle数据库，速度比MySQL快很多。\n安装MySQL # 在Ubuntu 22中安装MySQL非常简单，只需要在终端中运行以下命令即可：\n1 sudo apt install mysql-server 更改MySQL账户设置 # 安装好之后，默认的账户只有超级用户才能访问，非常不方便。因此我们需要修改一下MySQL的账户设置，创建一个普通账户，然后使得我们可以使用VS Code从远程来连接这个数据库。\n首先以超级用户身份登录MySQL： 1 sudo mysql -u root 创建一个新的MySQL账户： 1 2 3 CREATE User \u0026#39;test\u0026#39;@\u0026#39;localhost\u0026#39; IDENTIFIED BY \u0026#39;\u0026#39;; GRANT ALL ON *.* TO \u0026#39;test\u0026#39;@\u0026#39;localhost\u0026#39;; FLUSH PRIVILEGES; 这里的test是账户名，localhost是允许访问的主机名，*.*表示允许访问所有的数据库和表，WITH GRANT OPTION表示允许这个账户创建新的账户。然后输入exit退出MySQL。 注意：这里的密码为空。因为设置了密码后，也可以连接MySQL，但是在VS Code内运行SQL代码时有时会报错“ACCESS DENIED”，因此这里不设置密码。当然，有时候也能成功运行，我也没搞明白为啥。 这次以普通用户身份登录MySQL，指定用户名是test： 1 mysql -h 127.0.0.1 -P 3306 -u test 然后运行下面的SQL语句，查看当前的用户信息： 1 SELECT User, Host FROM mysql.user; 如果之前的操作都成功的话，应该会看到类似下面的输出： 1 2 3 4 5 6 7 8 9 10 11 mysql\u0026gt; SELECT User,Host FROM mysql.user; +------------------+-----------+ | User | Host | +------------------+-----------+ | debian-sys-maint | localhost | | mysql.infoschema | localhost | | mysql.session | localhost | | mysql.sys | localhost | | test | localhost | +------------------+-----------+ 5 rows in set (0.01 sec) VS Code # 安装MySQL插件 # 能够用VSCode remote连接MySQL数据库，需要安装一个MySQL插件MySQL： 连接MySQL数据库 # 安装好插件之后，就可以连接MySQL数据库了。按照插件的使用指南连接MySQL数据库： 其中，Host填写的是MySQL数据库所在的主机名，即127.0.0.1，Port填写的是MySQL数据库的端口号3306，User填写的是MySQL数据库的用户名test，Password填写的是MySQL数据库的密码。如果一切正常的话，就可以连接上MySQL数据库了。\n调试SQL代码 # 连接好数据库后，新建一个后缀为.sql的文件，然后在文件中输入SQL代码。文件头上会显示Active Connection，表示已经成功连接到MySQL数据库了。 每行SQL代码前面也会有一个Execute按钮，点击这个按钮就可以执行这一行的SQL代码了。点击侧边栏的插件按钮，也可以看到数据库中实时更新的内容： ","date":"2023年10月12日","externalUrl":null,"permalink":"/p/ubuntu-22%E4%B8%8B%E9%85%8D%E7%BD%AEsql%E5%BC%80%E5%8F%91%E6%B5%8B%E8%AF%95%E7%8E%AF%E5%A2%83mysql-vscode/","section":"Posts","summary":" 缘起 # 最近在学习使用SQL语言，主要是做LeetCode上的题目。但由于我是刚入门，对SQL数据库还不太熟悉，因此经常需要给自己写的SQL语句调试，这就需要一个可以连接的MySQL数据库。网上的教程大多都是在Windows系统下安装MySQL，但我平时的开发环境是在MacBook上用VSCode连接远程的Ubuntu服务器，因此我想在Ubuntu系统下安装MySQL，并且能够用VSCode remote连接MySQL数据库。\n","title":"Ubuntu 22下配置SQL开发测试环境（MySQL+VSCode）","type":"post"},{"content":"","date":"2023年10月12日","externalUrl":null,"permalink":"/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/","section":"Tags","summary":"","title":"数据库","type":"tags"},{"content":"","date":"2023年9月15日","externalUrl":null,"permalink":"/tags/hostinger/","section":"Tags","summary":"","title":"Hostinger","type":"tags"},{"content":" 缘起 # Hostinger是一个网络托管服务商，提供域名注册、虚拟主机、VPS等服务。 最近在Hostinger上入手了一个域名，打算把之前建的网站迁移到新的域名上。我一直在使用acme.sh为我的网站生成SSL证书。但是Hostinger不提供可供acme.sh调取的API，因此无法直接用acme.sh为托管在Hostinger上的域名生成SSL证书。Cloudflare是一家基于反向代理为客户提供内容分发网络的服务商，也提供域名解析服务，而且提供acme.sh可用的API，所以我就想将域名服务商从Hostinger迁移到Cloudflare。\n迁移域名 # 创建Cloudflare账户 # 首先需要创建一个Cloudflare账户，直接创建就行，创建过程没有太多特殊要求。\n添加域名 # 登录进Cloudflare账户后，进入首页，在首页点击Add a site，输入域名，点击Add site。\n然后需要选择一个套餐，这里免费套餐对我就足够了，点击Confirm plan，然后点击Confirm。\n接下来Cloudflare会扫描域名的DNS记录，定位你的域名之前使用的域名解析服务器，然后Cloudflare会提示你将域名解析服务器更改为Cloudflare的域名解析服务器。\n这里需要登录之前的域名服务商的账户，例如我需要登录hostinger的账户，将域名解析服务器更改为Cloudflare的域名解析服务器。选择你需要迁移的那个域名，会显示域名的状态，以及域名解析服务器的名字，点击Change，按照上一步中Cloudflare给的提示，将域名解析服务器更改为Cloudflare的域名解析服务器。\n更改完之后点击Done, check nameservers。然后需要等待一段时间让Cloudflare确认域名解析服务器已经更改为Cloudflare的域名解析服务器，这个过程可能需要几分钟到几个小时不等，Cloudflare确认后会发邮件通知你，等待过程中可以先进行下一步。\n添加DNS记录 # 在Cloudflare的首页，点击DNS，然后点击Add record，添加DNS记录。\n将之前在Hostinger上的DNS记录全部添加到Cloudflare上。\n迁移网站 # 生成SSL证书 # 这里我们使用acme.sh为网站生成SSL证书，acme.sh的安装和使用可参见个人网站的建立过程（二）：使用Hugo框架搭建个人网站。\n需要注意的是，在上面的链接里介绍的生成SSL证书的方法是针对阿里云的，而Cloudflare的API和阿里云的API不太一样，需要使用不同的账户口令，分别是账户名（CF_Account_ID）、区域名（CF_Zone_ID）和口令（CF_Token）。这些都可以在Cloudflare的账户里找到。具体过程可参见acme.sh的使用文档。\n这里我为所有网站都重新生成了SSL证书。\n更改nginx反向代理配置 # 之前的反向代理配置文件都放在/etc/nginx/vhost目录下，这里我将所有的配置文件中的旧域名都替换为新域名，注意如果在上一步中更改了SSL证书的存放路径，也需要将配置文件中的证书路径更改为新的路径。\n更改docker-compose配置 # 最后将之前搭建网站的docker-compose配置文件中的旧域名都替换为新域名，然后重新启动docker-compose服务。\n一些问题 # Cloudflare的SSL/TLS加密设置 # 在做完上面的设置后，在通过新域名访问网站时出现了ERR_TOO_MANY_REDIRECTS的错误： 这是因为在Cloudflare里SSL/TLS加密设置为了“Flexible”，这里需要将SSL/TLS加密设置为“Full”： 这样之前的网站就可以通过新域名访问了！\n","date":"2023年9月15日","externalUrl":null,"permalink":"/p/%E5%B0%86%E5%9F%9F%E5%90%8D%E6%9C%8D%E5%8A%A1%E5%95%86%E4%BB%8Ehostinger%E8%BF%81%E7%A7%BB%E5%88%B0cloudflare/","section":"Posts","summary":" 缘起 # Hostinger是一个网络托管服务商，提供域名注册、虚拟主机、VPS等服务。 最近在Hostinger上入手了一个域名，打算把之前建的网站迁移到新的域名上。我一直在使用acme.sh为我的网站生成SSL证书。但是Hostinger不提供可供acme.sh调取的API，因此无法直接用acme.sh为托管在Hostinger上的域名生成SSL证书。Cloudflare是一家基于反向代理为客户提供内容分发网络的服务商，也提供域名解析服务，而且提供acme.sh可用的API，所以我就想将域名服务商从Hostinger迁移到Cloudflare。\n","title":"将域名服务商从Hostinger迁移到Cloudflare","type":"post"},{"content":"","date":"2023年9月2日","externalUrl":null,"permalink":"/tags/almeida-cv/","section":"Tags","summary":"","title":"Almeida-Cv","type":"tags"},{"content":"","date":"2023年9月2日","externalUrl":null,"permalink":"/tags/code-repository/","section":"Tags","summary":"","title":"Code Repository","type":"tags"},{"content":"","date":"2 九月 2023","externalUrl":null,"permalink":"/en/tags/cv-template/","section":"Tags","summary":"","title":"CV Template","type":"tags"},{"content":"","date":"2023年9月2日","externalUrl":null,"permalink":"/tags/hugo-theme-stack/","section":"Tags","summary":"","title":"Hugo-Theme-Stack","type":"tags"},{"content":"","date":"2023年9月2日","externalUrl":null,"permalink":"/tags/hugo%E4%B8%BB%E9%A2%98/","section":"Tags","summary":"","title":"Hugo主题","type":"tags"},{"content":"","date":"2 九月 2023","externalUrl":null,"permalink":"/en/tags/resume-template/","section":"Tags","summary":"","title":"Resume Template","type":"tags"},{"content":" 缘起 # 在搭建本网站时，我使用了Hugo主题hugo-theme-stack，可以在关于页面添加个人简历。但是，这个页面的样式并不是传统简历的样式，而是类似于博客文章的样式，不太符合我的要求。 我对简历模板的要求是：\n传统简历的样式，不要花里胡哨 可以导出为PDF格式 尺寸为A4纸的大小或letter纸的大小，以便与传统简历的打印尺寸相同 于是我在网上找到了一个大致符合要求的简历模板Almeida CV，但是这个模板是一个完整的Hugo主题，我不想把整个主题都用上，只想用它的简历模板。 在网上查了一些资料后发现，确实是有办法在同一个网站中使用两个Hugo主题的。\n实现 # 下载主题 # 我们需要下载两个主题，一个是hugo-theme-stack，另一个是Almeida CV。由于我之前已经对hugo-theme-stack进行了一些修改，所以我fork了Jimmy Cai在GitHub上的仓库，我自己修改后的仓库是hugo-theme-stack。\n进入网站的根目录，下载这两个主题，并放在themes文件夹中：\n1 2 git clone https://github.com/jin-li/hugo-theme-stack themes/hugo-theme-stack git clone https://github.com/ineesalmeida/almeida-cv themes/almeida-cv 一些修改 # 修改配置文件。首先，我们需要在config/_default/config.yaml中指定两个主题：\n1 2 3 theme: - hugo-theme-stack - almeida-cv 然后将almeida-cv主题配置文件中的设置也合并到config/_default/config.yaml中：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 enableRobotsTXT: true enableEmoji: true params: enableMetaTags: true colorLight: \u0026#39;#fff\u0026#39; colorDark: \u0026#39;#666\u0026#39; colorPageBackground: \u0026#39;#ddd\u0026#39; colorPrimary: \u0026#39;#e3bfb8\u0026#39; colorSecondary: \u0026#39;#aaa\u0026#39; colorIconPrimary: \u0026#39;#fff\u0026#39; colorIconBackground: \u0026#39;#e3bfb8\u0026#39; colorRightColumnBackground: \u0026#39;#f5f5f5\u0026#39; colorRightColumnHeadingText: \u0026#39;#666\u0026#39; colorRightColumnBodyText: \u0026#39;#666\u0026#39; colorRightColumnIconPrimary: \u0026#39;#fff\u0026#39; colorRightColumnIconBackground: \u0026#39;#e3bfb8\u0026#39; pages: 1 swapColumns: false 新建简历样式。在almeida-cv主题中，简历的样式是在layouts/index.html中定义的，我们把index.html文件复制到layouts/_default文件夹中，并重命名为cv.html。\n1 cp themes/almeida-cv/layouts/index.html themes/almeida-cv/layouts/_default/cv.html 然后我们删除掉almeida-cv主题中的assests/scss/_custom.scss文件。这个文件本来就是个空文件，但留着它会覆盖掉hugo-theme-stack主题中的assests/scss/custom.scss文件，导致我们hugo-theme-stack中的配置无效， 因此需要删除掉它。\n复制静态文件。我们需要将almeida-cv主题中exampleSite中提供的模板静态文件复制到网站根目录下，需要关注的文件有两个：\nexampleSite/data文件夹中的content.yaml文件，这个文件定义了简历的内容，我们需要将它复制到网站根目录下的data文件夹中。之后我们可以修改这个文件中的内容，以便生成我们自己的简历。 exampleSite/static/img文件夹中的avatar.jpg文件，这个文件是简历中的头像。由于我们在hugo-theme-stack中其实已经有了一个头像，所以我们可以不用这个文件，只需要重新指定头像的路径即可。例如我在hugo-theme-stack中用的头像存放在网站根目录下的static/favicon.png，所以只需要在data/content.yaml中将头像的路径改为/favicon.png即可。 修改网站的关于页面。我想把简历放在网站的关于页面，因此需要修改掉依靠hugo-theme-stack主题定义的“关于”页面。网站的“关于”页面由网站根目录下的content/page/about/index.md文件所定义，我们需要指定它使用在第二步中定义的cv.html模板，因此需要在index.md文件中的内容为：\n1 2 3 4 5 6 7 8 9 10 11 --- title: \u0026#34;关于\u0026#34; date: 2023-08-22 layout: \u0026#34;cv\u0026#34; slug: \u0026#34;about\u0026#34; menu: main: weight: -70 params: icon: archives --- 在markdown文件中只需要这样一个文件头告诉Hugo依据cv的样式生成页面就行了，因为简历的内容已经在data/content.yaml中定义了。\n额外修改 # 其实有了上面的那些修改，网站的“关于”页面已经变成了almeida-cv主题的简历，但我又做了一些额外的修改，是的简历的样式更符合我的要求。主要的修改包括：\n修改config/_default/config.yaml中的params: swapColumns为true，这样简历中的左右两栏就会交换位置。 修改简历中education中的样式。 修改简历中页脚的半透明水印。 修改网站页面底部的版权信息，添加了almeida-cv主题的版权信息。 这里不再赘述代码上的修改，感兴趣的可以查看我的GitHub仓库https://github.com/jin-li/almeida-cv。\n最终效果 # 最终效果可参见本网站的“关于”页面https://jinli.cyou/about/。\n","date":"2023年9月2日","externalUrl":null,"permalink":"/p/%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AAhugo%E4%B8%BB%E9%A2%98%E5%9C%A8hugo-theme-stack%E4%B8%AD%E4%BD%BF%E7%94%A8almeida-cv%E4%B8%BB%E9%A2%98%E7%9A%84%E7%AE%80%E5%8E%86%E6%A8%A1%E6%9D%BF/","section":"Posts","summary":" 缘起 # 在搭建本网站时，我使用了Hugo主题hugo-theme-stack，可以在关于页面添加个人简历。但是，这个页面的样式并不是传统简历的样式，而是类似于博客文章的样式，不太符合我的要求。 我对简历模板的要求是：\n传统简历的样式，不要花里胡哨 可以导出为PDF格式 尺寸为A4纸的大小或letter纸的大小，以便与传统简历的打印尺寸相同 于是我在网上找到了一个大致符合要求的简历模板Almeida CV，但是这个模板是一个完整的Hugo主题，我不想把整个主题都用上，只想用它的简历模板。 在网上查了一些资料后发现，确实是有办法在同一个网站中使用两个Hugo主题的。\n","title":"合并两个Hugo主题：在hugo-theme-stack中使用almeida-cv主题的简历模板","type":"post"},{"content":"","date":"2023年9月2日","externalUrl":null,"permalink":"/tags/%E7%AE%80%E5%8E%86%E6%A8%A1%E6%9D%BF/","section":"Tags","summary":"","title":"简历模板","type":"tags"},{"content":" 缘起 # 之前的代码基本都放在GitHub上，但有些代码因为涉及到科研中的一些项目，即使放在GitHub的私有仓库可能也会有潜在的安全问题，所以就想在自己的电脑或服务器上搭建一个私有的代码仓库。\n前提 # docker和docker compose 域名（二级域名即可） acme.sh （生成SSL证书） nginx （反向代理） 准备工作 # 域名和SSL证书 # 在自己购买域名的网站上或者自己域名的管理网站上创建一个二级域名并配置域名解析，具体过程可参见\u0026ldquo;个人网站的建立过程（一）：购买个人域名并配置动态域名解析\u0026rdquo;。 例如我有一个主域名jinli.cyou，于是我为私有GitLab服务器创建了一个\u0026quot;git.jinli.cyou\u0026quot;的二级域名。然后在阿里云的域名管理平台上进行了域名和IP地址的绑定。\n这里使用acme.sh来为\u0026quot;git.jinli.cyou\u0026quot;生成证书。具体过程可参见个人网站的建立过程（二）：使用Hugo框架搭建个人网站。 主要过程如下： 找到你的域名管理账户的Access key。可以使用之前的，也可以重新获取，然后将Access key到处为系统变量Ali_Key和Ali_Secret。\n使用下面的命令生成证书：\n1 acme.sh --issue --dns dns_ali -d your.domain.com 使用下面的命令将证书复制到你的mailcow目录下，例如我在mailcow安装目录下新建了一个文件夹cert来存放证书文件：\n1 2 3 acme.sh --install-cert -d your.domain.com \\ --key-file /media/gitlab/cert/key.pem \\ --fullchain-file /media/gitlab/cert/cert.pem nginx反向代理配置 # 在nginx的配置目录下创建一个新的配置文件gitlab.conf作为云网盘的反向代理配置文件，文件内写入：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 upstream git.jinli.li { server 127.0.0.1:8080; } server { listen 80; server_name git.jinli.li; return 301 https://git.jinli.li$request_uri; } server { listen 443 ssl; server_name git.jinli.li; # ssl 配置 ssl_certificate /media/gitlab/cert/cert.pem; ssl_certificate_key /media/gitlab/cert/key.pem; location / { proxy_redirect off; proxy_pass http://git.jinli.li; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Ssl on; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Frame-Options SAMEORIGIN; client_max_body_size 100m; } } 这里我使用8080端口作为GitLab的服务端口。SSL证书存放在上一步中证书安装的位置。\n配置完成后重启nginx服务使修改生效：\n1 sudo service nginx restart 安装GitLab # 这里使用docker-compose来安装jellyfin，具体操作参照https://docs.gitlab.com/ee/install/docker.html。 在安装目录/media/gitlab下创建以下docker-compose.yml文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 version: \u0026#39;3.6\u0026#39; services: web: image: \u0026#39;gitlab/gitlab-ee:latest\u0026#39; restart: always hostname: \u0026#39;git.jinli.io\u0026#39; environment: GITLAB_OMNIBUS_CONFIG: | external_url \u0026#39;http://git.jinli.io\u0026#39; gitlab_rails[\u0026#39;gitlab_shell_ssh_port\u0026#39;] = 22 ports: - \u0026#39;1080:80\u0026#39; - \u0026#39;1443:443\u0026#39; - \u0026#39;1022:22\u0026#39; volumes: - \u0026#39;./config:/etc/gitlab\u0026#39; - \u0026#39;./logs:/var/log/gitlab\u0026#39; - \u0026#39;./data:/var/opt/gitlab\u0026#39; shm_size: \u0026#39;256m\u0026#39; 然后执行以下命令启动GitLab服务：\n1 sudo docker-compose up -d 网站初始化和设置 # 如果上述配置全部成功完成的话，就可以通过浏览器访问网站，可以在地址栏输入http://0.0.0.0:8080来访问，或者通过域名https://git.jinli.cyou来访问。 如果网站尚未初始化完毕，通过浏览器访问时会看到提示502 Bad Gateway，需要耐心等待几分钟，直到网站初始化完成。\n第一次访问网站时需要设置管理员账户和密码，然后可以创建一个普通账户，用于日常使用。\n配置系统通知邮箱 # 经过以上配置，GitLab已经可以正常使用了。但是我们还想使用系统通知功能，例如发送系统通知邮件、注册账户时的邮件验证等，就需要配置系统通知邮箱。这里我使用的是自己的邮箱服务器，具体配置如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 ### GitLab email server settings ###! Docs: https://docs.gitlab.com/omnibus/settings/smtp.html ###! **Use smtp instead of sendmail/postfix.** gitlab_rails[\u0026#39;smtp_enable\u0026#39;] = true gitlab_rails[\u0026#39;smtp_address\u0026#39;] = \u0026#34;mail.jinli.cyou\u0026#34; gitlab_rails[\u0026#39;smtp_port\u0026#39;] = 587 gitlab_rails[\u0026#39;smtp_user_name\u0026#39;] = \u0026#34;mailer@jinli.cyou\u0026#34; gitlab_rails[\u0026#39;smtp_password\u0026#39;] = \u0026#34;your_email_password\u0026#34; gitlab_rails[\u0026#39;smtp_domain\u0026#39;] = \u0026#34;mail.jinli.cyou\u0026#34; gitlab_rails[\u0026#39;smtp_authentication\u0026#39;] = \u0026#34;login\u0026#34; gitlab_rails[\u0026#39;smtp_enable_starttls_auto\u0026#39;] = true gitlab_rails[\u0026#39;smtp_tls\u0026#39;] = false # gitlab_rails[\u0026#39;smtp_pool\u0026#39;] = false ###! **Can be: \u0026#39;none\u0026#39;, \u0026#39;peer\u0026#39;, \u0026#39;client_once\u0026#39;, \u0026#39;fail_if_no_peer_cert\u0026#39;** ###! Docs: http://api.rubyonrails.org/classes/ActionMailer/Base.html gitlab_rails[\u0026#39;smtp_openssl_verify_mode\u0026#39;] = \u0026#39;none\u0026#39; # gitlab_rails[\u0026#39;smtp_ca_path\u0026#39;] = \u0026#34;/etc/ssl/certs\u0026#34; # gitlab_rails[\u0026#39;smtp_ca_file\u0026#39;] = \u0026#34;/etc/ssl/certs/ca-certificates.crt\u0026#34; ### Email Settings # gitlab_rails[\u0026#39;gitlab_email_enabled\u0026#39;] = true ##! If your SMTP server does not like the default \u0026#39;From: gitlab@gitlab.example.com\u0026#39; ##! can change the \u0026#39;From\u0026#39; with this setting. gitlab_rails[\u0026#39;gitlab_email_from\u0026#39;] = \u0026#39;mailer@jinli.cyou\u0026#39; gitlab_rails[\u0026#39;gitlab_email_display_name\u0026#39;] = \u0026#39;GitLab Mailer\u0026#39; gitlab_rails[\u0026#39;gitlab_email_reply_to\u0026#39;] = \u0026#39;mailer@jinli.cyou\u0026#39; # gitlab_rails[\u0026#39;gitlab_email_subject_suffix\u0026#39;] = \u0026#39;\u0026#39; # gitlab_rails[\u0026#39;gitlab_email_smime_enabled\u0026#39;] = false # gitlab_rails[\u0026#39;gitlab_email_smime_key_file\u0026#39;] = \u0026#39;/etc/gitlab/ssl/gitlab_smime.key\u0026#39; # gitlab_rails[\u0026#39;gitlab_email_smime_cert_file\u0026#39;] = \u0026#39;/etc/gitlab/ssl/gitlab_smime.crt\u0026#39; # gitlab_rails[\u0026#39;gitlab_email_smime_ca_certs_file\u0026#39;] = \u0026#39;/etc/gitlab/ssl/gitlab_smime_cas.crt\u0026#39; 这样我们自己的GitLab服务器也有系统通知功能了。\n","date":"2023年9月2日","externalUrl":null,"permalink":"/p/%E4%BD%BF%E7%94%A8gitlab%E5%92%8Cdocker%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E4%BB%A3%E7%A0%81%E4%BB%93%E5%BA%93/","section":"Posts","summary":" 缘起 # 之前的代码基本都放在GitHub上，但有些代码因为涉及到科研中的一些项目，即使放在GitHub的私有仓库可能也会有潜在的安全问题，所以就想在自己的电脑或服务器上搭建一个私有的代码仓库。\n","title":"使用GitLab和docker搭建个人代码仓库","type":"post"},{"content":"","date":"2023年8月26日","externalUrl":null,"permalink":"/tags/jellyfin/","section":"Tags","summary":"","title":"Jellyfin","type":"tags"},{"content":"","date":"2023年8月26日","externalUrl":null,"permalink":"/tags/media-server/","section":"Tags","summary":"","title":"Media Server","type":"tags"},{"content":"","date":"2023年8月26日","externalUrl":null,"permalink":"/tags/personal-media-library/","section":"Tags","summary":"","title":"Personal Media Library","type":"tags"},{"content":" 缘起 # 之前下载了不少影视资源，放在家里主机的硬盘里，但是想在其他地方或自己的其他设备上观看就不太方便了。于是想到搭建一个线上影视库，可以在任何地方通过网络观看自己的影视资源。 在网上看到了很多搭建线上影视库的方法，比如使用plex、emby、jellyfin等软件，这些软件都是开源的，可以在自己的电脑或服务器上搭建，然后通过网页或客户端来观看自己的影视资源。 由于plex和emby的许多高级功能都需要付费版才能用，而jellyfin则是所有功能都免费，这里我选择使用jellyfin来搭建个人线上影视库。\n前提 # docker和docker compose 域名（二级域名即可） acme.sh （生成SSL证书） nginx （反向代理） 准备工作 # 准备域名 # 在自己购买域名的网站上或者自己域名的管理网站上创建一个二级域名并配置域名解析，具体过程可参见\u0026ldquo;个人网站的建立过程（一）：购买个人域名并配置动态域名解析\u0026rdquo;。\n例如我有一个主域名jinli.cyou，于是我为私有邮箱服务器创建了一个\u0026quot;m.jinli.cyou\u0026quot;的二级域名。然后在阿里云的域名管理平台上进行了域名和IP地址的绑定。\n使用acme.sh生成SSL证书 # 网上很多教程都使用Let\u0026rsquo;s Encrypt来生成SSL证书，但由于我之前在搭建这个网站时已经下载了acme.sh工具，因此这里我使用了acme.sh来为\u0026quot;m.jinli.cyou\u0026quot;生成证书。具体过程可参见个人网站的建立过程（二）：使用Hugo框架搭建个人网站。\n过程如下：\n找到你的域名管理账户的Access key。可以使用之前的，也可以重新获取，然后将Access key到处为系统变量Ali_Key和Ali_Secret。\n使用下面的命令生成证书：\n1 acme.sh --issue --dns dns_ali -d your.domain.com 使用下面的命令将证书复制到你的mailcow目录下，例如我在mailcow安装目录下新建了一个文件夹cert来存放证书文件：\n1 2 3 acme.sh --install-cert -d your.domain.com \\ --key-file /media/jellyfin/cert/key.pem \\ --fullchain-file /media/jellyfin/cert/cert.pem nginx反向代理配置 # 因为我之前在建立本网站时就安装了nginx，所以这里我使用了nginx作为反向代理工具。\n在nginx的配置目录下创建一个新的配置文件mailcow.conf作为云网盘的反向代理配置文件，文件内写入：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 upstream m.jinli.li { server 127.0.0.1:8081; } server { listen 80; server_name m.jinli.li; return 301 https://m.jinli.li$request_uri; } server { listen 443 ssl; server_name m.jinli.li; # ssl 配置 ssl_certificate /media/jellyfin/cert/cert.pem; ssl_certificate_key /media/jellyfin/cert/key.pem; location / { proxy_redirect off; proxy_pass http://m.jinli.li; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Ssl on; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Frame-Options SAMEORIGIN; client_max_body_size 1000m; } } 这里我使用8081端口作为jellyfin的服务端口。SSL证书存放在上一步指定的位置。\n配置完成后重启nginx服务使修改生效：\n1 sudo service nginx restart 安装jellyfin # 这里使用docker-compose来安装jellyfin，具体操作参照https://hub.docker.com/r/linuxserver/jellyfin。\ndocker-compose.yml文件中可以修改的参数有：\nTZ：时区 ports：端口 volumes：映射的文件夹 这里我选择将jellyfin的配置文件都放在jellyfin安装目录下的config文件夹内。影视文件存放在原来的硬盘内，这里将存放影视文件的文件夹（例如名为/media/Disk8T）映射到jellyfin容器内的/data/media文件夹内。 那么关于volumes的具体设置在docker-compose.yml文件中就如下所示： 1 2 3 4 5 volumes: - ./config:/config - type: bind source: /media/Disk8T target: /data/media 网站初始化和设置 # 安装好Jellyfin之后可以通过浏览器访问网站，可以在地址栏输入http://0.0.0.0:8081，如果 nginx已经设置好，也可以通过网址来访问：http://m.jinli.cyou。\n首次登录需要设置管理员账户和密码，然后就可以进入jellyfin的管理界面了。\n添加影视库 # 然后可以创建影视库，将在硬盘上存放的影视资源加入影视库。由于在创建docker容器时我们把存放影视资源的文件夹映射到了/data/media文件夹内，所以我们可以通过在/media/Disk8T内创建不同的文件夹，然后在jellyfin里创建对应的影视库。 例如我们在/media/Disk8T内创建了一个名为movies和一个music的文件夹，那么我们就可以在jellyfin里创建一个名为My Movies的影视库和一个名为My Music的音乐库，然后指定/data/media/movies作为Movies的源路径，指定/data/media/music/song作为My Music的源路径。\n创建影视库的方法是\n登录jellyfin的管理员账户，点击左上角三条横线的图标，展开菜单栏，然后点击Dashboard。 点击左侧菜单栏的Libraries，然后点击右上角的Add Library。 选择影视库的类型，例如Music，然后点击Next。 输入影视库的名称，例如My Music，然后指定影视库的源路径，例如/data/media/music/song，然后点击OK。 安装插件 # jellyfin有很多有用的插件，可以在Dashboard的Plugins里安装插件。\n安装插件前需要先把插件仓库地址添加到jellyfin的插件仓库列表里，这里推荐两个插件仓库：\nhttps://repo.jellyfin.org/releases/plugin/manifest-stable.json https://gitee.com/jjm2473/jellyfin-plugins-repo/raw/master/manifest.json 推荐安装下面几个插件： 其中TMDb和OMDb插件可以用来获取影视的信息，例如海报、简介等，Meiam.Shooter、Meiam.Thunder插件可以用来获取影视的字幕。\n安装完插件后需要重启jellyfin容器使插件生效。\n乱码问题 # 当系统和docker容器内缺乏字幕文件中指定的字体文件时，有些中文字体会显示方框。想要彻底解决这个问题需要在docker容器内安装中文字体，并且给jellyfin设置备用字体。安装完成后需要重启jellyfin容器使字体生效。\n给jellyfin设置备用字体 # jellyfin内的备用字体需要使用woff2格式的字体，这里我们选用微软雅黑和Noto Sans字体。\n字体文件下载后需要放在jellyfin安装目录下的config文件夹内，然后在jellyfin的管理界面里设置备用字体，具体操作如下：\n在jellyfin安装目录下的config文件夹内创建一个名为fonts的文件夹，然后将下载的字体文件放在fonts文件夹内。 进入jellyfin设置界面，在左侧菜单栏点击Playback，设置Fallback font folder path为/config/fonts。 在docker容器内安装字体 # 先进入jellyfin容器内：\n1 sudo docker exec -it jellyfin /bin/bash 然后在docker内安装中文字体：\n1 2 apt update apt install fonts-noto-cjk-extra 给系统安装中文字体 # 如果需要在服务器电脑本地播放视频并显示字幕，最好给服务器系统也安装中文字体。\n这个比较简单，只需下载tff格式的中文字体文件，然后双击安装就行了。这里推荐安装思源黑体。\n","date":"2023年8月26日","externalUrl":null,"permalink":"/p/%E4%BD%BF%E7%94%A8jellyfin%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E7%BA%BF%E4%B8%8A%E5%BD%B1%E8%A7%86%E5%BA%93/","section":"Posts","summary":" 缘起 # 之前下载了不少影视资源，放在家里主机的硬盘里，但是想在其他地方或自己的其他设备上观看就不太方便了。于是想到搭建一个线上影视库，可以在任何地方通过网络观看自己的影视资源。 在网上看到了很多搭建线上影视库的方法，比如使用plex、emby、jellyfin等软件，这些软件都是开源的，可以在自己的电脑或服务器上搭建，然后通过网页或客户端来观看自己的影视资源。 由于plex和emby的许多高级功能都需要付费版才能用，而jellyfin则是所有功能都免费，这里我选择使用jellyfin来搭建个人线上影视库。\n","title":"使用jellyfin搭建个人线上影视库","type":"post"},{"content":"","date":"2023年8月5日","externalUrl":null,"permalink":"/tags/calibre-web/","section":"Tags","summary":"","title":"Calibre-Web","type":"tags"},{"content":"","date":"2023年8月5日","externalUrl":null,"permalink":"/tags/ebook/","section":"Tags","summary":"","title":"Ebook","type":"tags"},{"content":"","date":"2023年8月5日","externalUrl":null,"permalink":"/tags/personal-library/","section":"Tags","summary":"","title":"Personal Library","type":"tags"},{"content":" 缘起 # 之前下载过一些电子书，都存在了云盘上，但是每次想在不同的设备上看都需要先下载下来（虽然在云盘上也 可以预览，但还是觉得下载下来用专用的阅读器体验更好）。而且电子书一多，放在云盘里管理起来就比较 混乱。最近偶然看到了calibre-web这个工具，可以用它自己搭建一个电子书库，感觉效果挺不错的。\n前提 # docker和docker compose 域名（二级域名即可） acme.sh （生成SSL证书） nginx （反向代理） 准备工作 # 准备域名 # 在自己购买域名的网站上或者自己域名的管理网站上创建一个二级域名并配置域名解析，具体过程可参见\u0026ldquo;个人网站的建立过程（一）：购买个人域名并配置动态域名解析\u0026rdquo;。\n例如我有一个主域名jinli.cyou，于是我为私有邮箱服务器创建了一个\u0026quot;book.jinli.cyou\u0026quot;的二级域名。然后在阿里云的域名管理平台上进行了域名和IP地址的绑定。\n使用acme.sh生成SSL证书 # 网上很多教程都使用Let\u0026rsquo;s Encrypt来生成SSL证书，但由于我之前在搭建这个网站时已经下载了acme.sh工具，因此这里我使用了acme.sh来为\u0026quot;book.jinli.cyou\u0026quot;生成证书。具体过程可参见个人网站的建立过程（二）：使用Hugo框架搭建个人网站。\n过程如下：\n找到你的域名管理账户的Access key。可以使用之前的，也可以重新获取，然后将Access key到处为系统变量Ali_Key和Ali_Secret。\n使用下面的命令生成证书：\n1 acme.sh --issue --dns dns_ali -d your.domain.com 使用下面的命令将证书复制到你的mailcow目录下，例如我在mailcow安装目录下新建了一个文件夹cert来存放证书文件：\n1 2 3 acme.sh --install-cert -d your.domain.com \\ --key-file /media/calibre/cert/key.pem \\ --fullchain-file /media/calibre/cert/cert.pem nginx反向代理配置 # 因为我之前在建立本网站时就安装了nginx，所以这里我使用了nginx作为反向代理工具。\n在nginx的配置目录下创建一个新的配置文件mailcow.conf作为云网盘的反向代理配置文件，文件内写入：\nserver{\nlisten 80;\nserver_name book.jinli.cyou;\nreturn 301 https://book.jinli.cyou$request_uri;\n}\nserver{\nlisten 443 ssl;\nssl_certificate /media/lijin/book/certs/cert.pem;\nssl_certificate_key /media/lijin/book/certs/key.pem;\nserver_name book.jinli.cyou;\nlocation / {\nproxy_redirect off;\nproxy_pass http://book.jinli.cyou;\nproxy_set_header Host $http_host;\nproxy_set_header X-Real-IP $remote_addr;\nproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\nproxy_set_header X-Forwarded-Proto $scheme;\nclient_max_body_size 1000m;\n}\n}\n这里我使用8080端口作为mailcow的服务端口。SSL证书存放在上一步指定的位置。\n配置完成后重启nginx服务使修改生效：\n1 sudo service nginx restart 安装calibre-web # 这里使用docker-compose来安装calibre-web，具体操作参照https://hub.docker.com/r/linuxserver/calibre-web。\n网站初始化和设置 # 安装好calibre-web之后可以通过浏览器访问网站，可以在地址栏输入http://0.0.0.0:8080，如果 nginx已经设置好，也可以通过网址来访问：http://book.jinli.cyou。\n默认的管理员账户和密码是admin和admin123。登录之后可以进行初始化设置。\n首先需要下载一个空的数据库文件metadata.db，放在calibre-web安装目录下的library文件夹内， 例如/media/calibre-web/library。\n然后在calibre-web里指定存储数据库的位置为/books，注意docker里的/books实际上就对应 服务器上的/media/calibre-web/library。 此时可能因为文件夹的读写权限问题，calibre-web依然会报错： 。\n需要进入docker更改数据库文件所在文件夹的读写权限。具体命令如下：\n1 2 3 4 docker exec -it calibre_web /bin/bash chmod 777 books cd books chmod 766 metadata.db 然后可以进行一些其他设置，如服务器端口，SSL证书等。\n添加电子书 # 设置好之后就可以往书库里添加电子书了。添加完之后可以创建不同类别的书架，对书籍进行分类。 。\n","date":"2023年8月5日","externalUrl":null,"permalink":"/p/%E4%BD%BF%E7%94%A8calibre-web%E5%92%8Cdocker%E8%87%AA%E5%BB%BA%E4%B8%AA%E4%BA%BA%E6%95%B0%E5%AD%97%E5%9B%BE%E4%B9%A6%E9%A6%86/","section":"Posts","summary":" 缘起 # 之前下载过一些电子书，都存在了云盘上，但是每次想在不同的设备上看都需要先下载下来（虽然在云盘上也 可以预览，但还是觉得下载下来用专用的阅读器体验更好）。而且电子书一多，放在云盘里管理起来就比较 混乱。最近偶然看到了calibre-web这个工具，可以用它自己搭建一个电子书库，感觉效果挺不错的。\n","title":"使用calibre-web和docker自建个人数字图书馆","type":"post"},{"content":" 缘起 # 既然有了私有的域名，就可以充分利用一下，搭建一个私有的邮件服务器，这样就可以创建自己专属的邮箱，邮件的所有信息也都可以由自己完全掌控，更加安全。\n私有邮件服务器有多种部署方式，因为我之前就已经在服务器上安装了docker，服务器上的其他软件工具也都使用docker进行容器化部署运行，因此这里的邮件服务器也选择用比较方便容器化的工具来部署。\n在网上看到用得比较多的搭建邮件服务器的工具有Mailu和mailcow。最开始感觉Mailu的设置比较简单，界面也比较清爽，就试着弄了一下，但费了很大功夫也没能成功发送和接收邮件，最后只得作罢。\nmailcow是另一个开源的邮箱服务器套装，包含搭建私有邮件服务器的必要软件包。mailcow官方提供用于docker容器化的安装指南，部署在服务器上比较方便。 然后测试了一下mailcow，能成功接收邮件，但对外发送邮件失败了，后来发现是因为我使用的运营商关闭了25端口，这时我已接近放弃自己搭建邮件服务器的想法。但后来看到网上说可以使用邮件中继/邮件转发作为替代方案，于是有花了很长时间摸索，终于用邮件中继解决了对外发送的问题。这里记录一下折腾这个邮件服务器的过程。\n前提 # docker和docker compose 域名（二级域名即可） acme.sh （生成SSL证书） nginx （反向代理） 准备工作 # 准备域名 # 在自己购买域名的网站上或者自己域名的管理网站上创建一个二级域名并配置域名解析，具体过程可参见\u0026ldquo;个人网站的建立过程（一）：购买个人域名并配置动态域名解析\u0026rdquo;。\n例如我有一个主域名jinli.cyou，于是我为私有邮箱服务器创建了一个\u0026quot;mail.jinli.cyou\u0026quot;的二级域名。然后在阿里云的域名管理平台上进行了域名和IP地址的绑定。\n使用acme.sh生成SSL证书 # 网上很多教程都使用Let\u0026rsquo;s Encrypt来生成SSL证书，但由于我之前在搭建这个网站时已经下载了acme.sh工具，因此这里我使用了acme.sh来为\u0026quot;cloud.jinli.cyou\u0026quot;生成证书。具体过程可参见个人网站的建立过程（二）：使用Hugo框架搭建个人网站。\n过程如下：\n找到你的域名管理账户的Access key。可以使用之前的，也可以重新获取，然后将Access key到处为系统变量Ali_Key和Ali_Secret。\n使用下面的命令生成证书：\n1 acme.sh --issue --dns dns_ali -d your.domain.com 使用下面的命令将证书复制到你的mailcow目录下，例如我在mailcow安装目录下新建了一个文件夹cert来存放证书文件：\n1 2 3 acme.sh --install-cert -d your.domain.com \\ --key-file /media/mailcow/cert/key.pem \\ --fullchain-file /media/mailcow/cert/cert.pem nginx反向代理配置 # 因为我之前在建立本网站时就安装了nginx，所以这里我使用了nginx作为反向代理工具。\n在nginx的配置目录下创建一个新的配置文件mailcow.conf作为云网盘的反向代理配置文件，文件内写入：\nserver{\nlisten 80;\nserver_name mail.jinli.cyou;\nreturn 301 https://mail.jinli.cyou$request_uri;\n}\nserver{\nlisten 443 ssl;\nssl_certificate /media/lijin/mailcow/certs/cert.pem;\nssl_certificate_key /media/lijin/mailcow/certs/key.pem;\nserver_name mail.jinli.cyou;\nlocation / {\nproxy_pass http://127.0.0.1:8080/;\nproxy_set_header Host $http_host;\nproxy_set_header X-Real-IP $remote_addr;\nproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\nproxy_set_header X-Forwarded-Proto $scheme;\nclient_max_body_size 0;\nproxy_buffer_size 128k;\nproxy_buffers 64 512k;\nproxy_busy_buffers_size 512k;\n}\n}\n这里我使用8080端口作为mailcow的服务端口。SSL证书存放在上一步指定的位置。\n配置完成后重启nginx服务使修改生效：\n1 sudo service nginx restart 安装mailcow # 使用docker-compose安装mailcow的方法可以参见mailcow官方提供的教程。\n需要注意的是，安装mailcow需要使用docker compose而非docker-compose。\n添加新用户等后续设置 # 以管理员身份登录进mailcow管理界面后，点击右上角的管理员账户头像，在下拉菜单中可以看到“Users”选项。点击这个选项，就可以添加用户组和新用户了。\n邮箱中继（邮箱转发） # 很多运营商都会屏蔽25端口，这会导致邮件无法从服务器发送出去。因此一种选择是联系运营商，让运营商解封25端口，但这并不容易。\n另一种解决方案是使用邮箱中继（邮箱转发）服务，即将需要从自建邮箱A发出的邮件交给另一个有发送邮件功能的邮箱服务器B，将要发送的邮件由邮箱服务器B代发出去。通常，邮箱服务器B可以是Gmail这样的 邮件服务商，可以是专门提供邮箱转发服务的商业公司（如Mailgun、SendGrid等），有些网络运营商（如AT\u0026amp;T）也会为用户提供邮件转发服务。\n由于Gmail在提供邮件转发服务时，发送的邮件会加上Google的签名信息，而Mailgun和SendGrid等商业邮件转发需要付费才能使用，因此这里我使用AT\u0026amp;T提供的免费邮件转发服务。\n中继邮箱设置 # 不同的邮箱可能有不同的设置方式，这里以AT\u0026amp;T提供的邮箱为例。\n在设置中找到Mailboxes选项，里面有Send-only email address，点击Add添加你需要代理转发的邮箱。例如我要使用AT\u0026amp;T邮箱替我的 个人邮箱i@jinli.cyou发送邮件，我就添加一个i@jinli.cyou。\n点击确认之后，AT\u0026amp;T会向代转发的邮箱发送一封邮件来核实身份。登录进个人邮箱i@jinli.cyou之后，按照邮件的说明点击链接确认就行了。\n邮箱核实成功之后，上面的AT\u0026amp;T设置里提示\u0026quot;Not verified\u0026quot;的红色提示会消失（可能需要等待几分钟到几小时）。\n注意事项：\n在点击验证链接验证之后，可能需要等待几分钟到几小时，AT\u0026amp;T邮箱设置里的\u0026quot;Not verified\u0026quot;红色提示才会消失。\n有时点击AT\u0026amp;T发送的验证链接并不能正确跳转到验证页面，这可能是因为浏览器中有之前登录AT\u0026amp;T邮箱中的缓存问题，这时可以手动复制链接到新的隐私浏览器或者别的浏览器中打开。\nmailcow设置 # 按照mailcow官方文档给出的设置邮箱转发的步骤为个人邮箱设置邮箱转发：https://docs.mailcow.email/manual-guides/Postfix/u_e-postfix-relayhost/。\n主要分为三步：\n登录mailcow管理员界面，在Configuration and Details设置中找到Routing，添加上你的转发服务器，例如我使用AT\u0026amp;T的邮箱，就使用smtp.mail.att.net:587。 然后填上你的用户名和密码，建议在代理邮箱里重新设置一个给外部使用的密码。\n设置好之后点击Test按钮，填上要代理的邮箱进行测试。若一切顺利，则可以看到最后发送成功的绿色的提示信息。若收到红色的失败信息，则表示设置的不对，需要检查前面的设置。\n在mailcow的管理界面，进入Mail Setup，找到Domains。把前面在Routing中设置的代理邮箱添加到Sender-dependent transports。\n测试 # 在Mail tester可以测试一下自己搭建的邮箱效果怎么样。\n分数不高，将将及格，但也基本够用了。\n想要提升分数的话可以看一下扣分的项，根据Mail tester的建议进行改进。\n","date":"2023年6月17日","externalUrl":null,"permalink":"/p/%E4%BD%BF%E7%94%A8mailcow%E5%92%8Cdocker%E8%87%AA%E5%BB%BA%E7%A7%81%E4%BA%BA%E9%82%AE%E7%AE%B1%E6%9C%8D%E5%8A%A1%E5%99%A8/","section":"Posts","summary":" 缘起 # 既然有了私有的域名，就可以充分利用一下，搭建一个私有的邮件服务器，这样就可以创建自己专属的邮箱，邮件的所有信息也都可以由自己完全掌控，更加安全。\n","title":"使用mailcow和docker自建私人邮箱服务器","type":"post"},{"content":"","date":"15 四月 2023","externalUrl":null,"permalink":"/en/tags/cluster/","section":"Tags","summary":"","title":"Cluster","type":"tags"},{"content":"","date":"15 四月 2023","externalUrl":null,"permalink":"/en/tags/hpc/","section":"Tags","summary":"","title":"HPC","type":"tags"},{"content":"","date":"15 四月 2023","externalUrl":null,"permalink":"/en/tags/scientific-computing/","section":"Tags","summary":"","title":"Scientific Computing","type":"tags"},{"content":" 简介 # Slurm是一个广泛使用的Linux和Unix系统的工作负载管理器和作业调度程序。它允许用户在集群环境中提交和管理作业。\n在集群上管理作业的一个重要方面是作业优先级控制。在本文中，我们将讨论如何使用Slurm中的\u0026quot;\u0026ndash;nice\u0026quot;选项来设置作业的优先级。\n缘起 # 考虑这样一个场景：\n用户A和用户B共用一个计算集群（Cluster）。 用户A提交了大量的计算任务占用了所有可用的计算节点（node），而且还有一些计算任务正在等待，但单个计算任务所花的时间并不是特别长。 这时用户B需要提交一个计算任务，这个计算任务所需的计算资源（核心数、内存等）很少。但由于B提交这个计算任务晚于A提交的大量任务，所以B的这个任务必须等A的所有计算任务全部完成后才能启动。 在这种场景下，用户B需要的计算资源虽然很少，但由于提交计算任务较晚，所以必须等待很长时间才能开始计算，这就浪费了用户B大量的时间。\n而如果能让用户B的这个计算任务排在用户A正在等待的计算任务前面， 那么一旦用户A正在运行的某个任务结束，用户B的任务立刻就能开始计算；用户B的计算完成后，用户A可以继续使用B空下来的计算资源。这样B可以节省大量的等待时间，而A增加的等待时间微乎其微。\n所以在这种情况下，使B后提交的计算任务的优先级高于A先提交的计算任务就非常重要。\n管理作业优先级 # Slurm中的作业优先级 # 通常，通过Slurm提交的作业按照\u0026quot;先进先出（First In, First Out, FIFO）\u0026ldquo;的原则确定优先级。\n但有时集群管理员也会通过配置“多因子优先级插件”来计算作业的优先级，这时作业的优先级取决于作业规模、排队时间、关联性、分区等多种因素。\n调整作业优先级的方法 # 对管理员而言，可以通过指定或更改作业的\u0026quot;priority\u0026quot;选项的值直接管理作业的优先级。 对普通用户而言，通常不允许直接指定或更改作业的\u0026quot;priority\u0026quot;选项。但上面提到的决定作业优先级的多个因子中有一个\u0026quot;nice\u0026quot;因子，是普通用户可以指定的。\u0026ldquo;nice\u0026quot;因子可以理解为\u0026quot;好人值\u0026rdquo;， 其值越大，作业的优先级越低，即表示你对其他用户更友好。 \u0026ldquo;nice\u0026quot;选项的用法 # 指定和更改 # 用户可以在提交作业时指定\u0026quot;nice\u0026quot;的值：\n1 sbatch --nice=100 your_slurm_script 用户也可以更新已经提交完正在等待启动的作业的\u0026quot;nice\u0026quot;值：\n1 scontrol update JobId=\u0026lt;job_id\u0026gt; Nice=\u0026lt;new_nice_value\u0026gt; 查看作业优先级 # 用户可以查看某个已提交作业的优先级： 1 scontrol show job=\u0026lt;job_id\u0026gt; | grep Priority 注意事项 # 默认的\u0026quot;nice\u0026quot;值是0。 普通用户只能指定一个正的\u0026quot;nice\u0026quot;值，管理员才可以指定负的\u0026quot;nice\u0026quot;值。即普通用户只能做“好人”，管理员才能做“坏人”。 ","date":"2023年4月15日","externalUrl":null,"permalink":"/p/slurm-%E6%97%A0%E9%9C%80%E7%AE%A1%E7%90%86%E5%91%98%E6%9D%83%E9%99%90%E6%9B%B4%E6%94%B9%E8%AE%A1%E7%AE%97%E4%BB%BB%E5%8A%A1%E7%9A%84%E4%BC%98%E5%85%88%E7%BA%A7/","section":"Posts","summary":" 简介 # Slurm是一个广泛使用的Linux和Unix系统的工作负载管理器和作业调度程序。它允许用户在集群环境中提交和管理作业。\n","title":"Slurm: 无需管理员权限更改计算任务的优先级","type":"post"},{"content":"","date":"15 四月 2023","externalUrl":null,"permalink":"/en/tags/workload-manager/","section":"Tags","summary":"","title":"Workload Manager","type":"tags"},{"content":"","date":"2023年4月15日","externalUrl":null,"permalink":"/tags/%E9%AB%98%E6%80%A7%E8%83%BD%E8%AE%A1%E7%AE%97/","section":"Tags","summary":"","title":"高性能计算","type":"tags"},{"content":"","date":"2023年4月15日","externalUrl":null,"permalink":"/tags/%E8%AE%A1%E7%AE%97%E9%9B%86%E7%BE%A4/","section":"Tags","summary":"","title":"计算集群","type":"tags"},{"content":"","date":"2023年4月15日","externalUrl":null,"permalink":"/tags/%E8%AE%A1%E7%AE%97%E4%BC%98%E5%85%88%E7%BA%A7%E7%AE%A1%E7%90%86/","section":"Tags","summary":"","title":"计算优先级管理","type":"tags"},{"content":"","date":"2023年4月15日","externalUrl":null,"permalink":"/tags/%E7%A7%91%E5%AD%A6%E8%AE%A1%E7%AE%97/","section":"Tags","summary":"","title":"科学计算","type":"tags"},{"content":"","date":"2023年2月21日","externalUrl":null,"permalink":"/tags/chevereto/","section":"Tags","summary":"","title":"Chevereto","type":"tags"},{"content":"","date":"21 二月 2023","externalUrl":null,"permalink":"/en/tags/cloud-photo-album/","section":"Tags","summary":"","title":"Cloud Photo Album","type":"tags"},{"content":"","date":"21 二月 2023","externalUrl":null,"permalink":"/en/tags/google-photo-alternative/","section":"Tags","summary":"","title":"Google Photo Alternative","type":"tags"},{"content":"","date":"2023年2月21日","externalUrl":null,"permalink":"/tags/google-photo%E6%9B%BF%E4%BB%A3%E5%93%81/","section":"Tags","summary":"","title":"Google Photo替代品","type":"tags"},{"content":" 缘起 # 之前在自己服务器上用Nextcloud搭建了一个自己的云网盘（详见\u0026ldquo;使用Nextcloud和docker自建个人云网盘\u0026rdquo;），也把自己在各个设备上的照片都备份到了云网盘上。 然后发现Nextcloud在移动设备的App里有自动上传的选项，因此就产生了用它来替代Google Photo，自建一个人云相册的想法。\n后来朋友告诉我学校开始把Google云空间的容量限制到只有250G，而且毕业后会进一步限制到只有15G，因此我开始正式用Nextcloud搭建云相册，以替换掉Google Photo。\n使用方法 # 在手机上安装Nextcloud客户端，登录自己的Nextcloud个人网盘。 在Nextcloud网盘里新建一个文件夹用来存放相册，例如叫做\u0026quot;Photos\u0026quot;。 打开Nextcloud手机App，点击左上方的三条横线进入菜单页面，点击“设置（Settings）”选项进入设置界面。 点击“自动上传（Auto Upload）”选项，进入自动上传的设置界面。 自动上传的设置界面会列出手机中的各个相册，每个相册右方都有一个白云图标和三个点，点击三个点再点击“配置（Config）”可以设置上传的路径、仅WIFI网络上传等。 配置好后点击空白且划了斜线的白云图标，使之变为蓝色，自动上传就设置好了。 注意：一定要先配置上传路径，再开启白云图标，否则Nextcloud会把相册上传到默认的文件夹，而不是你想指定的文件夹！\n也可以给其他手机、平板、电脑等做类似的设置。可以将相册都上传到同一个文件夹，也可以分别上传到不同的文件夹。这样就可以实现多设备上的相册在云上的同步啦！\n","date":"2023年2月21日","externalUrl":null,"permalink":"/p/google-photo%E6%9B%BF%E4%BB%A3%E5%93%81%E4%BD%BF%E7%94%A8nextcloud%E5%AE%9E%E7%8E%B0%E5%8F%AF%E5%A4%9A%E8%AE%BE%E5%A4%87%E5%90%8C%E6%AD%A5%E7%9A%84%E4%B8%AA%E4%BA%BA%E4%BA%91%E7%9B%B8%E5%86%8C/","section":"Posts","summary":" 缘起 # 之前在自己服务器上用Nextcloud搭建了一个自己的云网盘（详见“使用Nextcloud和docker自建个人云网盘”），也把自己在各个设备上的照片都备份到了云网盘上。 然后发现Nextcloud在移动设备的App里有自动上传的选项，因此就产生了用它来替代Google Photo，自建一个人云相册的想法。\n","title":"Google Photo替代品：使用Nextcloud实现可多设备同步的个人云相册","type":"post"},{"content":"","date":"2023年2月21日","externalUrl":null,"permalink":"/tags/nextcloud/","section":"Tags","summary":"","title":"Nextcloud","type":"tags"},{"content":" 缘起 # 在网上写博客时有可能同一篇文章要上传到不同的平台，例如有些文章既会放在本网站上，也会放在项目的GitHub仓库的README里。 这时如果文章中的图片使用本地图片的话，就需要在不同平台上各放一份，不易于统一更新和管理。因此就产生了搭建一个个人图床 来统一管理这些图片的想法。\n使用私人图床，写文章引用图片时直接插入图片在私人图床中的链接，便于之后的统一管理。\n前置条件 # docker和docker compose 域名（二级域名即可） acme.sh （生成SSL证书） nginx （反向代理） 关于上述软件和工具的安装和准备，详情请参见\u0026ldquo;使用Nextcloud和docker自建个人云网盘\u0026rdquo;\n安装Chevereto # 配置docker-compose # 由于我们在上面已经生成了SSL证书并配置了nginx，因此在docker中我们只需要配置Chevereto即可。\n这里我在服务器的硬盘上单独划了一个分区作为Chevereto的安装根目录。在Chevereto安装根目录下创建docker-compose.yml文件，写入以下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 version: \u0026#34;3.2\u0026#34; services: database: container_name: chevereto-free_database image: mariadb:focal networks: - chevereto_network volumes: - ./database:/var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: your_root_password MYSQL_DATABASE: chevereto MYSQL_USER: chevereto MYSQL_PASSWORD: your_user_password chevereto: container_name: chevereto-free_app image: ghcr.io/rodber/chevereto-free:1.6 networks: - chevereto_network volumes: - ./images:/var/www/html/images/ - ./content:/var/www/html/content/ ports: - 8810:80 restart: always environment: CHEVERETO_TAG: \u0026#34;free\u0026#34; CHEVERETO_DB_HOST: database CHEVERETO_DB_USER: chevereto CHEVERETO_DB_PASS: your_chevereto_password CHEVERETO_DB_PORT: 3306 CHEVERETO_DB_NAME: chevereto CHEVERETO_DISABLE_UPDATE_HTTP: 1 CHEVERETO_DISABLE_UPDATE_CLI: 1 CHEVERETO_HTTPS: 0 networks: chevereto_network: 这里需要把“MYSQL_ROOT_PASSSWORD”和“MYSQL_PASSWORD”改为你自己设置的数据库密码，也可以把“MYSQL_DATABASE”和“MYSQL_USER”的名字改掉。\n用docker-compose生成容器 # 在Nextcloud安装根目录下执行如下命令来生成容器：\n1 sudo docker-compose up -d 域名证书和反向代理 # 用acme.sh生成免费的域名证书 # 具体过程可参见个人网站的建立过程（二）：使用Hugo框架搭建个人网站。\n主要步骤如下：\n使用下面的命令生成证书：\n1 acme.sh --issue --dns dns_ali -d your.domain.name 使用下面的命令将证书复制到你的nextcloud目录下，例如我在nextcloud安装目录下新建了一个文件夹cert来存放证书文件：\n1 2 3 acme.sh --install-cert -d your.domain.name \\ --key-file your_cert_path/key.pem \\ --fullchain-file your_cert_path/cert.pem 用nginx设置反向代理 # 在nginx的配置目录下创建一个新的配置文件nextcloud.conf作为云网盘的反向代理配置文件，文件内写入：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 upstream your.domain.name { server 127.0.0.1:8810; } server { listen 80; server_name your.domain.name; return 301 https://your.domain.name$request_uri; } server { listen 443 ssl; server_name your.domain.name; gzip on; # ssl 配置 ssl_certificate your_cert_path/cert.pem; ssl_certificate_key your_cert_path/key.pem; location / { proxy_redirect off; proxy_pass http://your.domain.name; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Ssl on; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Frame-Options SAMEORIGIN; client_max_body_size 100m; client_body_buffer_size 128k; proxy_buffer_size 4k; proxy_buffers 4 32k; proxy_busy_buffers_size 64k; proxy_temp_file_write_size 64k; } } 这里我使用8810端口作为Chevereto的服务端口，限制用户上传文件最大为100 M。SSL证书存放在上一步指定的位置。\n配置完成后重启nginx服务使修改生效：\n1 sudo service nginx restart 初始化和后续设置 # 完成上述步骤后就可以从浏览器访问自己的私人图床啦！在浏览器内输入图床的网址，例如我的是https://img.jinli.io。就会进入如下页面：\n我忘记第一次进入这个页面是否还需要做额外的设置了，但只要按照提示应该就很容易完成。\n然后按照提示创建用户名、密码，就可以登录进入了！\n","date":"2023年2月21日","externalUrl":null,"permalink":"/p/%E4%BD%BF%E7%94%A8chevereto-free%E6%90%AD%E5%BB%BA%E7%A7%81%E4%BA%BA%E5%9B%BE%E5%BA%8A/","section":"Posts","summary":" 缘起 # 在网上写博客时有可能同一篇文章要上传到不同的平台，例如有些文章既会放在本网站上，也会放在项目的GitHub仓库的README里。 这时如果文章中的图片使用本地图片的话，就需要在不同平台上各放一份，不易于统一更新和管理。因此就产生了搭建一个个人图床 来统一管理这些图片的想法。\n","title":"使用Chevereto-free搭建私人图床","type":"post"},{"content":"","date":"2023年2月21日","externalUrl":null,"permalink":"/tags/%E4%BA%91%E7%9B%B8%E5%86%8C/","section":"Tags","summary":"","title":"云相册","type":"tags"},{"content":"","date":"2023年1月23日","externalUrl":null,"permalink":"/tags/barrier/","section":"Tags","summary":"","title":"Barrier","type":"tags"},{"content":"","date":"2023年1月23日","externalUrl":null,"permalink":"/tags/deskflow/","section":"Tags","summary":"","title":"Deskflow","type":"tags"},{"content":"","date":"2023年1月23日","externalUrl":null,"permalink":"/tags/inputleap/","section":"Tags","summary":"","title":"InputLeap","type":"tags"},{"content":"","date":"23 一月 2023","externalUrl":null,"permalink":"/en/tags/kvm-software/","section":"Tags","summary":"","title":"KVM Software","type":"tags"},{"content":"","date":"2023年1月23日","externalUrl":null,"permalink":"/tags/kvm%E8%BD%AF%E4%BB%B6/","section":"Tags","summary":"","title":"KVM软件","type":"tags"},{"content":" 缘起 # 工作中经常需要用到不止一台电脑，例如办公室有一台Linux系统的台式机，另有一台macOS的笔记本电脑 用于上课或者开会。有时完成某个任务需要同时用到这两台电脑，这时用两套鼠标和键盘来回切换就会变得 非常麻烦，Barrier就是一款可以在多个电脑间跨系统共享鼠标键盘的免费软件。\n更新：Barrier已于2021年停止维护，现推荐使用InputLeap。InputLeap和Barrier是同源的，功能和使用方法与Barrier几乎一致。本文所讲的Barrier的安装和设置基本也都适用于InputLeap。\n更新2：除了InputLeap之外，2024年10月发布的Deskflow也是一款同源的免费KVM软件，功能和使用方法与Barrier/InputLeap几乎一致，但目前来看Deskflow的更新更频繁一些，而且我在安装时直接安装的最新版本（v1.24.0），并没有出现版本兼容性问题。因此现在我个人更推荐使用Deskflow。\n简介 # 键鼠共享软件其实不止barrier这一款，其他的还有Mouse Without Borders、Synergy等。但 Mouse Without Borders只适用于Windows系统，而Synergy是一款收费软件。\nBarrier是和Synergy同源的一款软件。最初Synergy是基于Chris Schoeneman编写的 CosmoSynergy开发的一个免费软件，Synergy在迭代数个版本后开始收费，于是有团队开始基于 Synergy的开源内核再次开发出了免费版的barrier。\nBarrier是一款免费且开源的跨系统键盘鼠标共享软件，主要的特点有：\n共享鼠标和共享键盘 共享剪切板 跨系统。适用于Windows、macOS和Linux系统 免费、开源 安装 # Windows和macOS # Windows和macOS系统有已经打包好的安装包，可以从Barrier开源GitHub仓库的发布页面下载： https://github.com/debauchee/barrier/releases。Windows系统选择后缀为exe的安装包，macOS系统选择后缀为dmg的安装包。\nWindows # Windows下直接双击安装包一步一步安装即可。\nmacOS # macOS下的安装相对复杂一些，主要是因为Barrier或InputLeap的发布者未经认证，所以安装时会有一些安全方面的限制。因此我们需要手动绕过这些设置。\n在macOS下下载安装包时需要注意自己的电脑是Intel芯片还是苹果芯片的————Intel芯片的下载x86版本的dmg安装包，苹果芯片的下载AppleSilicon版本的安装包。另外，我使用的macOS版本是Sequoia 15.5，使用InputLeap 3.0.3版本的安装包之后会显示无法安装，而InputLeap 3.0.2版本没问题，因此这里使用InputLeap 3.0.2版本。在Fedora 42系统下InputLeap 3.0.2版本在设置为服务端时会闪退，3.0.3版本没有问题。\n双击安装包后会显示因安全原因无法安装，点击右上角的问号图标，会打开如下窗口：\n然后根据提示开启安全设置，再安装Barrier或者InputLeap。\n注意，直接点击上述页面中的链接打开“Security”设置界面后并不会看到打开Barrier或者InputLeap的选项。这时需要关闭Barrier或者InputLeap的安装窗口，重新双击开启安装就可以看到“Open Anyway”选项了。\nLinux # Debian系的Linux系统（如Ubuntu等），可以直接使用包管理工具apt安装：\n1 sudo apt install barrier 其他Linux系统可以通过包管理工具snap安装：\n1 sudo snap install barrier 如果系统没有自带snap，则需先安装snap。\n设置和使用 # barrier的设置分为服务端（server）和客户端（client）。\n服务端 # 直接连接键盘鼠标的那台电脑是服务端。\n打开服务端电脑上的Barrier，勾选Server。 记下服务端的IP地址（一般是局域网IP）。 勾选手动配置服务端，再点击“配置服务端”（Configure Server）。 拖动配置界面右上侧的电脑图标到下方的格子里，双击电脑图标，更改电脑名称为客户端电脑显示的名称（可以在客户端barrier软件界面“屏幕名称”一栏找到）。两个电脑图标在格子里的相对位置和实际电脑的屏幕位置相对应。 注意：这一步非常重要，如果屏幕名称不匹配，则无法共享成功。 上述设置保存后，点击重新加载（Reload）按钮，或者直接重启Barrier软件。 客户端 # 要使用服务端电脑键盘鼠标的电脑是客户端。\n打开客户端电脑上的Barrier，勾选Client。 在服务端IP一栏（Server IP）填入服务端的IP地址。如果这一栏不可编辑，则取消勾选“自动配置”（Auto config）。 点击Barrier菜单栏的“Barrier”，选择“更改设置”，确保使用的网络端口（Port）是24800，并勾选“Enable SSL”。 重启或重载（Reload）Barrier。 常见问题 # 无法连接 # 确保服务端和客户端在同一个局域网内。\n如果鼠标键盘不能成功共享，可以检查上述设置，并重启Barrier软件。\n如果还是不行，可以点击菜单栏“显示日志”（Show Log），查看日志中的报错信息。\nWayland下剪切板无法共享 # 虽然Barrier/InputLeap/Deskflow都声称支持共享剪切板，但我在Wayland下实测发现，截止目前（2025年10月），无论是InputLeap或者Deskflow都无法实现剪切板共享功能。\nDeskflow的讨论区中表示Wayland环境下剪切板的功能需要上游工具的支持，详情参见Issue #1803。\n所以如果坚持使用Wayland的话，目前只能放弃剪切板共享功能了。如果你需要这个功能，可以考虑切换到X11环境下使用。\n但是如果你确实既想使用Wayland又想要剪切板共享功能，可以考虑使用第三方工具来实现。例如：\nCopyQ：一个跨平台的剪切板管理软件。CopyQ可以通过网络同步剪切板内容，支持Windows、macOS和Linux系统。 ClipboardFusion：一个功能强大的剪切板管理工具，支持Windows和macOS系统。ClipboardFusion可以通过云同步剪切板内容。 ClipCascade：一个跨平台的剪切板管理工具，支持Windows、macOS和Linux系统。ClipCascade可以通过网络同步剪切板内容，并且支持自建服务器与加密传输。 我选择使用ClipCascade，因为它是开源免费的，并且支持自建服务器与加密传输，安全性更高一些。ClipCascade的使用方法可以参考它的GitHub仓库。\n","date":"2023年1月23日","externalUrl":null,"permalink":"/p/%E5%85%A8%E5%B9%B3%E5%8F%B0%E5%85%8D%E8%B4%B9%E9%94%AE%E7%9B%98%E9%BC%A0%E6%A0%87%E5%85%B1%E4%BA%AB%E8%BD%AF%E4%BB%B6barrier/inputleap/deskflow%E7%9A%84%E5%AE%89%E8%A3%85%E4%B8%8E%E4%BD%BF%E7%94%A8/","section":"Posts","summary":" 缘起 # 工作中经常需要用到不止一台电脑，例如办公室有一台Linux系统的台式机，另有一台macOS的笔记本电脑 用于上课或者开会。有时完成某个任务需要同时用到这两台电脑，这时用两套鼠标和键盘来回切换就会变得 非常麻烦，Barrier就是一款可以在多个电脑间跨系统共享鼠标键盘的免费软件。\n","title":"全平台免费键盘鼠标共享软件Barrier/InputLeap/Deskflow的安装与使用","type":"post"},{"content":" 简介 # Nextcloud是一个开源的网络硬盘工具，个人用户可以使用Nextcloud在自己的私有服务器上搭建属于自己的云网盘。\nNextcloud同时提供服务器端和客户端的软件，既支持Windows、Linux、macOS这些桌面级操作系统，也支持安卓和iOS这些移动级操作系统，因此个人用户搭建私有云网盘之后使用非常便捷。\nDocker是一个创建容器的工具，可以将操作系统层虚拟化，为某个或某些软件的运行隔离出一个独立的环境，避免软件的依赖和运行环境的冲突等问题，可以非常方便地在服务器上部署某些软件。\n前提 # docker和docker compose 域名（二级域名即可） acme.sh （生成SSL证书） nginx （反向代理） 准备工作 # 安装docker和docker-compose # Debian系系统可直接使用apt安装docker：\n1 sudo apt-get install docker-ce 安装docker-compose：\n1 sudo apt-get install docker-compose docker-compose使用一个名为docker-compose.yml的文件来“合成”一个docker容器，docker-compose.yml中是合成这个docker容器的配方。\n准备域名 # 在自己购买域名的网站上或者自己域名的管理网站上创建一个二级域名并配置域名解析，具体过程可参见\u0026ldquo;个人网站的建立过程（一）：购买个人域名并配置动态域名解析\u0026rdquo;。\n例如我有一个主域名jinli.cyou，于是我为个人云网盘创建了一个\u0026quot;cloud.jinli.cyou\u0026quot;的二级域名。然后在阿里云的域名管理平台上进行了域名和IP地址的绑定。\n使用acme.sh生成SSL证书 # 网上很多教程都使用Let\u0026rsquo;s Encrypt来生成SSL证书，但由于我之前在搭建这个网站时已经下载了acme.sh工具，因此这里我使用了acme.sh来为\u0026quot;cloud.jinli.cyou\u0026quot;生成证书。具体过程可参见个人网站的建立过程（二）：使用Hugo框架搭建个人网站。\n过程如下：\n找到你的域名管理账户的Access key。可以使用之前的，也可以重新获取，然后将Access key到处为系统变量Ali_Key和Ali_Secret。\n使用下面的命令生成证书：\n1 acme.sh --issue --dns dns_ali -d your.domain.com 使用下面的命令将证书复制到你的nextcloud目录下，例如我在nextcloud安装目录下新建了一个文件夹cert来存放证书文件：\n1 2 3 acme.sh --install-cert -d your.domain.com \\ --key-file /media/nextcloud/cert/key.pem \\ --fullchain-file /media/nextcloud/cert/cert.pem nginx反向代理配置 # 因为我之前在建立本网站时就安装了nginx，所以这里我使用了nginx作为反向代理工具（尽管Nextcloud网站上主要是以Apache为例进行安装说明的）。\n在nginx的配置目录下创建一个新的配置文件nextcloud.conf作为云网盘的反向代理配置文件，文件内写入：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name cloud.jinli.cyou; ssl_certificate /media/nextcloud/cert/cert.pem; ssl_certificate_key /media/nextcloud/cert/key.pem; client_max_body_size 0; underscores_in_headers on; location ~ { 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; add_header Front-End-Https on; proxy_headers_hash_max_size 512; proxy_headers_hash_bucket_size 64; proxy_buffering off; proxy_redirect off; proxy_max_temp_file_size 0; proxy_pass http://127.0.0.1:7080; } } 这里我使用7080端口作为Nextcloud的服务端口，对用户上传文件对大小不做限制。SSL证书存放在上一步指定的位置。\n配置完成后重启nginx服务使修改生效：\n1 sudo service nginx restart 安装Nextcloud # 配置docker-compose # 由于我们在上面已经生成了SSL证书并配置了nginx，因此在docker中我们只需要配置nextcloud和数据库两个容器即可。这里的数据库我们选择使用mariadb。\n在Nextcloud安装根目录下创建docker-compose.yml文件，写入以下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 version: \u0026#39;3\u0026#39; services: db: image: mariadb container_name: nextcloud-mariadb networks: - nextcloud_network volumes: - ./db:/var/lib/mysql - /etc/localtime:/etc/localtime:ro environment: - MYSQL_ROOT_PASSWORD=PASSWORD1 - MYSQL_PASSWORD=PASSSWORD2 - MYSQL_DATABASE=nextcloud - MYSQL_USER=nextcloud restart: unless-stopped app: image: nextcloud:latest container_name: nextcloud-app networks: - nextcloud_network ports: - 7080:80 depends_on: - db volumes: - ./nextcloud:/var/www/html - ./app/config:/var/www/html/config - ./app/custom_apps:/var/www/html/custom_apps - ./app/data:/var/www/html/data - ./app/themes:/var/www/html/themes - /etc/localtime:/etc/localtime:ro environment: - VIRTUAL_HOST=your.cloud.domain.name restart: unless-stopped networks: nextcloud_network: 这里需要\n把“MYSQL_ROOT_PASSSWORD”和“MYSQL_PASSWORD”改为你自己设置的数据库密码，也可以把“MYSQL_DATABASE”和“MYSQL_USER”的名字改掉。 把“VIRTUAL_HOST”里的域名改成自己的云网盘域名。 用docker-compose生成容器 # 在Nextcloud安装根目录下执行如下命令来生成容器：\n1 sudo docker-compose up -d 安装和初始化Nextcloud # 完成上述步骤后就可以从浏览器访问自己的云网盘啦！在浏览器内输入云网盘的网址，例如我的是https://cloud.jinli.cyou。就会进入如下页面：\n这里需要\n创建一个管理员账号，把用户名和密码记下来。 点开\u0026quot;Storage \u0026amp; databases\u0026quot;，将默认的数据库“SQLite”改为“MySQL/MariaDB”，然后填入数据库的用户、名字、密码，最后一栏（默认\u0026quot;localhost\u0026quot;）处应该填数据库的地址。由于我们在上面的docker-compose.yml文件中定义的数据库容器服务是db，而且将它与Nextcloud的容器放在同一个网络中，因此这里的地址应该填db，这样Nextcloud容器就可以通过这个名字访问到数据库容器了。如果你的docker-compose.yml文件中定义的数据库容器服务不是db，那么这里就需要填入你定义的数据库容器服务名。 最后点击“Finish Setup”就可以安装Nextcloud了，这可能会需要数分钟。安装完成后就可以用管理员账户登录进Nextcloud啦！\n添加新用户等后续设置 # 以管理员身份登录进NextCloud后，点击右上角的管理员账户头像，在下拉菜单中可以看到“Users”选项。点击这个选项，就可以添加用户组和新用户了。\n问题解决 # file/directory is locked 错误 # 在某次上传一个文件失败后，想重新上传但一直失败，就想把整个文件夹删除，但删除时提示“file/directory is locked”，无法删除。 这个问题是因为Nextcloud的锁定机制导致的。可以通过以下步骤解决：\n进入Nextcloud容器的命令行：\n1 sudo docker exec -it nextcloud-app bash 这时你应该在/var/www/html目录下，里面应该有一个occ文件，它是Nextcloud的命令行工具。\n进入维护模式\n1 ./occ maintenance:mode --on 安装MySQL客户端：\n1 apt-get update \u0026amp;\u0026amp; apt-get install -y mariadb-client 连接到数据库：\n1 mysql -h db -u ${MYSQL_USER} -p${MYSQL_PASSWORD} ${MYSQL_DATABASE} 这里的${MYSQL_USER}和${MYSQL_PASSWORD}是你在docker-compose.yml中设置的数据库用户名和密码。\n删除锁定记录：\n1 DELETE FROM oc_file_locks; 退出MySQL：\n1 exit; 再退出维护模式：\n1 ./occ maintenance:mode --off 最后回到Nextcloud的网页端，刷新页面就可以看到之前被锁定的文件夹已经可以删除了。\n注意：网上还有另一种解决方案，是进入Nextcloud容器后，使用命令./occ files:scan --all来扫描所有文件，使用这个命令的确可以消除锁定，但会刷新所有文件的最后修改时间，导致所有文件的修改时间都变成了当前时间，如果你想保留文件的原始修改时间，那么就不要使用这个命令。\n","date":"2022年9月23日","externalUrl":null,"permalink":"/p/%E4%BD%BF%E7%94%A8nextcloud%E5%92%8Cdocker%E8%87%AA%E5%BB%BA%E4%B8%AA%E4%BA%BA%E4%BA%91%E7%BD%91%E7%9B%98/","section":"Posts","summary":" 简介 # Nextcloud是一个开源的网络硬盘工具，个人用户可以使用Nextcloud在自己的私有服务器上搭建属于自己的云网盘。\n","title":"使用Nextcloud和docker自建个人云网盘","type":"post"},{"content":"","date":"22 九月 2022","externalUrl":null,"permalink":"/en/tags/environment-modules/","section":"Tags","summary":"","title":"Environment Modules","type":"tags"},{"content":"","date":"22 九月 2022","externalUrl":null,"permalink":"/en/tags/environment-variables/","section":"Tags","summary":"","title":"Environment Variables","type":"tags"},{"content":" Build Dakota # Prerequisites # Dakota relies on some other tools and libraries. Please refer to the Dakota\u0026rsquo;s official website. Most of the tools (such as CMake, Python) are already installed. The ones that need to take care of are:\nLinear Algebra libraries: BLAS and LAPACK Boost System information of my case:\nSystem: Red Hat 8 Conda package: Mambaforge, moose environment Install Prerequisites # The linear algebra libraries BLAS and LAPACK are already in the moose conda environment. Please refer to \u0026ldquo;Build SAM from Source\u0026rdquo; for how to install moose conda environment using Mambaforge.\nInstall Boost 1.69.0. I also tried Boost 1.80, but it didn\u0026rsquo;t work.\nDownload Boost 1.69.0 source code from https://boostorg.jfrog.io/artifactory/main/release/1.69.0/source/boost_1_69_0.tar.gz. Or use following command: 1 wget https://boostorg.jfrog.io/artifactory/main/release/1.69.0/source/boost_1_69_0.tar.gz Extract the source code: 1 tar xzf boost_1_69_0.tar.gz Configure and install. --prefix option specifies the installation location: 1 2 3 cd boost_1_69_0 ./bootstrap.sh --prefix=${HOME}/local/boost/1.69 ./b2 -j4 install Setup Dakota compiling environment # Activate moose conda environment:\n1 mamba activate moose Add Boost dynamic linked library to the LD_LIBRARY_PATH:\n1 export LD_LIBRARY_PATH=${HOME}/local/boost/1.69:$LD_LIBRARY_PATH Or if you use Environment Modules to manage your libraries, you can make a module file for Boost and load the Boost module. Please refer to \u0026ldquo;Use Environment Module to Manage Software Packages and Environment Variables in Linux\u0026rdquo; for how to use Environment Modules.\nBuild Dakota from Source # Build Dakota # Download Dakota source code. I downloaded the Dakota version 1.16.0, Source (Unix/OS X) from https://dakota.sandia.gov/sites/default/files/distributions/public/dakota-6.16.0-public-src-cli.tar.gz. Or you can download it via command:\n1 wget https://dakota.sandia.gov/sites/default/files/distributions/public/dakota-6.16.0-public-src-cli.tar.gz Extract the source code:\n1 tar xzf dakota-6.16.0-public-src-cli.tar.gz Make a build directory. Usually we can make the build directory inside the source code directory:\n1 2 cd dakota-6.16.0-public-src-cli mkdir build Configure in the build directory:\n1 2 cd build cmake -DCMAKE_INSTALL_PREFIX=${HOME}/local/dakota/6.16.0 .. Here we specify ${HOME}/local/dakota/6.16.0 as the Dakota installation directory.\nBuild Dakota:\n1 make -j 4 It may take a while.\nTest Dakota # Test the Dakota build:\n1 2 cd test ctest -j 4 -L Accept If all the tests are passed, it shows the Dakota build was successful.\nInstall Dakota # Finally you can install Dakota:\n1 2 cd .. make install The installation location is ${HOME}/local/dakota/6.16.0. You should add the Dakota binary executable to the system PATH:\n1 export PATH=${HOME}/local/dakota/6.16.0/bin:$PATH so that the system can find it. Or you can make a symbolic link for it to link it to you executable directory. For example, I have all my user\u0026rsquo;s exectable or tools in $HOME/bin, so I linked Dakota binary executable to this directory:\n1 ln -s ${HOME}/local/dakota/6.16.0/bin/dakota $HOME/bin You can check whether Dakota is recongnized by the system using:\n1 dakota -v ","date":"20 九月 2022","externalUrl":null,"permalink":"/en/p/build-dakota-from-source/","section":"Posts","summary":" Build Dakota # Prerequisites # Dakota relies on some other tools and libraries. Please refer to the Dakota’s official website. Most of the tools (such as CMake, Python) are already installed. The ones that need to take care of are:\nLinear Algebra libraries: BLAS and LAPACK Boost System information of my case:\n","title":"Build Dakota from Source","type":"post"},{"content":"","date":"20 九月 2022","externalUrl":null,"permalink":"/en/tags/dakota/","section":"Tags","summary":"","title":"Dakota","type":"tags"},{"content":"","date":"20 九月 2022","externalUrl":null,"permalink":"/en/categories/research/","section":"Categories","summary":"","title":"Research","type":"categories"},{"content":" Build SAM # Prerequisites # SAM relies on the MOOSE framework. We build MOOSE by following the instructions on the MOOSE website first.\nInstall Mambaforge3 # For Linux users:\n1 2 curl -L -O https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Linux-x86_64.sh bash Mambaforge-Linux-x86_64.sh -b -p ~/mambaforge3 After installing, put Mambaforge\u0026rsquo;s path to system environment PATH:\n1 export PATH=$HOME/mambaforge3/bin:$PATH so that the system recognizes the mamba command.\nThen initialize mambaforge by\n1 mamba init This command will append several mamba initialization to your ~/.bashrc file. Since I use zsh, I moved those lines from ~/.bashrc to ~/.zshrc.\nThen restart the terminal.\nInstall MOOSE # Add the INL public channel to Conda:\n1 conda config --add channels https://conda.software.inl.gov/public Create moose environment in Conda\n1 mamba create -n moose moose-dev Activate the environment\n1 mamba activate moose Clone SAM and Build # Clone SAM from ANL\u0026rsquo;s GitLab # Get an access token to the GitLab repository.\nGitLab -\u0026gt; Settings -\u0026gt; Access Tokens\nClone the SAM repository to your computer or cluster\n1 2 3 mkdir ~/Documents cd ~/Documents git clone https://git-nse.egs.anl.gov/*YourUserName_or_SAM*/SAM.git It will ask you for the GitLab username and password. Use the access token you generated for the password.\nBuild libmesh # Get moose submodule\n1 2 3 cd SAM git submodule init git submodule update --recursive Build SAM # Build SAM\n1 2 cd ~/Documents/SAM make -j4 Run the tests\n1 ./run_tests -j4 If SAM was built correctly, all the tests will pass.\n","date":"9 九月 2022","externalUrl":null,"permalink":"/en/p/build-sam-from-source/","section":"Posts","summary":" Build SAM # Prerequisites # SAM relies on the MOOSE framework. We build MOOSE by following the instructions on the MOOSE website first.\nInstall Mambaforge3 # For Linux users:\n1 2 curl -L -O https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Linux-x86_64.sh bash Mambaforge-Linux-x86_64.sh -b -p ~/mambaforge3 After installing, put Mambaforge’s path to system environment PATH:\n","title":"Build SAM from Source","type":"post"},{"content":"","date":"9 九月 2022","externalUrl":null,"permalink":"/en/tags/sam/","section":"Tags","summary":"","title":"SAM","type":"tags"},{"content":" 缘起 # 学校的计算集群上默认的shell是bash，而没有我常用的zsh，没了自动补全和语法高亮用着非常不习惯。而且普通的账户没有管理员权限，无法用sudo直接通过包管理工具来安装zsh，因此必须从源码来编译和安装zsh。\n安装依赖项ncurse # ncurse库（new curse）是一个支持基于文本的用户界面的接口库，zsh在运行时依赖于这个库。在安装zsh前需要先安装ncurse库。\n下载ncurse 6.1版本（目前的最新版本）\n1 wget https://ftp.gnu.org/pub/gnu/ncurses/ncurses-6.1.tar.gz --no-check-certificate 解压并进入解压后的目录\n1 2 tar xvfz ncurses-6.1.tar.gz cd ncurses-6.1 编译安装配置\n1 ./configure --prefix=\u0026#34;$HOME/software\u0026#34; CXXFLAGS=\u0026#34;-fPIC\u0026#34; CFLAGS=\u0026#34;-fPIC\u0026#34; 这里指定安装目录是用户根目录下的software文件夹。\n编译并安装\n1 make \u0026amp;\u0026amp; make install 安装zsh # 下载zsh最新版\n1 wget -O zsh.tar.xz https://sourceforge.net/projects/zsh/files/latest/download --no-check-certificate 解压并进入解压后的目录\n1 2 tar xvfz -C zsh zsh.tar.xz cd zsh 编译安装配置\n1 ./configure --prefix=\u0026#34;$HOME/software\u0026#34; CPPFLAGS=\u0026#34;-I$HOME/software/include\u0026#34; LDFLAGS=\u0026#34;-L$HOME/software/lib\u0026#34; 同样指定安装目录是用户根目录下的software文件夹。\n编译并安装\n1 make \u0026amp;\u0026amp; make install 最后把zsh的二进制文件夹加入系统环境中\n1 export PATH=$HOME/software/bin:$PATH 也可以把这行代码加入bash的配置文件.bashrc中。\n安装与配置oh-my-zsh # 请参考\u0026ldquo;命令行用户的oh-my-zsh配置\u0026rdquo;。\n","date":"2022年8月26日","externalUrl":null,"permalink":"/p/%E6%97%A0%E9%9C%80sudo%E6%9D%83%E9%99%90%E4%BB%8E%E6%BA%90%E7%A0%81%E7%BC%96%E8%AF%91%E5%AE%89%E8%A3%85zsh/","section":"Posts","summary":" 缘起 # 学校的计算集群上默认的shell是bash，而没有我常用的zsh，没了自动补全和语法高亮用着非常不习惯。而且普通的账户没有管理员权限，无法用sudo直接通过包管理工具来安装zsh，因此必须从源码来编译和安装zsh。\n","title":"无需sudo权限从源码编译安装zsh","type":"post"},{"content":" 缘起 # 在使用学校的计算集群时，可以很方便地使用module load命令来加载一些如Intel编译器、Python Anaconda之类的软件包，在不需要它们时也可以直接丢掉，这样可以很方便地选用和启用某些环境变量，防止某些环境变量污染其他软件的编译环境。 在自己的电脑上，为了安装和编译各种程序有时会下载安装很多编译器和软件包，这样在编译或运行某些程序时，由于环境变量的影响，编译器或程序经常会报错，于是我打算在自己的电脑上也安装module来管理这些软件包和环境变量。\nmodule工具的安装与工作方式 # 安装module # 对于Debian系系统如Ubuntu，可直接用apt安装：\n1 sudo apt install environment-modules 一般会默认安装到/usr/share/modules下。该路径下的modulefiles文件夹会存放一些默认的module文件：\ndot module-git module-info modules null use.own\n用户使用module avail命令可以查看它们。\n如果发现module命令不能使用，是因为还没有进行初始化。在/usr/share/modules/init下有针对如bash、 ksh、 fish、 tcsh、 zsh等各种shell的初始化工具。例如我在zsh内使用module工具，那么就需要运行如下命令来完成初始化：\n1 source /usr/share/modules/init/zsh 由于每次重新登录系统或zsh之后都需要重新输入上述命令，为了方便起见，可以把它加入~/.zshrc中。\nmodule工具的工作方式 # module工具通过识别modulefile的方式来罗列和加载用户指定的软件包和环境变量，一个典型的modulefile内容如下所示：\n#%Module\nproc ModulesHelp { } { puts stderr \u0026ldquo;This module adds solar to your path\u0026rdquo; }\nmodule-whatis \u0026ldquo;This module adds solar to your path\\n\u0026rdquo;\nset basedir \u0026ldquo;/home/aturing/software/solar-1.2\u0026rdquo;\nprepend-path PATH \u0026ldquo;${basedir}/bin\u0026rdquo;\nprepend-path LD_LIBRARY_PATH \u0026ldquo;${basedir}/lib64\u0026rdquo;\nmodule load intel/19.1/64/19.1.1.217\nmodule load intel-mpi/intel/2019.7/64\n在安装好module后，module会在默认的/usr/share/modules/modulefiles路径下寻找modulefiles。在用户使用module avail命令查看可用的module时，module工具会把/usr/share/modules/modulefiles路径下的modulefiles展示出来。安装完module工具后默认的module一般如下所示：\n\u0026mdash;\u0026mdash;- /usr/share/modules/modulefiles \u0026mdash;\u0026mdash;\u0026ndash;\ndot module-git module-info modules null use.own\n如果用户使用module load use.own命令来加载use.own，那么module工具会在用户根目录下创建privatemodules文件夹，用户可以把自定义的modulefiles放在此文件夹下。之后位于~/privatemodules目录下的modulefiles也会被module工具检测到。\n安装软件包并用module工具管理 # 安装Intel的C++编译器icc # Intel为Intel芯片使用者免费提供自家的C++编译器icc和Fortran编译器ifort，用户可以在Intel的官网上下载安装。Intel提供多种安装方式，既有集成了多种Intel编译器与工具的oneAPI，也有这些编译器和工具的独立版本。完整版的oneAPI工具包很大，由于我目前只需要icc和ifort两个编译器，因此我只下载安装这两个独立版的编译器。\n下载安装包\n可以在Intel官网上下载独立版的icc编译器安装包，网址如下：https://www.intel.com/content/www/us/en/developer/articles/tool/oneapi-standalone-components.html#dpcpp-cpp。对于Linux/Windows/macOS各个系统，都有在线版和离线版两种安装包可选。可以选择在线版的安装包，例如我下载的安装包名称是l_dpcpp-cpp-compiler_p_2022.1.0.137.sh。\n也可以使用命令行下载：\n1 wget https://registrationcenter-download.intel.com/akdlm/irc_nas/18717/l_dpcpp-cpp-compiler_p_2022.1.0.137.sh 运行安装包\n下载完成后进入存放安装包的目录，使用如下命令安装：\n1 sudo sh ./l_dpcpp-cpp-compiler_p_2022.1.0.137.sh -a -s --eula accept 其中-a表示使用命令行参数；-s表示静默(silence)安装，即不弹出安装窗口；--eula accept表示同意用户协议。详情可见Intel的安装指导.\n编译器默认安装在/opt/intel/oneapi目录下。\n配置modulefiles\nIntel在安装包内集成了可自动生成modulefiles的脚本，用户只需运行该脚本，就可以将安装的编译器或工具包自动编写成module工具可以使用的modulefiles。\n配置脚本是位于安装目录/opt/intel/oneapi下的modulefiles-setup.sh文件。用户在运行此脚本时可指定放置生成的modulefiles的文件夹，这里我选择将生成的modulefiles放在~/privatemodules目录下：\n1 /opt/intel/oneapi/modulefiles-setup.sh --output-dir=$HOME/privatemodules 查看和加载编译器\n完成上述步骤后就可以使用module avail命令来查看刚才安装的icc编译器了，在运行module avail命令后会看到类似下面所示的输出：\n1 2 3 4 5 6 7 8 ------------------------------------------------ /usr/share/modules/modulefiles ------------------------------------------------ dot module-git module-info modules null use.own ------------------------------------------------- /home/lijin/privatemodules --------------------------------------------------- compiler-rt/2022.1.0 compiler/2022.1.0 debugger/2021.6.0 icc/2022.1.0 init_opencl/2022.1.0 tbb/2021.6.0 compiler-rt/latest compiler/latest debugger/latest icc/latest init_opencl/latest tbb/latest compiler-rt32/2022.1.0 compiler32/2022.1.0 dev-utilities/2021.6.0 icc32/2022.1.0 oclfpga/2022.1.0 tbb32/2021.6.0 compiler-rt32/latest compiler32/latest dev-utilities/latest icc32/latest oclfpga/latest tbb32/latest 由于我还安装了一些其他工具，所以除了icc，还显示了其他的module。\n然后使用module load icc/latest就可以加载安装的最新版本的icc编译器啦！\n安装Intel的Fortran编译器ifort # 安装ifort编译器的步骤与上述步骤类似，这里不再赘述。ifort编译器默认安装目录也是/opt/intel/oneapi，安装完成后再次运行\n1 /opt/intel/oneapi/modulefiles-setup.sh --output-dir=$HOME/privatemodules 即可更新~/privatemodules目录下的modulefiles。\n","date":"2022年8月18日","externalUrl":null,"permalink":"/p/linux%E4%B8%8B%E4%BD%BF%E7%94%A8module%E7%AE%A1%E7%90%86%E8%BD%AF%E4%BB%B6%E5%8C%85%E5%92%8C%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F/","section":"Posts","summary":" 缘起 # 在使用学校的计算集群时，可以很方便地使用module load命令来加载一些如Intel编译器、Python Anaconda之类的软件包，在不需要它们时也可以直接丢掉，这样可以很方便地选用和启用某些环境变量，防止某些环境变量污染其他软件的编译环境。 在自己的电脑上，为了安装和编译各种程序有时会下载安装很多编译器和软件包，这样在编译或运行某些程序时，由于环境变量的影响，编译器或程序经常会报错，于是我打算在自己的电脑上也安装module来管理这些软件包和环境变量。\n","title":"Linux下使用module管理软件包和环境变量","type":"post"},{"content":"","date":"2022年8月18日","externalUrl":null,"permalink":"/tags/module/","section":"Tags","summary":"","title":"Module","type":"tags"},{"content":"","date":"2022年8月18日","externalUrl":null,"permalink":"/tags/%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F/","section":"Tags","summary":"","title":"环境变量","type":"tags"},{"content":" Linux发行版 # Linux系统是一个自由软件的操作系统，它的内核是Linux内核，由芬兰计算机科学家Linus Torvalds在1991年发布。Linux系统的内核是自由软件，因此任何人都可以基于Linux内核发布自己的操作系统，这些操作系统就是Linux发行版。\n文件系统 # 文件系统在任何一个计算机系统中都是核心功能之一，不同操作系统会使用不同的文件系统，这里我们通过对比Windows系统和Linux系统，来简单了解一下Linux系统中的文件结构。\n目录树 # 计算机中的各种工具、软件程序、数据等都是以文件方式存储的。这些文件大部分存储在计算机的硬盘上，通常以树状结构组织起来，例如在Windows系统中，文件系统的目录树可能如下所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 此电脑 ├── 磁盘C │ ├── $Recycle.Bin │ ├── PerfLogs │ ├── Program Files │ ├── Program Files (x86) │ ├── ProgramData │ ├── Users │ │ ├── All Users -\u0026gt; /mnt/c/ProgramData │ │ ├── Default │ │ ├── Guest │ │ ├── Public │ │ ├── Visitor │ │ ├── desktop.ini │ │ └── lijin │ │ ├── .ssh │ │ ├── AppData │ │ ├── Desktop │ │ ├── Documents │ │ ├── Downloads │ │ ├── Music │ │ ├── Pictures │ │ ├── Videos │ │ └── Favorites │ └── Windows ├── CD-ROM │ └── 我的光盘 │ └── 磁盘F 以下是Windows系统中一些比较重要的目录：\n用户根目录\nWindows系统的用户根目录在C:\\Users\\文件夹下，每个用户有一个文件夹，例如我的用户根目录是C:\\Users\\lijin。在命令行中，用户根目录可以用~表示。\n用户根目录下一般存储用户的个人文件，例如个人的文档、照片、音乐、电影等可以分类存储到用户根目录下的Documents、Pictures、Music、Videos等文件夹下。\n只限当前用户使用的程序一般安装到用户根目录的AppData文件夹内，这个文件夹系统默认是隐藏的，用户在一般情况下不应该动这个文件夹。\nWindows系统目录\nWindows系统的系统根目录是C:\\Windows，操作系统和系统级的软件工具都在这个目录下，用户不应该轻易动这个文件夹，否则可能会损坏系统。\n应用程序目录\n用户一般可以将程序安装到C:\\Program Files (x86)和C:\\Program Files下，其中C:\\Program Files (x86)一般安装32位程序，C:\\Program Files安装64位程序。\n安装到这两个目录下的程序对于当前计算机的所有用户都是可见的。但有些商用软件可能会限制使用者，未注册的用户虽然可以看到该软件但却有可能无法运行使用。\n在Linux系统中，文件系统的目录树可能如下所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 系统根目录 ├── bin -\u0026gt; usr/bin ├── boot ├── dev ├── etc ├── home │ ├── Visitor │ ├── otherusers │ └── lijin │ ├── .ssh │ ├── AppData │ ├── Desktop │ ├── Documents │ ├── Downloads │ ├── Music │ ├── Pictures │ ├── Videos │ └── Favorites ├── lib -\u0026gt; usr/lib ├── lost+found ├── media ├── mnt │ ├── 硬盘F │ ├── 虚拟光驱 │ └── 我的U盘 ├── opt ├── root ├── run ├── snap ├── sys ├── tmp ├── usr └── var 以下是Linux系统中一些比较重要的目录：\n用户根目录\nLinux系统的用户根目录在/home/文件夹下，每个用户有一个文件夹，例如我的用户根目录是/home/lijin。\n用户根目录下一般存储用户的个人文件，例如个人的文档、照片、音乐、电影等可以分类存储到用户根目录下的Documents、Pictures、Music、Videos等文件夹下。\n只限当前用户使用的程序一般安装到用户根目录的AppData文件夹内，这个文件夹系统默认是隐藏的，用户在一般情况下不应该动这个文件夹。\nLinux系统目录\nLinux系统的系统根目录是/sys，操作系统在这个目录下，用户不应该轻易动这个文件夹，否则可能会损坏系统。\n应用程序目录\n用户一般可以将程序安装到/opt下。\n库目录\n系统默认的一些运行库和工具软件一般在/usr下，用户在一般情况下不应该动这个文件夹。\n注意：\n在Windows系统中，路径是带盘符的，例如C:表示磁盘C下的路径；而Linux系统中一般不用字母表示的盘符。 在Windows系统中，目录分隔符是反斜杠\\；在Linux系统中，目录分隔符则是斜杠/。 磁盘分区 # 对于机械硬盘而言，存储空间的起点在圆盘的外圈，磁头读取数据时也是从外圈开始寻址，位于磁盘外圈的数据访问速度较快。所以我们往往将硬盘进行分区，位于外层的分区用来安装操作系统和软件，内层的分区用来放数据文件，这样可以让系统和软件运行比较流畅。\n而对于固态硬盘而言，硬盘各部位的数据访问速度都差不多，因此单就访问速度而言，固态硬盘一般没有必要进行磁盘分区。\n除了区分访问速度的不同，硬盘分区的另一个作用是隔离各个分区的数据。例如将操作系统和用户的电影音乐等文件放置在不同分区，这样当需要重新安装操作系统时，只需要格式化操作系统所在的分区，用户的个人文件不会受到影响。\n挂载点 # 在Linux系统中，存储设备需要挂载（mount）到文件目录树的某个位置，这个位置就是挂载点。例如在手动分区并安装Linux系统时，我们往往会创建home、swap、usr等分区，这些分区将被自动挂载到系统根目录/下。如果我们给计算机外接一个硬盘或U盘，则这个硬盘或U盘一般会被挂载到/mnt/下。\n需要注意的是，挂载的存储设备不一定非得是一块真实的硬盘，也可以是网络云盘、远程的计算机，甚至是本电脑上的一个虚拟的光盘文件！例如在数年前，不少计算机软件或操作系统都存储在光盘上，用户可以购买存储有某个软件的光盘，然后用电脑上的光盘驱动器读取安装软件或操作系统。然而现在许多笔记本电脑为了做得轻便，都取消了光盘驱动器，用户也基本不再购买软件光盘了，取而代之的是在网上下载安装包，而有不少安装包依旧以光盘文件的格式来存储软件，这时用户就可以通过虚拟光驱将这个“虚拟的光盘”挂载到电脑上。\n软件安装与管理 # 我们选择使用某个操作系统的原因可能很多，但有一条原因往往是普遍的，那就是可以方便地使用这个操作系统上的一些特色软件。在安装了某个操作系统之后，我们往往还需要安装各种软件才能真正让这个操作系统发挥它的作用，成为生产力工具。\n在大多数用户都熟悉的Windows系统上，我们可以在一些软件应用商店安装某个软件，或者到某个软件的官网下载安装包，双击安装包来安装。但这些都需要在图形界面上点击来完成，但对于某些Linux（例如在Windows上安装的WSL，或者学校的计算集群）而言，我们可能并没有一个可用的图形界面，这时我们就需要熟悉在命令行界面下安装软件的技巧。这里我们介绍在Linux命令行下三种常见的软件安装方法：\n使用包管理工具 手动下载安装包 从源码编译安装 使用包管理工具 # 图形化的操作系统上往往会配备“应用中心”等图形化的软件管理工具，在命令行化的界面上，我们也可以有类似“应用中心”的软件包管理工具。下面以Debian系（包括Debian、Ubuntu、Deepin等系统）常用的包管理工具APT来简单介绍一下在Linux命令行界面内安装软件的方法。\nAPT简介 # APT是Advanced Packaging Tools（高级打包工具）的缩写，是在Debian系操作系统中最常用的包管理工具，它可以自动下载、配置以及安装很多软件包。通常情况下，我们在命令行内通过apt或apt-get等命令来使用APT，但需要注意的是，除此之外，APT还可以通过aptitude、Synaptic、GNOME软件商店等前端来使用。这里我们只介绍通过apt命令使用APT的方法。\n使用apt命令管理软件 # 搜索软件包\n1 apt search software_name 显示软件包详情\n1 apt show software_name 安装软件包\n1 apt install software_name 卸载软件包\n1 apt remove software_name 卸载软件包并删除配置文件\n1 apt purge software_name 自动删除不需要的软件包\n1 apt autoremove 更新软件仓库\n1 apt update 升级软件仓库\n1 apt upgrade 使用以上命令安装、更新或卸载软件时可能需要sudo权限。\n手动下载安装包 # 有些软件可能不在APT仓库里，无法直接通过APT安装。不过开发者可能提供.deb或.rpm格式的安装包，.deb格式的安装包一般在Debian系系统中使用，.rpm格式的安装包一般在Red Hat系系统（Red Hat、CentOS、Fedora等）中使用。\n例如文本编辑器Visual Studio Code （VS code）就不在APT仓库里，想要在Linux下安装，我们就需要下载它的安装包。\n下载完成后，进入存放安装包的文件夹，执行下面的命令就可以安装了：\n1 sudo dpkg -i package_name.deb 从源码编译安装 # 从源码编译安装的情形一般有3种：\n有些软件可能连安装包都不提供，开发者只提供源代码，这是我们就需要从源码编译安装。 通过APT和安装包安装都需要sudo权限，而有时候我们并没有sudo权限。例如在学校的计算集群上时，我们的账户只是普通用户，没有管理员权限，所以无法使用sudo。 我们需要对软件做一些改动以满足自己具体的需求，这时需要先更改源代码，再自己编译安装。 例如很多Linux用户喜欢使用zsh，而在学校的计算集群上往往只提供bash，且用户没有管理员权限无法使用包管理工具或者安装包直接安装zsh。这时如果依旧想要使用zsh，就需要用户自己从源码编译安装。\n图形界面GUI # 在Windows系统和macOS系统中，我们通常通过鼠标点击来操作电脑，这种操作方式叫做图形用户界面（Graphical User Interface，GUI），图形界面可以说是Windows和macOS系统的标志，也为计算机在普通人中的普及起到了巨大的作用。\n而在Linux系统中，图形界面长久以来未受到足够的重视，这也是Linux系统在普通人中难以普及的原因之一。尽管如此，近年来Linux系统的图形界面还是取得了不小的进步，现在的Linux系统图形界面也越来越美观、易用。\n在Windows系统和macOS系统中，图形界面是固定的，也是默认开启的，用户可以开箱即用。而在Linux系统中，图形界面不是必须的，用户可以选择是否安装图形界面，也可以选择安装不止一个图形界面。我们接下来简单介绍一下Linux系统中的图形界面。\nLinux图形界面——堆叠式窗口管理器和平铺式窗口管理器 # 按照操作习惯来分，Linux系统中的图形界面可以分为两大类：堆叠式窗口管理器（Stacking Window Manager）和平铺式窗口管理器（Tiling Window Manager）。\n堆叠式窗口管理器 # 堆叠式窗口管理器是最常见的窗口管理器，它的窗口可以重叠在一起，用户可以通过鼠标点击来切换窗口。常见的堆叠式窗口管理器有GNOME、KDE、Xfce等。\n堆叠式窗口管理器也是最常见的桌面环境，包括Windows和macOS系统的图形界面都使用堆叠式窗口管理器。\nGNOME\nGNOME是一个自由软件的桌面环境，它的设计理念是简洁、易用。GNOME的界面风格简洁大方，用户可以通过GNOME的扩展来定制自己的桌面环境。\nGNome是很多发行版的默认桌面环境，例如Ubuntu、Fedora等。\nKDE\nKDE是另一个自由软件的桌面环境，它的设计理念是强大、灵活、可定制性强。KDE的界面风格更加华丽，用户可以通过KDE的设置来定制自己的桌面环境。\nKDE也是很多发行版的默认桌面环境，例如Kubuntu、openSUSE等。\n平铺式窗口管理器 # 平铺式窗口管理器的窗口一般不重叠在一起，而是平铺在屏幕上，用户可以通过快捷键来切换窗口。常见的平铺式窗口管理器有i3、dwm、bspwm、Hyprland等。\n平铺式窗口管理器并不是很常见，一般只有一些专业用户或者极客用户会使用。其特点是很多操作都可以通过快捷键来完成，而且几乎所有设置都可以由用户自定义，因此用户可以高度定制，排除不需要的功能，保持系统的简洁。并且几乎所有操作都可以按照用户自己习惯的方式来配置，因此可以大大提高熟练用户的工作效率。\ni3\ni3是一个自由软件的平铺式窗口管理器，它的设计理念是简洁、高效。i3的界面风格简洁大方，用户可以通过i3的配置文件来定制自己的桌面环境。\nHyprland\nHyprland是一个自由软件的平铺式窗口管理器，基于Wayland。它的设计理念是简洁、高效。Hyprland的界面风格简洁大方，内置的窗口切换动画非常优雅流畅。目前Hyprland还在开发中，但已经受到了很多极客用户的喜爱。\nLinux图形界面——X Window系统和Wayland # 在Linux系统中，图形界面的实现主要有两种技术：X Window系统和Wayland。要想知道自己的Linux系统使用的是哪种技术，可以在终端输入echo $XDG_SESSION_TYPE，如果输出x11，则表示使用的是X Window系统；如果输出wayland，则表示使用的是Wayland。\nX Window系统 # X Window系统是一个自由软件的图形窗口系统，它是Linux系统中最常用的图形窗口系统。X Window系统的设计理念是基于服务端-客户端的架构，服务端负责管理硬件设备，客户端负责绘制图形界面。\n需要注意的是，X Window系统中客户端和服务端的定义与传统意义上的客户端和服务端不同。例如如果你有一台电脑A在远程，然后你在本地的电脑B上使用ssh登录到电脑A，而且在电脑B上显示了电脑A的图形界面。 这时对于ssh服务来说，电脑A是服务端，电脑B是客户端；但对于X Window系统来说，电脑A是客户端，电脑B是服务端。 因为对X Window系统来说，你在电脑B上起了一个X Window服务，而电脑A上的程序是客户端，通过这个服务来显示图形界面。\nX Window系统是在1984年发布的，而后快速迭代，不断发布新的版本。到1987年，X Window系统的第11个版本发布，自此版本之后，X Window系统的标准就基本固定下来了，因此我们通常称X Window系统为X11。\nXorg是X11的一个实现，也是Linux系统中最常用的X11实现。\nWayland # 随着硬件的发展和用户需求的变化，X Window系统的一些设计理念逐渐显得过时，例如X Window系统的设计理念是基于服务端-客户端的架构，这种架构在现代硬件上显得有些过时，因此一些开发者开始尝试设计新的图形窗口系统。\nWayland是一个自由软件的图形窗口系统，它的设计理念是简单、高效。Wayland的设计理念是基于内核-客户端的架构，内核负责管理硬件设备，客户端负责绘制图形界面。\nWayland的设计理念更加现代化，因此在一些新的Linux发行版中，例如Fedora、Arch Linux等，Wayland已经取代了X Window系统成为默认的图形窗口系统。\n而且Wayland对高分辨率屏幕（HiDPI）的支持更好，有些基于X11的软件在高分辨率屏幕下的显示过小，而如果通过缩放来解决这个问题，会导致图形界面模糊，而Wayland则可以更好地支持高分辨率屏幕。\n","date":"2022年8月8日","externalUrl":null,"permalink":"/p/linux%E7%B3%BB%E7%BB%9F%E5%85%A5%E9%97%A8%E7%9F%A5%E8%AF%86%E7%AE%80%E4%BB%8B/","section":"Posts","summary":" Linux发行版 # Linux系统是一个自由软件的操作系统，它的内核是Linux内核，由芬兰计算机科学家Linus Torvalds在1991年发布。Linux系统的内核是自由软件，因此任何人都可以基于Linux内核发布自己的操作系统，这些操作系统就是Linux发行版。\n","title":"Linux系统入门知识简介","type":"post"},{"content":"","date":"2022年8月8日","externalUrl":null,"permalink":"/tags/wsl/","section":"Tags","summary":"","title":"WSL","type":"tags"},{"content":"","date":"2022年8月6日","externalUrl":null,"permalink":"/tags/oh-my-zsh/","section":"Tags","summary":"","title":"Oh-My-Zsh","type":"tags"},{"content":" 配置shell # 效果展示:\n特点 # Shell: oh-my-zsh 主题: powerlevel10k 插件: 命令自动补全建议zsh-autosuggestions, 命令语法高亮zsh-syntax-highlighting, 命令修正建议thefuck 注意 # 这里使用的powerlevel10k主题从2024年5月开始将不再维护，当然这应该不影响使用。主题作者romkatv的说法是powerlevel10k已经非常成熟，功能也已经齐全。目前的issue主要是一些个性化的需求，或者涉及到底层的改变，作者romkatv认为这些issue对于99%的用户而言都是不必要的。如果你不在这99%的用户之内，就意味着你需要有能力自己修改代码来满足你的需求。因此romkatv决定不再维护powerlevel10k主题。这里是相关的讨论：\npowerlevel10k issue #2690 reddit讨论 本文现在已经不再是主要安装指南。这里保留基于 P10k 的配置，供仍然需要旧方案的读者参考；新安装建议阅读后续文章，使用基于 Oh My Posh 的配置。新方案可以在 Zsh 和 PowerShell 中使用同一个主题，并将公共配置与机器专属配置分离。\n如果你仍然需要旧的 P10k 配置，可以继续阅读本文后面的安装步骤；否则请参阅Oh My Posh 迁移指南。\n其他可选方案包括：\nstarship：一个跨平台的命令行提示符，支持多种shell。相比powerlevel10k，starship的配置也更为简单。 oh-my-posh：一个跨平台的命令行提示符，最开始应该是为PowerShell设计的，但现在已经支持了多种shell，包括zsh。oh-my-posh的配置也很简单。 安装 # 方法一：使用脚本自动安装（推荐） # 下载安装脚本：\n1 curl -s https://raw.githubusercontent.com/jin-li/ShellConfig/main/install_OhMyZsh_p10k.sh -o install_OhMyZsh_p10k.sh 运行脚本：\n注意：不要在root用户，也不要使用sudo来运行本脚本。以普通用户运行，运行中可能需要你输入密码，到时再输入即可。\n1 2 chmod +x install_OhMyZsh_p10k.sh ./install_OhMyZsh_p10k.sh 方法二：手动安装 # 安装 Meslo Nerd 字体\n下载一下四个字体文件：\nMesloLGS NF Regular.ttf MesloLGS NF Bold.ttf MesloLGS NF Italic.ttf MesloLGS NF Bold Italic.ttf 双击各文件并点击安装按钮，然后电脑上包括命令行终端在内的的各个软件就可以使用MesloLGS NF字体了。\n请参考p10k Font来为Windows Power Shell、macOS Terminal等命令行终端工具配置MesloLGS NF字体。\n安装 zsh\nUbuntu, Debian 及Debian系衍生系统\n更新 apt 源 1 sudo apt update 安装依赖的工具库 (git, wget, python3, pip3)\n1 sudo apt install wget git python3-dev python3-pip python3-setuptools 安装 zsh\n1 sudo apt install zsh Arch Linux 或 Manjaro\n1 sudo pacman -S zsh macOS已自带zsh\nFedora\n1 sudo dnf install zsh 通过wget安装 oh-my-zsh\n1 sh -c \u0026#34;$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\u0026#34; 安装 powerlevel10k\n1 git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k 安装插件和工具\n命令自动补全建议zsh-autosuggestions 1 git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions 命令语法高亮fast-syntax-highlighting 1 git clone https://github.com/zdharma-continuum/fast-syntax-highlighting.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/fast-syntax-highlighting 下载配置文件，并链接到用户根目录下\n从GitHub下载 ShellConfig 1 2 3 mkdir -p ~/Documents/GitHub cd ~/Documents/GitHub git clone https://github.com/jin-li/ShellConfig.git 备份旧配置文件 1 2 mv ~/.bashrc ~/.bashrc_bak mv ~/.zshrc ~/.zshrc_bak 将配置文件链接到 home 目录 1 2 3 ln ~/Documents/GitHub/ShellConfig/.bashrc ~/.bashrc ln ~/Documents/GitHub/ShellConfig/.zshrc ~/.zshrc ln ~/Documents/GitHub/ShellConfig/.p10k.zsh ~/.p10k.zsh 重启命令行终端\n配置Vim # 动机 # 虽然日常的代码开发中我一般都使用vscode，但是在编辑一些简单的文本文件，或者在服务器上编辑文本文件时，还是用vim更加方便快捷。之前一直使用vim，但总觉得配置起来比较麻烦。于是我准备迁移到neovim，并使用LazyVim，这样就可以做到开箱即用，不需要再配置了。\n安装 # 方法一：使用脚本自动安装（推荐） # 下载安装脚本：\n1 curl -s https://raw.githubusercontent.com/jin-li/ShellConfig/main/install_LazyVim.sh -o install_LazyVim.sh 运行脚本：\n1 2 chmod +x install_LazyVim.sh ./install_LazyVim.sh 方法二：手动安装 # 卸载 vim-tiny 或 vim-minimal\nDebian系统默认的vim是vim-tiny，Fedora系统默认的vim是vim-minimal，这两个版本都不支持插件。在使用本配置之前，需要先检查你的vim版本。\n1 vi --version 如果输出中有\u0026quot;Small version without GUI\u0026quot;，说明你的vim是vim-tiny或vim-minimal。你可以卸载它：\nDebian / Ubuntu 1 sudo apt remove vim-tiny Fedora 1 sudo dnf remove vim-minimal 安装 Neovim\n然后安装Neovim。\nDebian / Ubuntu 由于LazyVim需要Neovim 0.8或更高版本，而在Ubuntu 22.04中，Neovim的版本是0.6.1。因此，我们需要从官方网站安装更新的Neovim。\n1. 从[官方网站](https://github.com/neovim/neovim/releases)下载预编译的*Neovim*二进制文件 ```sh curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux64.tar.gz ``` 1. 解压下载的文件 ```sh sudo tar -C /opt -xzf nvim-linux64.tar.gz ``` 这将*Neovim*安装到`/opt/nvim-linux64`目录。 1. 创建一个符号链接到*Neovim*可执行文件 ```sh sudo ln -s /opt/nvim-linux64/bin/nvim /usr/bin/nvim ``` Fedora\n在Fedora 40中，Neovim的版本是0.9.5，这个版本足够新，可以使用LazyVim。所以我们可以直接使用dnf安装它。 sh sudo dnf install neovim 安装 LazyVim\n参考官方网站，我们可以使用以下命令安装LazyVim：\n安装 LazyVim 1 git clone https://github.com/LazyVim/starter ~/.config/nvim 删除.git文件夹，这样你就可以将它添加到你自己的仓库中 1 rm -rf ~/.config/nvim/.git 第一次运行nvim会自动安装插件。这可能需要一段时间才能完成。 1 nvim 退出nvim，然后重新打开nvim，你就可以看到LazyVim的效果了。 替换苹果系统默认的终端 # 在安装完LazyVim之后，我发现在macOS Terminal中的LazyVim显示效果非常差，颜色暗淡，对比度低：\n原因是macOS Terminal的颜色方案不支持256真彩色，而LazyVim默认使用的是256色的颜色方案。 我听说iTerm2支持256真彩色，所以我决定从macOS内置终端迁移到iTerm2。 安装完iTerm2之后，你需要设置iTerm2使用之前下载并安装的Meslo Nerd字体。 然后在iTerm2中打开LazyVim，现在看起来好多了：\n问题解决 # 在Fedora 42上运行完上述脚本之后，打开nvim时nvim界面显示如下报错：\n1 No C compiler found! \u0026#34;cc\u0026#34;, \u0026#34;gcc\u0026#34;, \u0026#34;clang\u0026#34;, \u0026#34;cl\u0026#34;, \u0026#34;zig\u0026#34; are not executable. 这是因为LazyVim需要C编译器来编译某些插件或功能，而Fedora 42上默认并没有安装C编译器。你可以安装某个C编译器，例如gcc。我安装了zig，它是一款轻量化的编译器，支持C语言：\n1 sudo dnf install zig 然后再打开nvim，就会发现报错消失了。\n","date":"2022年8月6日","externalUrl":null,"permalink":"/p/%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%94%A8%E6%88%B7%E7%9A%84oh-my-zsh%E5%92%8Cneovim%E9%85%8D%E7%BD%AE/","section":"Posts","summary":" 配置shell # 效果展示:\n特点 # Shell: oh-my-zsh 主题: powerlevel10k 插件: 命令自动补全建议zsh-autosuggestions, 命令语法高亮zsh-syntax-highlighting, 命令修正建议thefuck 注意 # 这里使用的powerlevel10k主题从2024年5月开始将不再维护，当然这应该不影响使用。主题作者romkatv的说法是powerlevel10k已经非常成熟，功能也已经齐全。目前的issue主要是一些个性化的需求，或者涉及到底层的改变，作者romkatv认为这些issue对于99%的用户而言都是不必要的。如果你不在这99%的用户之内，就意味着你需要有能力自己修改代码来满足你的需求。因此romkatv决定不再维护powerlevel10k主题。这里是相关的讨论：\n","title":"命令行用户的oh-my-zsh和Neovim配置","type":"post"},{"content":"","date":"2 八月 2022","externalUrl":null,"permalink":"/en/tags/color-scheme/","section":"Tags","summary":"","title":"Color Scheme","type":"tags"},{"content":"","date":"2 八月 2022","externalUrl":null,"permalink":"/en/tags/plotting/","section":"Tags","summary":"","title":"Plotting","type":"tags"},{"content":" 封面 # 内容概览 # 目录 # 计算机简介\n计算机发展简史 手动式计算机 机械式计算机 机电式计算机 电子计算机 现代电子计算机 计算机的分类 按运行原理分 按通用性分 按适用范围分 第1章习题 现代数字计算机基础数学物理知识\n二进制 数的进制 进制转换 编码——万物皆数 逻辑代数 简单逻辑运算 运算律 逻辑函数与化简 逻辑电路 逻辑门 组合逻辑 可编程逻辑 第2章习题 现代计算机基本原理简介\n图灵机模型 图灵机 可计算性 图灵完备 计算机体系结构简介 计算机体系结构 冯·诺伊曼结构 哈佛结构 改进型哈佛结构 第3章习题 计算机基本硬件简介\n处理器 CPU发展简史 CPU的分类 CPU的结构 CPU制造工艺简介 显卡简介 存储设备 寄存器 缓存 主存 硬盘 输入输出 输入设备 输出设备 主板和外设 主板 外设 第4章习题 操作系统简介\n操作系统基本结构 驱动 内核 接口库 壳 操作系统分类 按内核结构分 按应用平台分 第5章习题 计算机软件简介\n计算机的启动过程简介 上电 BIOS 加载启动项 启动操作系统 编程语言简介 按照等级分 按照运行方式分 算法 算法基本概念和表示方法 算法复杂度简介 数据结构简介 软件工程简介 编译过程简介 软件开发流程 第6章习题 附录\n国际单位制词头 摩尔定律发展图 ASCII编码表 英特尔和AMD桌面级CPU世代表 索引\n名词索引 人名索引 下载PDF # 可以在本书GitHub仓库的发布页面(Releases)下载本书的PDF。\n仓库暂不提供本书的LaTeX源码 本书后续若有修订或更新，亦会同步到仓库的发布页面 发布页面提供压缩版和未压缩版两种格式 文件名带有_compressed的为压缩版，文件体积小，但图片质量差 另一个为未压缩版，文件体积较大，但图片清晰度高 版权 # 本书使用CC-BY-NC-ND 4.0协议。\n作者保留本书一切权利 允许在各个互联网平台上复制、下载、分享、传播本书，但须注明作者和出处(出处即本仓库，地址：https://github.com/jin-li/ComputerIntro_release) BY：必须按照作者或者许可人指定的方式对作品进行署名 NC：不得为商业目的而使用本书 ND：不得改变、转变或更改本书 鸣谢 # 本书使用了ElegantLaTeX项目制作的ElegantBook书籍模板。\n","date":"2022年7月22日","externalUrl":null,"permalink":"/p/%E7%8E%B0%E4%BB%A3%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%BD%AF%E7%A1%AC%E4%BB%B6%E7%AE%80%E4%BB%8B%E5%BD%93%E4%BB%A3%E7%A0%81%E9%81%87%E4%B8%8A%E7%94%B5%E8%B7%AF/","section":"Posts","summary":" 封面 # 内容概览 # 目录 # 计算机简介\n","title":"《现代计算机软硬件简介——当代码遇上电路》","type":"post"},{"content":"","date":"2022年7月22日","externalUrl":null,"permalink":"/tags/matlab/","section":"Tags","summary":"","title":"MatLab","type":"tags"},{"content":" 缘起 # 在使用MatLab绘图时有时会对MatLab默认的配色方案不太满意，平时遇到好看的配色方案也会想要保存下来自己用。但是之前保存的MatLab使用笔记过于凌乱，想要用某个配色时不太好找，所以就有了自制一个配色方案库的想法，以便可以随时保存好看的配色方案，在绘图时也可以很方便地预览和调用。\n功能 # 预览某个配色方案，或预览所有配色方案 调用配色方案 自定义并保存配色方案 从图片提取颜色定义配色方案 前提 # MatLab版本号高于2021a\n可选。jlShowColor.m中使用了tiledlayout()函数，这个函数是在2021a版本引入MatLab的。如果MatLab版本低于2021a，可修改jlShowColor.m文件，将使用的tiledlayout()函数改为subplot()等功能相似的函数。\n图像处理工具箱 （Image Processing Toolbox）\n可选。如果需要使用从图片上提取颜色自制配色方案的话就需要在MatLab内安装此工具箱。\n下载安装 # MatLab默认将用户自定义的脚本、函数工具等放在~/Documents/MATLAB文件夹下，这里建议将本绘图配色方案库也放在此文件夹下：\n1 git clone https://github.com/jin-li/jlcolor.git ~/Documents/MATLAB/jlcolor 然后将本配色方案库所在的目录加入到MATLAB的搜索路径中。 也可通过命令搞定。打开MatLab，在命令窗口运行如下命令：\nWindows用户 1 2 addpath([getenv(\u0026#39;USERPROFILE\u0026#39;),\u0026#39;\\Documents\\MATLAB\\jlcolor\u0026#39;]); savepath; Linux或macOS用户 1 2 addpath([getenv(\u0026#39;HOME\u0026#39;),\u0026#39;/Documents/MATLAB/jlcolor\u0026#39;]); savepath; 工具简介 # 预览配色方案\n打开MatLab，在命令窗口输入如下命令可预览目前库中所有配色方案： 1 jlShowColor(\u0026#39;all\u0026#39;) 库中的配色方案和它们的名字将会展示出来： 也可预览单个配色方案，例如预览名为“彩虹”的配色方案，就可以在命令窗口运行如下命令： 1 jlShowColor(\u0026#39;彩虹\u0026#39;) 调用配色方案\n使用jlGetColor()函数可以方便地调用本配色方案库中储存的配色方案，例如 1 c = jlGetColor(\u0026#39;彩虹\u0026#39;) 就可以将“彩虹”配色方案中的7种颜色调出到变量c中了。 然后将可以在绘图时方便地使用这7种颜色啦，例如 1 2 3 4 5 6 c = jlGetColor(\u0026#39;彩虹\u0026#39;); % 获取配色 figure; hold on; x=0:0.01:1; for i=1:length(c) plot(x,sin(2*pi*x-pi*i/12),\u0026#39;color\u0026#39;,c(i),\u0026#39;linewidth\u0026#39;,5); end 效果如下图： 自定义并保存配色方案\n本配色方案库中各个配色方案保存在jlColorLib.m中，用户可以手动往jlColorLib.m文件中添加配色方案。例如MatLab中默认的几种配色如下： 用户可以将这7种颜色的十六进制码按照jlColorLib.m中已有配色方案的写法添加进去，即在jlColorLib.m文件中添加一行\n1 jlColor(\u0026#39;matlab\u0026#39;) = [\u0026#39;#0072BD\u0026#39;;\u0026#39;#D95319\u0026#39;;\u0026#39;#EDB120\u0026#39;;\u0026#39;#7E2F8E\u0026#39;;\u0026#39;#77AC30\u0026#39;;\u0026#39;#4DBEEE\u0026#39;;\u0026#39;#A2142F\u0026#39;]; MatLab中的默认配色方案就以\u0026quot;matlab\u0026quot;为名保存在配色方案库中了。\n也可以使用jlAddColor()函数来添加配色方案。直接在MatLab命令窗口运行下面两行代码：\n1 2 colors = [\u0026#34;#0072BD\u0026#34;,\u0026#34;#D95319\u0026#34;,\u0026#34;#EDB120\u0026#34;,\u0026#34;#7E2F8E\u0026#34;,\u0026#34;#77AC30\u0026#34;,\u0026#34;#4DBEEE\u0026#34;,\u0026#34;#A2142F\u0026#34;]; jlAddColor(colors, \u0026#34;matlab\u0026#34;); 从图片中提取颜色自制配色方案\n使用库中提供的jlAddColorFromImage()可以方便地从一张图片中提取想要的颜色来生成自己的配色方案。例如上面使用的“彩虹”配色和库中的“落日余晖”配色都是根据图片生成的。准备好需要的图片文件sunset.jpeg，在Matlab命令窗口中输入下面的代码：\n1 jlAddColorFromImage(\u0026#39;sunset.jpeg\u0026#39;,\u0026#39;落日余晖\u0026#39;); 然后MatLab就会弹出下图所示的窗口，用鼠标在图片上连续点取所需的颜色即可： 选完按回车键结束，就会弹出另一个窗口来展示已成功地创建了这个配色方案！ 鸣谢 # 本库中使用的RGB转十六进制的函数来自MathWorks社区用户Chad Greene上传的脚本。 本库中部分配色方案取自知乎用户CoderMan发布的知乎专栏文章【2.1.1 颜色模板】科研绘图必备的60套颜色模板中提到的配色方案。 ","date":"2022年7月22日","externalUrl":null,"permalink":"/p/matlab%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%98%E5%9B%BE%E9%85%8D%E8%89%B2%E6%96%B9%E6%A1%88%E5%BA%93jlcolor/","section":"Posts","summary":" 缘起 # 在使用MatLab绘图时有时会对MatLab默认的配色方案不太满意，平时遇到好看的配色方案也会想要保存下来自己用。但是之前保存的MatLab使用笔记过于凌乱，想要用某个配色时不太好找，所以就有了自制一个配色方案库的想法，以便可以随时保存好看的配色方案，在绘图时也可以很方便地预览和调用。\n","title":"MatLab自定义绘图配色方案库jlcolor","type":"post"},{"content":"","date":"2022年7月22日","externalUrl":null,"permalink":"/tags/%E7%BB%98%E5%9B%BE/","section":"Tags","summary":"","title":"绘图","type":"tags"},{"content":"","date":"2022年7月22日","externalUrl":null,"permalink":"/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/","section":"Tags","summary":"","title":"计算机基础","type":"tags"},{"content":"","date":"2022年7月22日","externalUrl":null,"permalink":"/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%BD%AF%E4%BB%B6/","section":"Tags","summary":"","title":"计算机软件","type":"tags"},{"content":"","date":"2022年7月22日","externalUrl":null,"permalink":"/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A1%AC%E4%BB%B6/","section":"Tags","summary":"","title":"计算机硬件","type":"tags"},{"content":"","date":"2022年7月22日","externalUrl":null,"permalink":"/tags/%E9%85%8D%E8%89%B2/","section":"Tags","summary":"","title":"配色","type":"tags"},{"content":"","date":"2022年4月1日","externalUrl":null,"permalink":"/tags/bash/","section":"Tags","summary":"","title":"Bash","type":"tags"},{"content":"","date":"2022年4月1日","externalUrl":null,"permalink":"/tags/linux%E5%AD%90%E7%B3%BB%E7%BB%9F/","section":"Tags","summary":"","title":"Linux子系统","type":"tags"},{"content":" 简介 # Windows的Linux子系统（Windows Subsystem for Linux），简称WSL，是微软公司开发的一个可以在Windows 10或Windows 11上运行原生Linux二进制程序的兼容层。借助WSL，Windows用户不再需要安装Linux系统就可以使用Linux上的工具和程序，而且可以在WSL中直接使用Windows系统的文件系统，用户在两个系统间的切换也更加流畅和方便。\nWSL的初始版本，或WSL1在2016年8月发布。WSL1是一个Linux兼容层，用户在WSL1中输入的Linux指令由微软实现的接口转换为对于Windows的调用，这种模式具有不少的局限性，例如它只能运行一部分Linux软件，缺乏音频支持和GPU支持等。\n微软在2019年6月发布了WSL的第二个版本，也是目前最常用的一个版本WSL2。WSL2使用了虚拟化的技术，使得在Windows上运行Linux内核和各种发行版成为可能，同时WSL2对于图形化的支持更好，利用gWSL可方便地运行一些图形化Linux程序。\n因此这里我们推荐安装WSL2，本文中若无特殊说明，提到WSL时均指WSL2。\n前提 # Windows 10版本号高于或等于1903（2019年春季更新），最好高于或等于2004（2020年春季更新） 安装 # 方法一（Windows 10版本高于或等于2004） # 在开始菜单搜索\u0026quot;Windows Powershell\u0026quot;，然后右击选择“以管理员身份运行”，然后在打开的命令行窗口输入如下命令\n1 wsl.exe --install 然后回车运行。等待数分钟后WSL2就会自动完成安装，然后需要重启Windows，在WSL命令窗口根据提示设置用户名和密码。\n方法二（Windows 10版本高于或等于1903，但低于2004） # 开启WSL。 在开始菜单搜索\u0026quot;Windows Powershell\u0026quot;，然后右击选择“以管理员身份运行”，然后在打开的命令行窗口输入如下命令并运行\n1 dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart 开启Windows 10自带的虚拟机平台。 继续在Powershell的命令行窗口（管理员身份运行）输入如下命令并运行\n1 Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart 将WSL默认版本设置为WSL2。 继续在Powershell的命令行窗口（管理员身份运行）输入如下命令并运行\n1 wsl --set-default-version 2 安装Linux发行版。 然后你可以在微软应用商店搜索并安装支持的Linux发行版。这里我们推荐安装Ubuntu。安装好之后打开你安装的Linux发行版，根据提示设置用户名和密码。\n","date":"2022年4月1日","externalUrl":null,"permalink":"/p/windows%E4%B8%8B%E7%9A%84%E5%8E%9F%E7%94%9Flinux%E5%AD%90%E7%B3%BB%E7%BB%9Fwsl%E7%9A%84%E5%AE%89%E8%A3%85%E4%B8%8E%E9%85%8D%E7%BD%AE/","section":"Posts","summary":" 简介 # Windows的Linux子系统（Windows Subsystem for Linux），简称WSL，是微软公司开发的一个可以在Windows 10或Windows 11上运行原生Linux二进制程序的兼容层。借助WSL，Windows用户不再需要安装Linux系统就可以使用Linux上的工具和程序，而且可以在WSL中直接使用Windows系统的文件系统，用户在两个系统间的切换也更加流畅和方便。\n","title":"Windows下的原生Linux子系统WSL的安装与配置","type":"post"},{"content":"","date":"24 二月 2022","externalUrl":null,"permalink":"/en/tags/seo/","section":"Tags","summary":"","title":"SEO","type":"tags"},{"content":"本网站使用Hugo搭建，而且使用的stack主题支持自动生成基于Open Graph协议（OG协议）的标签，此处记录一下如何在Hugo搭建的网站中做搜索引擎优化（SEO）。\n引言 # 到本系列的第三篇文章为止，我们已经建立了一个可以依靠域名和HTTPS协议访问的中英双语个人网站。但是，如果我们试图在搜索引擎上搜索我们的网站，就会发现我们网站上的什么内容都不会出现在搜索引擎上。\n这是因为我们的网站并没有被搜索引擎收录，也没有与外界的网站建立连接。因此，想要让其他人能在搜索引擎上发现我们的网站，我们就需要将网站提交给搜索引擎。同时，也可以做一些搜索引擎优化（SEO），以让搜索引擎获得关于我们网站的更详细的信息，这有助于提升我们的网站在搜索引擎中的排名。\n开启Open Graph # Open Graph（开放图谱）协议，简称OG协议，是Facebook在2010年公布的一项协议，用来标记网页内容。简单来讲，OG协议就是嵌在网页头部的一些标签，这些标签标记了网页的标题、描述等特征，使得网页成为一个“富媒体对象”，可以被其他社交网站引用。\n很多搜索引擎都支持OG协议，在网页中使用OG协议的标签，就更有利于提升我们的网页在搜索引擎中的排名。\nOG协议的标签在网页中通常表示为类似下面所示的格式：\n1 2 3 4 \u0026lt;meta property=\u0026#34;og:title\u0026#34; content=\u0026#34;The Rock\u0026#34; /\u0026gt; \u0026lt;meta property=\u0026#34;og:type\u0026#34; content=\u0026#34;video.movie\u0026#34; /\u0026gt; \u0026lt;meta property=\u0026#34;og:url\u0026#34; content=\u0026#34;https://www.imdb.com/title/tt0117500/\u0026#34; /\u0026gt; \u0026lt;meta property=\u0026#34;og:image\u0026#34; content=\u0026#34;https://ia.media-imdb.com/images/rock.jpg\u0026#34; /\u0026gt; stack主题提供了对OG协议的支持，只需要在网站根目录下的config/_default/params.en.yaml和config/_default/params.zh-cn.yaml配置文件中开启即可：\n1 2 3 4 5 6 7 opengraph: twitter: # Your Twitter username site: JinliCyou # Available values: summary, summary_large_image card: summary_large_image 这样，Hugo在生成和部署网站时就会在网页HTML文件中自动嵌入OG标签。\n谷歌搜索优化 # 在将我们的站点信息提交给谷歌时，谷歌需要验证我们对网站的所有权。验证方式有好几种，例如\n在网站根目录下放一个谷歌生成的验证文件 在网页HTML文件头部嵌入谷歌生成的特定标签 使用谷歌分析的Tracking ID（或者Measurement ID） 由于stack主题集成了对谷歌分析的支持，这里我们使用第三种验证方式。\n开启谷歌分析 # 谷歌分析（Google Analytics）是一个分析网站流量的工具，用它可以统计网站的访问量等信息。\n首先前往谷歌分析官网注册谷歌分析的账号，也可以直接用已有的谷歌账号登录。现在的谷歌分析一般是谷歌分析4（Google Analytics 4），使用Measurement ID而非之前的Tracking ID来跟踪网站。\n获取Measurement ID。具体可参见谷歌分析的帮助文档。下面是具体操作：\n在用户首页找到“Admin“选项，新建一个“Property”，按照说明填入必要的信息。 然后点击“Property”这一列中的“Data Streams”选项。 点击“Add stream”，选择“Web”，填入你的网站域名和网站名字。 再在“Property”页面点击刚添加的stream，就能看到一个以“G-”开头的Measurement ID。记录下你的网站的Measurement ID。 在网站根目录下的config/_default/config.yaml配置文件中找到“googleAnalytics\u0026quot;配置项，填入你的Measurement ID。\n1 2 # GA Tracking ID googleAnalytics: G-measuremntID 提交站点地图 # 站点地图（Site Map）是一个存储有站点网页信息的XML数据文件，通常命名为sitemap.xml，将它提交给搜索引擎，搜索引擎将可以获取我们网站的网页信息。\nHugo会在生成和部署网站时在public文件夹下自动生成sitemap.xml文件。\n我们把站点地图提交到谷歌搜索，具体说明可参见谷歌站长页面的说明，下面是具体操作：\n登录谷歌搜索控制台（Google Search Console）https://search.google.com/search-console，可以使用在谷歌分析注册的账号。\n点击左上角的“Add property”，选择右侧的“URL prefix”方式，输入以https开头的网站网址。在验证所有权的选项中选择“Google Analytics”，点击验证。如果你在上一步开启谷歌分析后使用Hugo重新部署了网站的话，就可以直接验证通过。\n提交站点地图文件sitemap.xml。在左侧菜单栏点击“Sitemaps”选项，然后在添加站点地图的页面填入sitemap.xml所在的URL。例如对于本站，由于是双语站点，Hugo在部署网站时会生成3个sitemap.xml文件，分别是/public/sitemap.xml、/public/zh-cn/sitemap.xml以及/public/en/sitemap.xml。\n注意，添加sitemap时不要漏了路径开头的斜杠/，即使网站域名后面已经有一个斜杠了，也不能省略。如下图所示 提交成功之后“status”会显示“success”。\nHugo生成的3个站点地图中，/public/sitemap.xml中的内容其实是指向/public/zh-cn/sitemap.xml和/public/en/sitemap.xml的，所以我们只提交一个/public/sitemap.xml就可以。\n一般在站点地图成功提交之后大约1到2天后，就可以看到自己的网站已经被谷歌收录了。可以在谷歌搜索框中输入site:xxx.com来查看某个网站是否被谷歌搜索收录。\n百度搜索优化 # 针对百度搜索的优化是在百度资源搜索平台上完成的。\n前往百度资源搜索平台，登录百度账号。\n点击“链接提交”，然后点击\u0026quot;添加站点\u0026quot;。输入你的网站域名，同样需要验证站点的所有权，这里选择下载验证文件，然后把验证文件放在网站public文件夹内。点击“验证”即可。\n然后点击左侧菜单栏“资源提交”中的“普通收录”，在资源提交的页面下选择“sitemap”，输入sitemap.xml所在的URL就可以了。\n不过在百度提交sitemap有两个限制：\n不允许提交索引型sitemap 对新账号每天只允许提交一个sitemap文件 ","date":"2022年2月24日","externalUrl":null,"permalink":"/p/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99%E7%9A%84%E5%BB%BA%E7%AB%8B%E8%BF%87%E7%A8%8B%E5%9B%9B%E7%BD%91%E7%AB%99%E7%9A%84%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E%E4%BC%98%E5%8C%96seo/","section":"Posts","summary":"本网站使用Hugo搭建，而且使用的stack主题支持自动生成基于Open Graph协议（OG协议）的标签，此处记录一下如何在Hugo搭建的网站中做搜索引擎优化（SEO）。\n","title":"个人网站的建立过程（四）：网站的搜索引擎优化（SEO）","type":"post"},{"content":"","date":"2022年2月24日","externalUrl":null,"permalink":"/tags/%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E%E4%BC%98%E5%8C%96/","section":"Tags","summary":"","title":"搜索引擎优化","type":"tags"},{"content":"","date":"22 二月 2022","externalUrl":null,"permalink":"/en/tags/hugo-themes/","section":"Tags","summary":"","title":"Hugo Themes","type":"tags"},{"content":"","date":"22 二月 2022","externalUrl":null,"permalink":"/en/tags/multi-lingual/","section":"Tags","summary":"","title":"Multi-Lingual","type":"tags"},{"content":"","date":"2022年2月22日","externalUrl":null,"permalink":"/tags/%E5%A4%9A%E8%AF%AD%E7%A7%8D/","section":"Tags","summary":"","title":"多语种","type":"tags"},{"content":"本网站使用Hugo主题stack，此处记录一下如何使用不同的Hugo主题，stack主题的特点，以及对stack主题做的优化。\n缘起 # 最初建立个人网站时使用了Hexo框架，也尝试了几个Hexo框架的主题，包括hexo-theme-yilia、hexo-theme-yilia-plus、hexo-theme-next。也使用过一个用户名为“leirock”的GitHub用户基于hexo-theme-next创建的一个主题https://leirock.github.io/blog/，这个主题是我目前为止最喜欢的一个主题，其GitHub仓库地址为https://github.com/leirock/blog，效果如下：\n然而遗憾的是，由于在Hexo上配置多语言十分糟心，我最终决定放弃Hexo，并转向了使用Hugo框架。使用Hugo框架后并没有尝试太多主题，一直在使用目前这个hugo-theme-stack。使用时发现这个主题缺了一些自己需要的功能，就动手修改了一下。\n前置条件 # 已配置域名\n已安装Hugo框架\n本系列文章的前两篇分别介绍了上面两项的操作过程，如果尚未完成的话请参考这两篇文章：\n\u0026ldquo;个人网站的建立过程（一）：购买个人域名并配置动态域名解析\u0026rdquo; \u0026ldquo;个人网站的建立过程（二）：使用Hugo框架搭建个人网站\u0026rdquo; 安装主题 # 我们把本地服务器上的网站目录称为“网站根目录”。例如~/Documents/www/website1是本文章中所用到的网站根目录。 我们把网站根目录里面themes文件夹下的主题目录称为“主题根目录”。例如~/Documents/www/website1/themes/hugo-theme-stack是本文章中所用到的主题根目录。 首先进入本地服务器网站根目录下，使用下面的命令将stack主题放在themes文件夹下：\n1 2 cd ~/Documents/www/website1 git clone https://github.com/CaiJimmy/hugo-theme-stack/ themes/hugo-theme-stack 应用示例网站。stack主题内带了一个示例网站，放在主题根目录下的exampleSite文件夹内，只要我们把exampleSite文件夹内的文件复制到网站根目录下，我们就可以应用示例网站了。不过需要注意的是，这将覆盖掉网站根目录下原有的同名文件，如果你不想让你的文件被覆盖掉，请在执行下面的命令前做好备份。\n将stack主题内exampleSite文件夹目录下的文件复制到网站根目录下：\n1 cp -r themes/hugo-theme-stack/exampleSite/* ./ 然后需要删除Hugo在网站根目录下默认生成的config.toml，防止跟从stack主题exampleSite复制到网站根目录下的config.yaml冲突。\n1 rm config.toml 预览示例网站。在网站根目录下执行如下命令：\n1 hugo server 然后在本地服务器打开浏览器，输入http://localhost:1313即可预览示例网站，显示效果可参见本文的封面图片，或stack主题作者给出的示例网站https://demo.stack.jimmycai.com/。\n基本使用指南 # Hugo网站文件结构 # 网站根目录下面的文件夹目录树一般如下所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 . ├── archetypes │ └── default.md ├── config.yaml # 网站配置文件 ├── content # 站点内的内容都在这里 │ ├── categories # “分类”页面的首页 │ │ └── Test # “分类”页面下的一个分类页面 │ ├── page # 显示在网站主页左侧边栏菜单的选项 │ │ ├── about # 左侧边栏菜单中的“关于”页面 │ │ ├── archives # 左侧边栏菜单中的“归档”页面 │ │ ├── links # 左侧边栏菜单中的“链接”页面 │ │ └── search # 左侧边栏菜单中的“搜索”页面 │ └── post # 用户写的帖子都放在这里，每个子文件夹对应一个帖子 │ ├── chinese-test │ ├── emoji-support │ ├── markdown-syntax │ ├── math-typesetting │ ├── placeholder-text │ └── rich-content ├── data ├── layouts ├── LICENSE ├── README.md ├── resources │ └── _gen │ ├── assets │ └── images ├── static # 放用户自定义字体、用户头像、网站小图标等 └── themes # 放各种主题 └── hugo-theme-stack # stack主题 ├── archetypes ├── assets ├── config.yaml ├── data ├── debug.sh ├── exampleSite ├── go.mod ├── i18n ├── images ├── layouts ├── LICENSE ├── netlify.toml ├── README.md └── theme.toml 此外，上面没有显示的是网站根目录下的private文件夹和public文件夹。\npublic文件夹会在运行hugo -D部署网站时生成，是暴露给外界的文件夹，网站上页面的HTML文件都会由Hugo生成并放在public文件夹下。public文件夹内不要放置任何包含用户信息的敏感文件！ private文件夹可以由用户创建，这里我们用它来存放网站的SSL证书。 关于网站根目录下各文件夹，更详细的说明可参见Hugo的官方文档https://gohugo.io/getting-started/directory-structure/。\n配置文件夹config # Hugo支持两种配置方式：\n一种是直接在网站根目录的.config文件中配置 另一种是在网站根目录下创建config文件夹，配置放在config文件夹内 第二种配置方式支持将不同的配置项分开放在不同的文件中，相比第一种配置方式可以让配置项的条理更加清晰，因此我们这里选择第二种配置方式。本网站的配置目录结构如下所示：\n1 2 3 4 5 6 7 8 config └── _default ├── config.yaml ├── languages.yaml ├── menu.en.yaml ├── menu.zh-cn.yaml ├── params.en.yaml └── params.zh-cn.yaml 关于Hugo的配置，更详细的说明可参考官方文档https://gohugo.io/getting-started/configuration/。\n修改并优化主题 # 这里我对原stack主题做了两项修改：\n为用户的联系方式添加自定义图标，使得主页左侧边栏用户头像下可以显示豆瓣、知乎、码云等图标 在主页左侧边栏底部和文章页面右侧边栏顶部添加语言转换按钮，便于网页的中英文切换 自定义图标 # 由于原stack主题只提供了有限的几个社交图标，包括GitHub、Twitter和RSS，却不包含中国用户常用的微博、知乎等，所以我决定更换原stack主题使用的svg图标，引入包含微博、知乎等图标的字体。\n之前用Hexo框架时，在Hexo的yilia主题和yilia-plus主题中看到过不少社交图标，于是就将这两款主题里的图标引入了stack主题，下面是具体操作。\n获取yilia-plus主题中的字体。前往yilia-plus主题的GitHub仓库https://github.com/JoeyBling/hexo-theme-yilia-plus，下载其中source-src/css目录下的fonts文件夹，并将该文件夹复制到网站根目录下的static文件夹中。\n更改stack主题菜单的样式。找到stack主题根目录下的assets/scss/partial/menu.scss文件，这是stack主题中左侧菜单的样式文件，将文件内最后一项定义.social-menu的代码块更换为如下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 .social-menu { list-style: none; padding: 0%; display: flex; flex-direction: row; gap: 0px; a { border-radius:50%; display:-moz-inline-stack; display:inline-block; vertical-align:middle; *vertical-align:auto; zoom:1; *display:inline; margin:0 8px 15px 8px; transition:0.3s; text-align: center; color: #fff; opacity: 0.7; width: 28px; height: 28px; line-height: 26px; \u0026amp;:hover { opacity:1 } } a.weibo { background: #aaaaff; border:1px solid #aaaaff; \u0026amp;:hover { border:1px solid #aaaaff; } } a.segmentfault { background: #009a61; border:1px solid #009a61; \u0026amp;:hover { border:1px solid #009a61; } } a.rss { background: #ef7522; border:1px solid #ef7522; \u0026amp;:hover { border:1px solid #cf5d0f; } } a.github { background: #afb6ca; border:1px solid #afb6ca; \u0026amp;:hover { border:1px solid #909ab6; } } a.gitee { background: #c8171e; border:1px solid #c8171e; \u0026amp;:hover { border:1px solid #c8171e; } } a.facebook { background: #3b5998; border:1px solid #3b5998; \u0026amp;:hover { border:1px solid #2d4373; } } a.google { background: #4086f4; border:1px solid #4086f4; \u0026amp;:hover { border:1px solid #4086f4; } } a.twitter { background: #55cff8; border:1px solid #55cff8; \u0026amp;:hover { border:1px solid #24c1f6; } } a.linkedin { background: #005a87; border:1px solid #005a87; \u0026amp;:hover { border:1px solid #006b98; } } a.acfun { background: #fd4c5d; border:1px solid #fd4c5d; \u0026amp;:hover { border:1px solid #fd4c5d; } } a.bilibili { background: #e15280; border:1px solid #e15280; \u0026amp;:hover { border:1px solid #e15280; } } a.zhihu { background: #0078d8; border:1px solid #0078d8; \u0026amp;:hover { border:1px solid #0078d8; } } a.douban { background: #06c611; border:1px solid #06c611; \u0026amp;:hover { border:1px solid #06c611; } } a.mail { background: #005a87; border:1px solid #005a87; \u0026amp;:hover { border:1px solid #006b98; } } a.jianshu { background: #ff5722; border:1px solid #ff5722; \u0026amp;:hover { border:1px solid #ff5722; } } a.weixin { background: #4caf50; border:1px solid #4caf50; \u0026amp;:hover { border:1px solid #4caf50; } } a.qq { background: #34baad; border:1px solid #34baad; \u0026amp;:hover { border:1px solid #34baad; } } a.psn { background: #086ef6; border:1px solid #086ef6; \u0026amp;:hover { border:1px solid #086ef6; } } } 添加字符索引。该字体的字符索引将作为stack主题中的可定制样式文件中作为用户自定义样式。前往yilia-plus主题的GitHub仓库https://github.com/JoeyBling/hexo-theme-yilia-plus，下载其中的source-src/css/fonts.scss文件，将其中的内容全部复制到stack主题根目录下的assets/scss/custom.scss文件内。\n打开stack主题根目录下的layouts/partials/sidebar/left.html文件，找到{{- with .Site.Menus.social -}}这一行，将这一行和其对应的{{- end -}}这一行中间的内容替换为以下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;ol class=\u0026#34;social-menu\u0026#34;\u0026gt; {{ range . }} \u0026lt;li\u0026gt; \u0026lt;a class=\u0026#34;{{ .Identifier }}\u0026#34; href=\u0026#34;{{ .URL }}\u0026#34; {{ with .Name }}title=\u0026#34;{{ . }}\u0026#34;{{ end }} {{ if eq (default true .Params.newTab) true }}target=\u0026#34;_blank\u0026#34;{{ end }} \u0026gt; \u0026lt;i class=\u0026#34;icon-{{ .Params.Icon }}\u0026#34;\u0026gt;\u0026lt;/i\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/li\u0026gt; {{ end }} \u0026lt;/ol\u0026gt; 如需在主页左侧边栏头像下显示社交网站图标，则只需在配置文件中加入对应的项即可。例如如果想要显示GitHub和电子邮件，则可以在config/_default/menu.en.yaml和config/_default/menu.zh-cn.yaml配置文件中加入如下配置内容：\n1 2 3 4 5 6 7 8 9 10 11 12 social: - identifier: github name: GitHub url: https://github.com/jin-li/ params: icon: github - identifier: mail name: Email url: \u0026#34;mailto:i@jinli.cyou\u0026#34; params: icon: mail 显示效果可参考本网站主页左侧边栏。\n语言转换按钮 # 尽管Hugo框架原生支持多语言，但stack主题却未提供语言转换的按钮，我参考了Hugo论坛上的相关讨论，并修改了stack主题的代码，添加了语言转换的按钮。\n具体功能有两个：\n在主页左侧边栏的底部“暗色模式”按钮下面添加一个语言转换按钮，实现全站的语言转换。 在当前站点语言是中文时，点击按钮则站点语言变为英文 在当前站点语言是英文时，点击按钮则站点语言变为中文 在文章页面的右侧边栏顶部“目录”上面添加一个语言转换按钮，实现单个页面的语言转换。 在当前页面对应有双语页面时，显示转为另一种语言的语言转换按钮 在当前页面对应没有双语页面时，不显示语言转换按钮 具体效果可参考本网站。下面是具体操作。\n下载一个翻译按钮的SVG文件，并放到stack主题根目录的assets/icons文件夹内。\n找到stack主题根目录下的assets/scss/partials/sidebar.scss文件，此文件是定义网站侧边栏的样式文件。在文件末位添加如下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 .language-select{ align-self: stretch; color: var(--body-text-color); align-items: center; } .language-select #selected{ font-weight: 500; display: flex; margin-top: 0px; margin-bottom: 0px; } .language-select #to-select{ font-weight: 100; display: flex; a { margin-left: 60px; } margin-top: 0px; margin-bottom: 0px; } 找到stack主题根目录下的assets/scss/partials/layout/article.scss文件，加入以下内容：\n1 2 3 4 5 6 7 8 9 .widget--translation { background-color: var(--card-background); border-radius: var(--card-border-radius); box-shadow: var(--shadow-l1); display: flex; flex-direction: column; color: var(--card-text-color-main); overflow: hidden; } 找到stack主题根目录下的layouts/partials/sidebar/left.html文件，在该文件的倒数第3行处加入以下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 {{ if .Site.IsMultiLingual }} {{ $siteLanguages := .Site.Languages}} {{ $pageLang := .Page.Lang}} {{ range .Page.AllTranslations }} {{ $translation := .}} {{ range $siteLanguages }} {{ if eq $translation.Lang .Lang }} {{ $selected := false }} {{ if eq $pageLang .Lang}} {{ else }} \u0026lt;div class=\u0026#34;language-select\u0026#34;\u0026gt; \u0026lt;li id=\u0026#34;selected\u0026#34;\u0026gt;{{ partial \u0026#34;helper/icon\u0026#34; \u0026#34;trans\u0026#34; }}\u0026lt;a href=\u0026#34;{{ $translation.RelPermalink }}\u0026#34;\u0026gt;\u0026lt;span\u0026gt;{{ .LanguageName }}\u0026lt;/span\u0026gt;\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/div\u0026gt; {{ end }} {{ end }} {{ end }} {{ end }} {{ end }} 打开stack主题根目录下的layouts/_default/single.html文件，找到该文件最后一个定义右侧边栏的代码块。将其替换为以下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 {{ if .Site.IsMultiLingual }} {{ $siteLanguages := .Site.Languages}} {{ $pageLang := .Page.Lang}} {{ range .Page.AllTranslations }} {{ $translation := .}} {{ range $siteLanguages }} {{ if eq $translation.Lang .Lang }} {{ $selected := false }} {{ if eq $pageLang .Lang}} {{ else }} \u0026lt;div class=\u0026#34;language-select\u0026#34;\u0026gt; \u0026lt;li id=\u0026#34;selected\u0026#34;\u0026gt;{{ partial \u0026#34;helper/icon\u0026#34; \u0026#34;trans\u0026#34; }}\u0026lt;a href=\u0026#34;{{ $translation.RelPermalink }}\u0026#34;\u0026gt;\u0026lt;span\u0026gt;{{ .LanguageName }}\u0026lt;/span\u0026gt;\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/div\u0026gt; {{ end }} {{ end }} {{ end }} {{ end }} {{ end }} 在网站根目录配置文件夹config/_default内添加语言配置文件languages.yaml，加入以下内容：\n1 2 3 4 5 6 7 en: languageName: \u0026#34;English\u0026#34; languageNameShort: \u0026#34;en\u0026#34; zh-cn: languageName: \u0026#34;简体中文\u0026#34; languageNameShort: \u0026#34;zh\u0026#34; 最后将两种语言对应的文章在文章文件夹内分别命名为index.md和index.en.md，在用Hugo部署网站时Hugo就会自动生成两种语言的网页，并自动添加语言转换的按钮。\n","date":"2022年2月22日","externalUrl":null,"permalink":"/p/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99%E7%9A%84%E5%BB%BA%E7%AB%8B%E8%BF%87%E7%A8%8B%E4%B8%89hugo%E4%B8%BB%E9%A2%98stack%E7%9A%84%E4%BD%BF%E7%94%A8%E4%B8%8E%E4%BC%98%E5%8C%96/","section":"Posts","summary":"本网站使用Hugo主题stack，此处记录一下如何使用不同的Hugo主题，stack主题的特点，以及对stack主题做的优化。\n缘起 # 最初建立个人网站时使用了Hexo框架，也尝试了几个Hexo框架的主题，包括hexo-theme-yilia、hexo-theme-yilia-plus、hexo-theme-next。也使用过一个用户名为“leirock”的GitHub用户基于hexo-theme-next创建的一个主题https://leirock.github.io/blog/，这个主题是我目前为止最喜欢的一个主题，其GitHub仓库地址为https://github.com/leirock/blog，效果如下：\n","title":"个人网站的建立过程（三）：Hugo主题stack的使用与优化","type":"post"},{"content":"本网站运行在我的私人电脑上，此处记录一下本网站的建立过程。\n缘起 # 在购买了域名之后就一直想用这个域名搭建一个个人网站。刚开始曾尝试过使用Hexo作为静态网站框架，但在使用过程中发现由于Hexo中并没有对多语种的原生支持，只能使用一些与国际化有关的插件（i18n），因此在我想尝试搭建一个中英双语的网站时遇到了数不清的问题，最终决定放弃，并转投Hugo框架。\nHugo是一个用Go语言开发的静态网站生成器，以生成网站速度快著称，我则是看中了它原生支持多语言的特性。更多关于Hugo特性的介绍，可移步Hugo官网https://gohugo.io。本网站的“关于”页面中也有一些对Hugo框架的简要介绍。\n前置条件 # 运行Linux系统的电脑 (作为私人服务器) nginx (作为HTTP服务器) Go语言运行环境 Hugo (个人网站框架) 私有域名 （详细操作请查看本系列的上一篇帖子\u0026ldquo;个人网站的建立过程（一）：购买个人域名并配置动态域名解析\u0026rdquo;） 配置网站服务器 # 本网站使用nginx作为HTTP服务器，服务器电脑使用深度操作系统（deepin OS），静态网站框架使用Hugo。\n安装并配置nginx # 安装nginx 1 sudo apt install nginx 开启nginx服务 1 sudo systemctl enable nginx 安装并配置Hugo # 用apt或snap安装Hugo之后遇到了1313端口被占用的报错，于是改为下载deb包直接安装。首先在Hugo项目GitHub仓库的Release页面下载最新版的适用于Linux Debian系系统的deb安装包，最好下载“hugo_extended”版本，如hugo_extended_0.92.2_Linux-64bit.deb。下载后使用dpkg包管理器安装： 1 sudo dpkg -i hugo_extended_0.92.2_Linux-64bit.deb 在终端输入如下命令查看是否安装成功： 1 hugo version 初始化网站。在本地服务器电脑上创建一个文件夹，用来存放网站文件。例如把文件夹路径为~/Documents/www/ 1 mkdir -p ~/Documents/www 然后用Hugo新建一个站点，站点名称为website1： 1 2 cd ~/Documents/www hugo new site website1 Hugo将在~/Documents/www/website1下创建网站模板。 本地测试。在本地浏览器上测试创建的网站： 1 hugo server 可以在本地服务器电脑上打开浏览器，输入默认的地址http://localhost:1313，如能看到Hugo生成的默认网页即表明网站在本地配置成功。 网站域名绑定 # 目前，我们的网站只能通过http://localhost:1313在本地服务器电脑的浏览器上访问，要想让该网站可以被世界各地的用户访问，我们需要给它绑定一个域名。\n创建nginx配置文件，将本地网站文件目录与域名绑定。 为了避免污染nginx原有的配置，我们新建一个配置文件：\n1 2 3 cd /etc/nginx sudo mkdir vhost sudo touch blog.conf 这将在/etc/nginx/vhost目录下创建一个针对我们网站的配置文件blog.conf。向blog.conf文件中写入如下配置内容（注意，需要sudo权限才能写入这个文件）：\nserver{\nlisten 80;\nroot /home/YourUserName/Documents/www/website1/public;\nserver_name www.your.domain.name;\nlocation /{\n}\n}\n注意，这里我们将域名指向了~/Documents/www/website1目录下的public文件夹，而非整个website1目录。因为我们不想暴露一些私有文件。\n如果你想为其他网站绑定域名，可以仿照上述格式加入另外的配置。\n为了让nginx识别到我们创建到配置文件/etc/nginx/vhost/blog.conf，我们在nginx的配置文件中包含此文件。在nginx的配置文件/etc/nginx/nginx.conf中找到http的配置block，在花括号结束前的一行中加入如下一行：\ninclude /etc/nginx/vhost/*.conf\n这将让nginx自动读取我们在/etc/nginx/vhost目录下创建的配置文件。再次重新加载nginx服务后，我们的域名就指向了在本地服务器电脑上创建的网站目录。\n更新nginx服务，并使用Hugo部署网站：\n1 2 3 sudo nginx -s reload cd ~/Documents/www/website1 hugo -D 这样，我们就可以在其他电脑上输入域名（如http://www.your.domain.name）来访问我们创建的网站了！\n配置SSL证书 # 完成上述工作后，我们的网站可以被世界各地的用户以HTTP方式访问。HTTP是“超文本传输协议”，其内容以明文传输，因此安全性很差。现在的大多数网站都使用更安全的HTTPS协议，而且不少浏览器都会限制使用HTTP协议的网站。因此当用户访问我们的网站时，浏览器会对用户发出安全警告，在搜索引擎的搜索排名中，我们的网站也会被降低权重，所以我们有必要将网站的传输协议升级为HTTPS。\nHTTPS协议依赖于SSL加密，需要申请SSL安全证书。很多SSL安全证书都需要付费申请，但也有一些网站提供免费的SSL安全证书，例如ZeroSSL。\n创建ZeroSSL账号 # 前往ZeroSSL官方网站https://zerossl.com，创建一个个人账号。\n使用acme.sh生成并自动定期更新SSL安全证书 # 直接在ZeroSSL网站上申请的SSL证书有效期一般只有60或90天，因此如果直接申请证书，那么每隔一段时间就需要重新申请。这里已经有人制作了自动从ZeroSSL申请并定期更新证书的工具acme.sh，我们参照里面的说明，在我们网站的本地服务器电脑上为我们的网站安装SSL安全证书。\n具体过程参见acme.sh的说明，这里简述我们的网站需要用到的步骤和命令。\n安装acme.sh 1 curl https://get.acme.sh | sh -s email=my@example.com 由于我们使用阿里云的域名，这里我们选择手动DNS的方式生成证书。具体说明参见这里。简单来说，就是利用阿里云提供的域名管理API（即我们在上一篇帖子\u0026ldquo;个人网站的建立过程（一）：购买个人域名并配置动态域名解析\u0026rdquo;中提到的Access key）。我们可以登录阿里云账户重新获取一个Access key，也可以使用之前生成的Access key。将Access key导出为系统变量Ali_Key和Ali_Secret，注意不要泄露此信息，这关系到我们阿里云账号的安全。 1 2 export Ali_Key=\u0026#34;sdfsdfsdfljlbjkljlkjsdfoiwje\u0026#34; export Ali_Secret=\u0026#34;jlsdflanljkljlfdsaklkjflsa\u0026#34; 然后用下面的命令生成证书： 1 acme.sh --issue --dns dns_ali -d your.domain.com -d www.your.domain.com 复制/安装证书到使用的地方。默认生成的证书都放在安装目录~/.acme.sh/下，但我们不直接使用安装目录下的证书，而是将它复制到指定到目标位置。 1 2 3 4 acme.sh --install-cert -d your.domain.com -d www.your.domain.com \\ --key-file /home/YourUserName/Documents/www/website1/private/key.pem \\ --fullchain-file /home/YourUserName/Documents/www/website1/private/cert.pem \\ --reloadcmd \u0026#34;sudo service nginx force-reload\u0026#34; 上述命令在复制完证书后，会重启nginx服务以使HTTPS生效，需要输入sudo的密码。 修改nginx的配置文件，以支持HTTPS协议。在配置文件/etc/nginx/vhost/blog.conf中添加如下配置内容（注意，需要sudo权限才能写入这个文件）： server{\nlisten 443;\nssl on;\nssl_certificate /home/YourUserName/Documents/www/website1/private/cert.pem;\nssl_certificate_key /home/YourUserName/Documents/www/website1/private/key.pem;\nroot /home/YourUserName/Documents/www/website1/public;\nserver_name www.your.domain.name;\nlocation /{\n}\n}\n重定向HTTP请求到HTTPS。在配置文件/etc/nginx/vhost/blog.conf中添加如下配置内容（注意，需要sudo权限才能写入这个文件）： server{\nlisten 80;\nserver_name www.your.domain.name;\nreturn 301 https://$host$request_uri; }\n更新nginx服务 1 sudo nginx -s reload 就能使用HTTPS协议访问我们的网站啦！快在其他电脑上以https为前缀输入你的域名（例如https://www.your.domain.name ）试试吧！ 路由器设置 # 很多路由器默认都是不暴露内网设备的端口的，因此想要从公网访问我们网站的服务器，还需要在路由器上设置端口转发，让访问我们的域名的流量转发到网站服务器对应的端口上。例如上述nginx的配置中，我们的网站监听443端口的服务请求，就需要在路由器上设置转发443端口。\n","date":"2021年12月17日","externalUrl":null,"permalink":"/p/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99%E7%9A%84%E5%BB%BA%E7%AB%8B%E8%BF%87%E7%A8%8B%E4%BA%8C%E4%BD%BF%E7%94%A8hugo%E6%A1%86%E6%9E%B6%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/","section":"Posts","summary":"本网站运行在我的私人电脑上，此处记录一下本网站的建立过程。\n缘起 # 在购买了域名之后就一直想用这个域名搭建一个个人网站。刚开始曾尝试过使用Hexo作为静态网站框架，但在使用过程中发现由于Hexo中并没有对多语种的原生支持，只能使用一些与国际化有关的插件（i18n），因此在我想尝试搭建一个中英双语的网站时遇到了数不清的问题，最终决定放弃，并转投Hugo框架。\n","title":"个人网站的建立过程（二）：使用Hugo框架搭建个人网站","type":"post"},{"content":"本网站使用我的个人域名，此处记录一下域名的购买和配置过程，包含动态域名解析（DDNS）。\n缘起 # 由于工作原因需要经常使用ssh登录家里的和办公室的个人电脑，但是这些电脑基本都没有固定的IP地址，所以当IP地址发生变动之后，往往不能及时连接到电脑上。\n于是就考虑使用域名代替IP地址来访问这些电脑，而且查了一下阿里云上面很多域名并不算太贵（以.com作为顶级域的次级域名一般比较贵，但.xyz，.top，.cyou等顶级域下面的次级域名有很多便宜的），就正式付诸行动。\n更新 # 我在2025年将域名转移到了Cloudflare上，关于Cloudflare的动态域名解析的文章可以参考“动态域名解析\u0026ndash;Cloudflare”。\n购买阿里云域名 # 注册阿里云账号\n前往阿里云国际版网站，注册境外账号（可以免除实名认证之类的麻烦）。\n查询和购买域名\n前往国际版阿里云域名市场，搜索想要注册的次级域名，如“jinli”。\n搜索结果会显示所有可用的域名： 用注册的阿里云账号购买需要的域名，如“jinli.cyou”。\n域名解析到服务器IP地址 # 购买的域名需要解析到服务器的IP地址才能直接通过域名访问服务器。 如果服务器具有固定IP，只需要在阿里云上将域名和服务器绑定即可。 如果服务器没有固定IP，则需要将域名动态解析到服务器的IP地址。 有固定IP——直接绑定 # 获取IP地址\n可以前往IPv6测试网站查看网络是否支持IPv6： 如果是类Unix系统，可以通过如下命令获取IPv4地址：\n1 curl ipinfo.io/ip 通过如下命令获取IPv6地址：\n1 curl ipv6.icanhazip.com 将域名与IP地址绑定\n登录阿里云国际版网站，点击右上角用户头像，在下拉菜单选择Account Management。 鼠标移到左上角菜单栏，会出现下拉菜单，点击“Domains”。 在域名列表点击你购买的域名。 点击右侧菜单“DNS Settings”。 点击“Add Record”来绑定域名和IP： Type：如果是IPv4地址，选择“A”；如果是IPv6地址，选择“AAAA”。 Host：输入需要绑定的三级域名地址，例如想要设置“surface.jinli.cyou”，就填入“surface”。关于更多域名名称的设置说明，可以点击后面的问号查看。 ISP Line：选default。 Value：输入你在上一步获取的IP地址（IPv4或IPv6与“Type”对应）。 TTL：IP地址刷新的时间间隔，默认的10分钟就行。 你可以绑定多个域名和IP，IPv4和IPv6都行。 测试是否绑定成功 绑定完成后可能需要10分钟的时间等待阿里云DNS服务器完成更新（一般提交之后立刻就会更新）。然后可以查看是否绑定成功。\n建议在另一个网络环境通过“ping”命令测试 1 ping surface.jinli.cyou 如果可以在公网ping通，则绑定成功。 完成！ 绑定成功之后就可以通过域名来访问服务器了，例如如果需要使用ssh远程登录服务器，就可以直接ssh域名：\n1 ssh lijin@surface.jinli.cyou -X 也可以用服务器搭建NAS（Network Attached Storage）服务等，然后通过域名访问。\n无固定IP——绑定之后再动态解析 # 如果电脑或服务器没有固定IP地址，则需要先按照上面的步骤在阿里云上绑定当前的IP地址。然后监控本机IP地址，一旦发生改变，就通过阿里云提供的API上传到阿里云DNS服务器，修改原来绑定的IP地址。\n使用一个Python3脚本来完成DNS动态解析 脚本使用了阿里云开放的API 需要使用你阿里云账户生成的Access key来免密码登录你的阿里云账户 为了账户安全，千万不要公开你的Access key。Access key一旦泄露，立刻登录你的阿里云账户删除该Access key。 下载DDNS脚本 DDNS(Dynamic Domain Name System)，即动态域名解析的脚本参考了GitHub上的aliyun-ddns-client。在美国使用该项目有两个问题： 源项目提供的用来获取IP地址的网站在美国无法使用 对IPv6的支持有一些问题 因此我fork了此项目，并修复了存在的问题，仓库地址：https://github.com/jin-li/aliyun-ddns-client 该DDNS脚本使用Python3运行 使用如下命令克隆仓库到本地，建议放在~/.config目录下（如果home下没有.config文件夹，则先创建该文件夹）： 1 2 cd ~/.config git clone https://github.com/jin-li/aliyun-ddns-client 安装依赖的Python库 requests 和 netifaces 如果使用Anaconda作为Python的包管理工具，则使用以下命令： 1 2 3 conda activate conda install requests conda install netifaces 如果使用pip3作为Python的包管理工具，则使用以下命令： 1 2 pip3 install requests pip3 install netifaces 在阿里云账户生成Access key 登录阿里云账户，点击左上角的菜单，在菜单里找到Management and Governance中的Resource Access Management： 进入RAM页面后，点击左侧菜单栏里Identities中的Users，然后点击Create User新建一个账户。填入Logon Name和Display Name，勾选API Call-based Access以便使用API登录账户： 新建用户之后回到RAM/Users页面，点击刚才新建的用户名，进入User设置页面。下拉到页面底部，点击User AccessKeys中的Create AccessKey Pair按钮，生成你的AccessKey。记录下弹出的Access ID 和 Access Key。注意，Access Key只会出现一次，当你关闭弹出的页面后就会消失，不过你可以重复创建。 配置并运行脚本 进入你刚才下载的aliyun-ddns-client文件夹，找到文件ddns.conf.example，将其重命名为ddns-conf。 用文本编辑器打开该文件（ddns.conf），填入你在上一步获取的Access ID 和 Access Key。再填入你想要解析的域名和对应的IP地址（IP地址正确与否不重要，如果错误的话，待会儿运行本脚本后会把它改正；但域名一定要是你在之前“有固定IP——直接绑定”第2步中创建的域名）。 如果你有多个域名需要解析，可以仿照ddns.conf中已有的例子自行添加域名，IPv4和IPv6都支持。再次强调，本文件中需要解析的域名需要是之前在“有固定IP——直接绑定”第2步中在阿里云上已经创建的域名。 然后就可以用Python3运行本脚本了： 1 python3 ddns.py 脚本每次运行时都会检测当前IP和阿里云DNS服务器上域名对应的IP是否相同，若不相同则会更新阿里云DNS服务器上的IP地址。 设置定时任务 如果电脑/服务器无固定IP，那么IP每隔一段时间就会改变，我们需要在IP改变之后尽快更新阿里云DNS服务器上的IP记录。 人工监控IP变化不太方便，我们可以每隔一段时间（例如每小时）让系统自动运行一次DDNS脚本，这样我们就可以做到在IP改变之后一小时内更新。 类Unix系统中的定时任务可以使用系统程序crontab实现，你可以在这里看到一些设置crontab的方法。 例如我们想要在每个小时的第42分钟运行该DDNS脚本，我们可以在命令行输入以下命令： 1 crontab -e 此命令会打开一个包含crontab定时任务的配置文件，在文件末尾加入一行 1 42 * * * * cd ~/.config/aliyun-ddns-client ipython3 ddns.py 即可。 之后可以使用如下命令查看crontab定时任务是否在运行： 1 crontab -l ","date":"2021年8月15日","externalUrl":null,"permalink":"/p/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99%E7%9A%84%E5%BB%BA%E7%AB%8B%E8%BF%87%E7%A8%8B%E4%B8%80%E8%B4%AD%E4%B9%B0%E4%B8%AA%E4%BA%BA%E5%9F%9F%E5%90%8D%E5%B9%B6%E9%85%8D%E7%BD%AE%E5%8A%A8%E6%80%81%E5%9F%9F%E5%90%8D%E8%A7%A3%E6%9E%90/","section":"Posts","summary":"本网站使用我的个人域名，此处记录一下域名的购买和配置过程，包含动态域名解析（DDNS）。\n缘起 # 由于工作原因需要经常使用ssh登录家里的和办公室的个人电脑，但是这些电脑基本都没有固定的IP地址，所以当IP地址发生变动之后，往往不能及时连接到电脑上。\n","title":"个人网站的建立过程（一）：购买个人域名并配置动态域名解析","type":"post"},{"content":"","date":"2019年5月28日","externalUrl":null,"permalink":"/archives/","section":"Pages","summary":"","title":"存档","type":"list"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"锦鲤围棋 锦鲤的在线围棋服务器。\n锦鲤旅行 锦鲤的在线旅行规划服务。\n锦鲤Twitch+ Twitch+是一个基于用户喜好的Twitch直播推荐平台。\n锦鲤云 锦鲤的私有云网盘。\n锦鲤Git 锦鲤的私有GitLab服务器。\n锦鲤图床 锦鲤的私有图床。\n锦鲤邮箱 锦鲤的私有邮箱。\n锦鲤Overleaf 锦鲤的私有LaTeX仓库。\n锦鲤读书 锦鲤的私有电子书库。\n锦鲤影视 锦鲤的私有影视库。\n锦鲤的GitHub主页 锦鲤的GitHub主页。\n","externalUrl":null,"permalink":"/%E9%93%BE%E6%8E%A5/","section":"Pages","summary":"锦鲤围棋 锦鲤的在线围棋服务器。\n锦鲤旅行 锦鲤的在线旅行规划服务。\n锦鲤Twitch+ Twitch+是一个基于用户喜好的Twitch直播推荐平台。\n锦鲤云 锦鲤的私有云网盘。\n锦鲤Git 锦鲤的私有GitLab服务器。\n","title":"链接","type":"page"}]