[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fv0vYMy3v3CEDE-0dXEKFtamd8Gg8W2XdnwSyuATYazc":3,"$fJU-4tot_gC5fDkujNeoE-cGsdMy5V_KcdUXLuAnTFgw":15,"$fTuCHrpxlbQXZrubgtWIVPadAxRGEUDlTrJGLjdxege0":423},{"slug":4,"title":5,"description":6,"content":7,"content_html":8,"pub_date":9,"tags":10,"draft":14},"docker-practical-guide","Docker 实战：从会用到用好","会 docker run 不够，Dockerfile 最佳实践、多阶段构建、Compose 编排、镜像瘦身才是日常真正需要的。","# Docker 实战：从会用到用好\n\n> 很多人会 `docker run`，但 Dockerfile 写得一塌糊涂，镜像动辄 2GB，CI 跑几分钟。这篇聚焦日常真正有用的实践。\n\n## 容器 vs 虚拟机\n\n容器不是\"轻量级虚拟机\"，本质上是**带隔离的进程**。\n\n| 技术 | 隔离机制 | 启动时间 | 开销 |\n|------|---------|---------|------|\n| 容器 | Linux namespace + cgroup | \u003C 1秒 | 极低 |\n| 虚拟机 | Hypervisor + 独立内核 | 数十秒 | 较高 |\n\n- **namespace**：隔离进程、网络、文件系统、用户\n- **cgroup**：限制 CPU、内存、IO 资源使用\n\n容器共享宿主机内核，这是它轻量的原因，也是安全边界比 VM 弱的原因。\n\n## Dockerfile 最佳实践\n\n### 层缓存利用\n\n```dockerfile\n# 糟糕：每次代码变化都重新安装依赖\nFROM node:20-alpine\nWORKDIR \u002Fapp\nCOPY . .\nRUN npm install\nRUN npm run build\n\n# 好：先复制依赖文件，利用缓存\nFROM node:20-alpine\nWORKDIR \u002Fapp\nCOPY package.json package-lock.json .\u002F\nRUN npm ci --only=production\nCOPY . .\nRUN npm run build\n```\n\n**缓存失效规则：** 某层失效，其后所有层都失效。把变化频率低的放前面（依赖 > 配置 > 源代码）。\n\n### .dockerignore\n\n```\nnode_modules\u002F\n.git\u002F\n.env\n*.log\ndist\u002F\ncoverage\u002F\n.DS_Store\n```\n\n减少构建上下文大小，防止 node_modules（数百MB）被发送到 Docker daemon。\n\n### 非 root 用户运行\n\n```dockerfile\nFROM node:20-alpine\nWORKDIR \u002Fapp\n\nCOPY package*.json .\u002F\nRUN npm ci --only=production\n\nRUN addgroup -S appgroup && adduser -S appuser -G appgroup\nCOPY --chown=appuser:appgroup . .\n\nUSER appuser\n\nEXPOSE 3000\nCMD [\"node\", \"server.js\"]\n```\n\n## 多阶段构建\n\n多阶段构建是**缩小镜像体积最有效的方法**。\n\n```dockerfile\n# Go 应用：编译 + 运行分离\nFROM golang:1.22-alpine AS builder\nWORKDIR \u002Fapp\nCOPY go.mod go.sum .\u002F\nRUN go mod download\nCOPY . .\nRUN CGO_ENABLED=0 GOOS=linux go build -o server .\u002Fcmd\u002Fserver\n\nFROM scratch\nCOPY --from=builder \u002Fapp\u002Fserver \u002Fserver\nCOPY --from=builder \u002Fetc\u002Fssl\u002Fcerts\u002Fca-certificates.crt \u002Fetc\u002Fssl\u002Fcerts\u002F\nENTRYPOINT [\"\u002Fserver\"]\n# 最终镜像：~10MB（而不是 ~800MB 的 golang 镜像）\n```\n\n```dockerfile\n# Node.js 前端应用\nFROM node:20-alpine AS builder\nWORKDIR \u002Fapp\nCOPY package*.json .\u002F\nRUN npm ci\nCOPY . .\nRUN npm run build\n\nFROM nginx:alpine\nCOPY --from=builder \u002Fapp\u002Fdist \u002Fusr\u002Fshare\u002Fnginx\u002Fhtml\nCOPY nginx.conf \u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf\nEXPOSE 80\n# 最终镜像：~25MB\n```\n\n## 镜像瘦身技巧\n\n```dockerfile\n# 1. 使用 slim 基础镜像\nFROM python:3.12-slim  # ~130MB vs python:3.12 的 ~900MB\n\n# 2. 合并 RUN 层（减少层数）\n# 不好的写法（3层）\nRUN apt-get update\nRUN apt-get install -y curl\nRUN rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n\n# 好的写法（1层）\nRUN apt-get update && \\\n    apt-get install -y --no-install-recommends curl && \\\n    rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n\n# 3. 不要缓存 pip 包\nRUN pip install -r requirements.txt --no-cache-dir\n\n# 4. 只安装需要的包\nRUN apt-get install -y --no-install-recommends libpq-dev && \\\n    rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n```\n\n## Docker Compose\n\n```yaml\n# docker-compose.yml\nversion: '3.9'\n\nservices:\n  app:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - \"3000:3000\"\n    environment:\n      - NODE_ENV=production\n      - DATABASE_URL=postgresql:\u002F\u002Fuser:pass@db:5432\u002Fmydb\n    depends_on:\n      db:\n        condition: service_healthy\n    volumes:\n      - .\u002Fuploads:\u002Fapp\u002Fuploads\n    restart: unless-stopped\n    deploy:\n      resources:\n        limits:\n          memory: 512M\n          cpus: '0.5'\n\n  db:\n    image: postgres:16-alpine\n    environment:\n      POSTGRES_DB: mydb\n      POSTGRES_USER: user\n      POSTGRES_PASSWORD: pass\n    volumes:\n      - postgres_data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U user -d mydb\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\n  redis:\n    image: redis:7-alpine\n    command: redis-server --appendonly yes\n    volumes:\n      - redis_data:\u002Fdata\n\nvolumes:\n  postgres_data:\n  redis_data:\n```\n\n```bash\ndocker compose up -d           # 后台启动\ndocker compose down            # 停止并删除容器\ndocker compose logs -f app     # 实时查看日志\ndocker compose exec app bash   # 进入容器\ndocker compose build --no-cache\n```\n\n## 网络模式\n\n```bash\n# bridge（默认）：容器间用服务名互相通信\n# 在 compose 中，服务名就是 DNS 名\n# app 访问 db：postgresql:\u002F\u002Fdb:5432\u002Fxxx\n\n# host：容器直接使用宿主机网络\ndocker run --network host nginx\n\n# 自定义网络\ndocker network create mynet\ndocker run --network mynet --name service1 image1\ndocker run --network mynet --name service2 image2\n# service2 可以 ping service1\n```\n\n## 数据持久化\n\n```bash\n# Volume（推荐用于持久数据）\ndocker volume create mydata\ndocker run -v mydata:\u002Fapp\u002Fdata myimage\n# volume 由 Docker 管理，路径在 \u002Fvar\u002Flib\u002Fdocker\u002Fvolumes\u002F\n\n# Bind Mount（推荐用于开发时挂载代码）\ndocker run -v $(pwd)\u002Fsrc:\u002Fapp\u002Fsrc myimage\n\n# 备份 volume\ndocker run --rm -v mydata:\u002Fdata -v $(pwd):\u002Fbackup alpine \\\n    tar czf \u002Fbackup\u002Fmydata-backup.tar.gz -C \u002Fdata .\n\n# 恢复\ndocker run --rm -v mydata:\u002Fdata -v $(pwd):\u002Fbackup alpine \\\n    tar xzf \u002Fbackup\u002Fmydata-backup.tar.gz -C \u002Fdata\n```\n\n## 常见坑\n\n### PID 1 信号处理\n\n```dockerfile\n# shell 形式（sh 是 PID 1，不转发信号给 node）\nCMD node server.js\n\n# exec 形式（node 直接是 PID 1，接收 SIGTERM）\nCMD [\"node\", \"server.js\"]\n\n# 使用 tini（需要僵尸进程回收时）\nFROM node:20-alpine\nRUN apk add --no-cache tini\nENTRYPOINT [\"\u002Fsbin\u002Ftini\", \"--\"]\nCMD [\"node\", \"server.js\"]\n```\n\n### 时区问题\n\n```dockerfile\n# Alpine\nRUN apk add --no-cache tzdata && \\\n    cp \u002Fusr\u002Fshare\u002Fzoneinfo\u002FAsia\u002FShanghai \u002Fetc\u002Flocaltime && \\\n    echo \"Asia\u002FShanghai\" > \u002Fetc\u002Ftimezone && \\\n    apk del tzdata\n\n# 或通过环境变量\nENV TZ=Asia\u002FShanghai\n```\n\n### 编码问题\n\n```dockerfile\nENV LANG=C.UTF-8\nENV LC_ALL=C.UTF-8\n```\n\n## 生产实践\n\n```bash\n# 资源限制\ndocker run \\\n    --memory 512m \\\n    --memory-swap 512m \\\n    --cpus 0.5 \\\n    --pids-limit 100 \\\n    myimage\n\n# 重启策略\ndocker run --restart unless-stopped myimage\n\n# 日志驱动\ndocker run \\\n    --log-driver json-file \\\n    --log-opt max-size=10m \\\n    --log-opt max-file=3 \\\n    myimage\n```\n\n## 调试技巧\n\n```bash\n# 进入运行中的容器\ndocker exec -it \u003Ccontainer_id> bash\n\n# 查看容器详细信息\ndocker inspect \u003Ccontainer_id>\ndocker inspect \u003Ccontainer_id> | jq '.[0].NetworkSettings'\n\n# 查看日志\ndocker logs -f --tail 100 \u003Ccontainer_id>\n\n# 资源使用\ndocker stats --no-stream\n\n# 事件监听\ndocker events --filter type=container\n```\n\n## 与 GitHub Actions 集成\n\n```yaml\n# .github\u002Fworkflows\u002Fdocker.yml\nname: Build and Push\n\non:\n  push:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      \n      - name: Set up Docker Buildx\n        uses: docker\u002Fsetup-buildx-action@v3\n      \n      - name: Login to Docker Hub\n        uses: docker\u002Flogin-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      \n      - name: Build and push\n        uses: docker\u002Fbuild-push-action@v5\n        with:\n          context: .\n          push: true\n          tags: myuser\u002Fmyapp:${{ github.sha }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n```\n","\u003Ch1>Docker 实战：从会用到用好\u003C\u002Fh1>\n\u003Cblockquote>\n\u003Cp>很多人会 \u003Ccode>docker run\u003C\u002Fcode>，但 Dockerfile 写得一塌糊涂，镜像动辄 2GB，CI 跑几分钟。这篇聚焦日常真正有用的实践。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"容器-vs-虚拟机\">容器 vs 虚拟机\u003C\u002Fh2>\n\u003Cp>容器不是&quot;轻量级虚拟机&quot;，本质上是\u003Cstrong>带隔离的进程\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>技术\u003C\u002Fth>\n\u003Cth>隔离机制\u003C\u002Fth>\n\u003Cth>启动时间\u003C\u002Fth>\n\u003Cth>开销\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>容器\u003C\u002Ftd>\n\u003Ctd>Linux namespace + cgroup\u003C\u002Ftd>\n\u003Ctd>&lt; 1秒\u003C\u002Ftd>\n\u003Ctd>极低\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>虚拟机\u003C\u002Ftd>\n\u003Ctd>Hypervisor + 独立内核\u003C\u002Ftd>\n\u003Ctd>数十秒\u003C\u002Ftd>\n\u003Ctd>较高\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cul>\n\u003Cli>\u003Cstrong>namespace\u003C\u002Fstrong>：隔离进程、网络、文件系统、用户\u003C\u002Fli>\n\u003Cli>\u003Cstrong>cgroup\u003C\u002Fstrong>：限制 CPU、内存、IO 资源使用\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>容器共享宿主机内核，这是它轻量的原因，也是安全边界比 VM 弱的原因。\u003C\u002Fp>\n\u003Ch2 id=\"dockerfile-最佳实践\">Dockerfile 最佳实践\u003C\u002Fh2>\n\u003Ch3 id=\"层缓存利用\">层缓存利用\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-dockerfile\"># 糟糕：每次代码变化都重新安装依赖\nFROM node:20-alpine\nWORKDIR \u002Fapp\nCOPY . .\nRUN npm install\nRUN npm run build\n\n# 好：先复制依赖文件，利用缓存\nFROM node:20-alpine\nWORKDIR \u002Fapp\nCOPY package.json package-lock.json .\u002F\nRUN npm ci --only=production\nCOPY . .\nRUN npm run build\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>缓存失效规则：\u003C\u002Fstrong> 某层失效，其后所有层都失效。把变化频率低的放前面（依赖 &gt; 配置 &gt; 源代码）。\u003C\u002Fp>\n\u003Ch3 id=\"dockerignore\">.dockerignore\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>node_modules\u002F\n.git\u002F\n.env\n*.log\ndist\u002F\ncoverage\u002F\n.DS_Store\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>减少构建上下文大小，防止 node_modules（数百MB）被发送到 Docker daemon。\u003C\u002Fp>\n\u003Ch3 id=\"非-root-用户运行\">非 root 用户运行\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-dockerfile\">FROM node:20-alpine\nWORKDIR \u002Fapp\n\nCOPY package*.json .\u002F\nRUN npm ci --only=production\n\nRUN addgroup -S appgroup &amp;&amp; adduser -S appuser -G appgroup\nCOPY --chown=appuser:appgroup . .\n\nUSER appuser\n\nEXPOSE 3000\nCMD [&quot;node&quot;, &quot;server.js&quot;]\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"多阶段构建\">多阶段构建\u003C\u002Fh2>\n\u003Cp>多阶段构建是\u003Cstrong>缩小镜像体积最有效的方法\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-dockerfile\"># Go 应用：编译 + 运行分离\nFROM golang:1.22-alpine AS builder\nWORKDIR \u002Fapp\nCOPY go.mod go.sum .\u002F\nRUN go mod download\nCOPY . .\nRUN CGO_ENABLED=0 GOOS=linux go build -o server .\u002Fcmd\u002Fserver\n\nFROM scratch\nCOPY --from=builder \u002Fapp\u002Fserver \u002Fserver\nCOPY --from=builder \u002Fetc\u002Fssl\u002Fcerts\u002Fca-certificates.crt \u002Fetc\u002Fssl\u002Fcerts\u002F\nENTRYPOINT [&quot;\u002Fserver&quot;]\n# 最终镜像：~10MB（而不是 ~800MB 的 golang 镜像）\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre>\u003Ccode class=\"language-dockerfile\"># Node.js 前端应用\nFROM node:20-alpine AS builder\nWORKDIR \u002Fapp\nCOPY package*.json .\u002F\nRUN npm ci\nCOPY . .\nRUN npm run build\n\nFROM nginx:alpine\nCOPY --from=builder \u002Fapp\u002Fdist \u002Fusr\u002Fshare\u002Fnginx\u002Fhtml\nCOPY nginx.conf \u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf\nEXPOSE 80\n# 最终镜像：~25MB\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"镜像瘦身技巧\">镜像瘦身技巧\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-dockerfile\"># 1. 使用 slim 基础镜像\nFROM python:3.12-slim  # ~130MB vs python:3.12 的 ~900MB\n\n# 2. 合并 RUN 层（减少层数）\n# 不好的写法（3层）\nRUN apt-get update\nRUN apt-get install -y curl\nRUN rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n\n# 好的写法（1层）\nRUN apt-get update &amp;&amp; \\\n    apt-get install -y --no-install-recommends curl &amp;&amp; \\\n    rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n\n# 3. 不要缓存 pip 包\nRUN pip install -r requirements.txt --no-cache-dir\n\n# 4. 只安装需要的包\nRUN apt-get install -y --no-install-recommends libpq-dev &amp;&amp; \\\n    rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"docker-compose\">Docker Compose\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-yaml\"># docker-compose.yml\nversion: '3.9'\n\nservices:\n  app:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - &quot;3000:3000&quot;\n    environment:\n      - NODE_ENV=production\n      - DATABASE_URL=postgresql:\u002F\u002Fuser:pass@db:5432\u002Fmydb\n    depends_on:\n      db:\n        condition: service_healthy\n    volumes:\n      - .\u002Fuploads:\u002Fapp\u002Fuploads\n    restart: unless-stopped\n    deploy:\n      resources:\n        limits:\n          memory: 512M\n          cpus: '0.5'\n\n  db:\n    image: postgres:16-alpine\n    environment:\n      POSTGRES_DB: mydb\n      POSTGRES_USER: user\n      POSTGRES_PASSWORD: pass\n    volumes:\n      - postgres_data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n    healthcheck:\n      test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U user -d mydb&quot;]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\n  redis:\n    image: redis:7-alpine\n    command: redis-server --appendonly yes\n    volumes:\n      - redis_data:\u002Fdata\n\nvolumes:\n  postgres_data:\n  redis_data:\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre>\u003Ccode class=\"language-bash\">docker compose up -d           # 后台启动\ndocker compose down            # 停止并删除容器\ndocker compose logs -f app     # 实时查看日志\ndocker compose exec app bash   # 进入容器\ndocker compose build --no-cache\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"网络模式\">网络模式\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-bash\"># bridge（默认）：容器间用服务名互相通信\n# 在 compose 中，服务名就是 DNS 名\n# app 访问 db：postgresql:\u002F\u002Fdb:5432\u002Fxxx\n\n# host：容器直接使用宿主机网络\ndocker run --network host nginx\n\n# 自定义网络\ndocker network create mynet\ndocker run --network mynet --name service1 image1\ndocker run --network mynet --name service2 image2\n# service2 可以 ping service1\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"数据持久化\">数据持久化\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-bash\"># Volume（推荐用于持久数据）\ndocker volume create mydata\ndocker run -v mydata:\u002Fapp\u002Fdata myimage\n# volume 由 Docker 管理，路径在 \u002Fvar\u002Flib\u002Fdocker\u002Fvolumes\u002F\n\n# Bind Mount（推荐用于开发时挂载代码）\ndocker run -v $(pwd)\u002Fsrc:\u002Fapp\u002Fsrc myimage\n\n# 备份 volume\ndocker run --rm -v mydata:\u002Fdata -v $(pwd):\u002Fbackup alpine \\\n    tar czf \u002Fbackup\u002Fmydata-backup.tar.gz -C \u002Fdata .\n\n# 恢复\ndocker run --rm -v mydata:\u002Fdata -v $(pwd):\u002Fbackup alpine \\\n    tar xzf \u002Fbackup\u002Fmydata-backup.tar.gz -C \u002Fdata\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"常见坑\">常见坑\u003C\u002Fh2>\n\u003Ch3 id=\"pid-1-信号处理\">PID 1 信号处理\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-dockerfile\"># shell 形式（sh 是 PID 1，不转发信号给 node）\nCMD node server.js\n\n# exec 形式（node 直接是 PID 1，接收 SIGTERM）\nCMD [&quot;node&quot;, &quot;server.js&quot;]\n\n# 使用 tini（需要僵尸进程回收时）\nFROM node:20-alpine\nRUN apk add --no-cache tini\nENTRYPOINT [&quot;\u002Fsbin\u002Ftini&quot;, &quot;--&quot;]\nCMD [&quot;node&quot;, &quot;server.js&quot;]\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"时区问题\">时区问题\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-dockerfile\"># Alpine\nRUN apk add --no-cache tzdata &amp;&amp; \\\n    cp \u002Fusr\u002Fshare\u002Fzoneinfo\u002FAsia\u002FShanghai \u002Fetc\u002Flocaltime &amp;&amp; \\\n    echo &quot;Asia\u002FShanghai&quot; &gt; \u002Fetc\u002Ftimezone &amp;&amp; \\\n    apk del tzdata\n\n# 或通过环境变量\nENV TZ=Asia\u002FShanghai\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"编码问题\">编码问题\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-dockerfile\">ENV LANG=C.UTF-8\nENV LC_ALL=C.UTF-8\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"生产实践\">生产实践\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-bash\"># 资源限制\ndocker run \\\n    --memory 512m \\\n    --memory-swap 512m \\\n    --cpus 0.5 \\\n    --pids-limit 100 \\\n    myimage\n\n# 重启策略\ndocker run --restart unless-stopped myimage\n\n# 日志驱动\ndocker run \\\n    --log-driver json-file \\\n    --log-opt max-size=10m \\\n    --log-opt max-file=3 \\\n    myimage\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"调试技巧\">调试技巧\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-bash\"># 进入运行中的容器\ndocker exec -it &lt;container_id&gt; bash\n\n# 查看容器详细信息\ndocker inspect &lt;container_id&gt;\ndocker inspect &lt;container_id&gt; | jq '.[0].NetworkSettings'\n\n# 查看日志\ndocker logs -f --tail 100 &lt;container_id&gt;\n\n# 资源使用\ndocker stats --no-stream\n\n# 事件监听\ndocker events --filter type=container\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"与-github-actions-集成\">与 GitHub Actions 集成\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-yaml\"># .github\u002Fworkflows\u002Fdocker.yml\nname: Build and Push\n\non:\n  push:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      \n      - name: Set up Docker Buildx\n        uses: docker\u002Fsetup-buildx-action@v3\n      \n      - name: Login to Docker Hub\n        uses: docker\u002Flogin-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      \n      - name: Build and push\n        uses: docker\u002Fbuild-push-action@v5\n        with:\n          context: .\n          push: true\n          tags: myuser\u002Fmyapp:${{ github.sha }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n\u003C\u002Fcode>\u003C\u002Fpre>\n","2026-05-03",[11,12,13],"docker","linux","工程实践",false,[16,29,39,51,59,66,73,76,83,90,100,109,119,128,136,144,153,162,171,181,188,198,204,211,217,226,233,240,248,258,267,276,286,296,306,314,324,335,345,354,362,368,376,384,392,400,408,415],{"slug":17,"title":18,"description":19,"pub_date":20,"tags":21,"draft":14,"word_count":28},"ide-skills-guide","Agent Skills 完全指南：21 款第三方 Skill 深度评测与使用心得","全面评测 21 款第三方 Agent Skills，涵盖 Vue 生态、前端设计、构建工具、实用工具四大分类。从安装配置到实际使用场景，带你了解每个 Skill 的功能特点、最佳实践与使用心得。","2026-06-15",[22,23,24,25,26,27],"agent","skills","AI","效率工具","前端","Vue",4169,{"slug":30,"title":31,"description":32,"pub_date":33,"tags":34,"draft":14,"word_count":38},"linux-kernel-skeleton-struct-funcptr-container_of","Linux 内核骨架：struct、函数指针与 container_of","读懂 Linux 内核源码的三件套：巨大的 struct 组合代替继承、函数指针表实现虚派发、container_of 宏从嵌入成员找回完整对象。","2026-05-09",[12,35,36,37],"kernel","C","container_of",1369,{"slug":40,"title":41,"description":42,"pub_date":43,"tags":44,"draft":14,"word_count":50},"astro-complete-guide-2025","Astro 5 深度剖析：Islands 架构原理、构建优化与 Cloudflare Workers 边缘部署","从编译器视角解析 Astro 5 的 Islands 架构实现原理，Content Layer API 的 Vite 插件机制，Server Islands 的流式渲染，以及如何在 Cloudflare Workers + D1 边缘环境下榨干性能。","2026-05-08",[45,46,47,48,49],"astro","frontend","cloudflare","performance","architecture",3663,{"slug":52,"title":53,"description":54,"pub_date":9,"tags":55,"draft":14,"word_count":58},"llm-prompt-engineering","Prompt Engineering 实战：让 LLM 真正听话的技巧","System prompt 怎么写、Few-shot 怎么设计、Chain-of-Thought 原理，以及常见失败模式和调试方法。",[56,57,13],"ai","llm",1723,{"slug":60,"title":61,"description":62,"pub_date":9,"tags":63,"draft":14,"word_count":65},"rag-system-design","RAG 系统设计：从 naive 到 production-ready","Retrieval-Augmented Generation 不只是「向量数据库 + LLM」，分块策略、召回质量、重排序、缓存才是工程核心。",[56,64,57,13],"rag",1613,{"slug":67,"title":68,"description":69,"pub_date":9,"tags":70,"draft":14,"word_count":72},"git-advanced-workflow","Git 进阶工作流：rebase、cherry-pick、bisect 的正确使用","merge 会了，但 rebase 总搞错？bisect 找 bug 提交？interactive rebase 整理历史？这篇一次说清楚。",[71,13],"git",1396,{"slug":4,"title":5,"description":6,"pub_date":9,"tags":74,"draft":14,"word_count":75},[11,12,13],1268,{"slug":77,"title":78,"description":79,"pub_date":9,"tags":80,"draft":14,"word_count":82},"anthropics-skills-guide","anthropics\u002Fskills：Anthropic 官方 Agent Skills 仓库解析","Anthropic 官方开源的 Agent Skills 标准仓库，127k stars，解析 SKILL.md 规范、17 个示例 skill 的设计模式，以及如何在 Claude Code \u002F Claude.ai \u002F API 中使用",[56,81,22,23],"Claude",2090,{"slug":84,"title":85,"description":86,"pub_date":9,"tags":87,"draft":14,"word_count":89},"karpathy-claude-code-guidelines","Karpathy 的 LLM 编码批评与 CLAUDE.md 最佳实践","基于 Andrej Karpathy 对 LLM 编程助手的观察，forrestchang 提炼出一个 CLAUDE.md 文件，4 条原则解决 AI 编码的典型失控问题：乱猜假设、过度设计、乱改代码、目标不清",[56,81,88,13],"Claude Code",2699,{"slug":91,"title":92,"description":93,"pub_date":9,"tags":94,"draft":14,"word_count":99},"typescript-advanced-patterns","TypeScript 高级模式：让类型系统为你工作","基础 TS 会了但类型总是 any？条件类型、映射类型、模板字面量类型、infer 关键字才是 TS 的真正威力。",[95,96,97,98],"typescript","类型系统","前端工程","高级模式",1419,{"slug":101,"title":102,"description":103,"pub_date":9,"tags":104,"draft":14,"word_count":108},"linux-performance-tuning","Linux 性能调优实战：从 top 到 perf 的完整工具链","遇到性能问题不知道从哪下手？这篇建立系统化的排查思路，从 CPU\u002F内存\u002FIO\u002F网络逐层分析。",[12,105,106,107],"性能","运维","系统编程",1524,{"slug":110,"title":111,"description":112,"pub_date":9,"tags":113,"draft":14,"word_count":118},"python-functional-programming","Python 函数式编程：map\u002Ffilter\u002Freduce 之外","Python 不是纯函数式语言，但 functools、itertools、偏函数、闭包这些工具用好了能让代码简洁一个量级。",[114,115,116,117],"python","函数式","闭包","装饰器",1867,{"slug":120,"title":121,"description":122,"pub_date":9,"tags":123,"draft":14,"word_count":127},"python-oop-guide","Python 面向对象：__init__ 之外你需要知道的","Python OOP 不只是 class + __init__，魔术方法、描述符、元类才是真正的武器。",[114,124,125,126],"OOP","面向对象","魔术方法",1792,{"slug":129,"title":130,"description":131,"pub_date":9,"tags":132,"draft":14,"word_count":135},"python-data-structures","Python 内置数据结构深度解析","list、dict、set、tuple 不只是数据容器，搞懂它们的底层实现和时间复杂度，才能写出高性能 Python。",[114,133,105,134],"数据结构","算法",1517,{"slug":137,"title":138,"description":139,"pub_date":9,"tags":140,"draft":14,"word_count":143},"python-basics-quick-start","Python 快速上手：写给有编程基础的人","已经会其他语言，想快速掌握 Python 的语法特性和思维方式，这篇是捷径。",[114,141,142],"入门","基础",1607,{"slug":145,"title":146,"description":147,"pub_date":9,"tags":148,"draft":14,"word_count":152},"python-dataclass-pydantic","Python dataclass vs Pydantic：数据类选型指南","dataclass 是标准库的轻量选择，Pydantic v2 是带验证的重武器，什么时候用哪个，这篇说清楚。",[114,149,150,151],"dataclass","pydantic","数据验证",1323,{"slug":154,"title":155,"description":156,"pub_date":9,"tags":157,"draft":14,"word_count":161},"python-asyncio-practical","Python asyncio 实战：从回调地狱到协程优雅","asyncio 是 Python 异步编程的核心，搞懂 event loop、Task、gather 这些概念才能写出真正高效的异步代码。",[114,158,159,160],"asyncio","并发","网络编程",1258,{"slug":163,"title":164,"description":165,"pub_date":9,"tags":166,"draft":14,"word_count":170},"python-type-hints-guide","Python 类型注解完全指南：从入门到实践","Python 3.5+ 引入类型注解，配合 mypy\u002Fpyright 让 Python 也能享受静态类型检查的好处。",[114,167,168,169],"typescript-style","type-hints","工具链",1102,{"slug":172,"title":173,"description":174,"pub_date":175,"tags":176,"draft":14,"word_count":180},"pwa-install-update-button","PWA 踩坑：为什么安装按钮从来不出现","从 beforeinstallprompt 到 Service Worker waiting，把 PWA 的安装与更新提示真正做对","2026-05-02",[177,178,179],"pwa","javascript","web",1683,{"slug":182,"title":183,"description":184,"pub_date":185,"tags":186,"draft":14,"word_count":187},"openclaw-vs-hermes-agent","OpenClaw vs Hermes Agent：两个本地优先 Agent 的设计差异","OpenClaw（Novita AI）和 Hermes Agent（Nous Research）都是本地运行的个人 AI Agent，但在记忆系统、技能学习、运行环境和模型生态上走了不同的路。深入对比两种架构的核心差异。","2026-05-01",[56,22,57],1679,{"slug":189,"title":190,"description":191,"pub_date":185,"tags":192,"draft":14,"word_count":197},"cpp-random-design-patterns","C++ 设计模式实战：RAII、观察者、工厂","用现代 C++（C++17\u002F20）实现三种高频设计模式：RAII 资源管理、观察者模式事件系统、工厂模式插件架构。每种模式给出问题场景、实现代码和真实工程案例。",[193,194,195,196],"cpp","设计模式","c++17","工程",2613,{"slug":199,"title":200,"description":201,"pub_date":185,"tags":202,"draft":14,"word_count":203},"data-structures-fundamentals","数据结构基础：从数组到红黑树","系统梳理常用数据结构的核心原理、时间复杂度和适用场景。数组、链表、栈、队列、哈希表、二叉树、堆、图，每种结构附实现要点和 C++ 代码片段。",[133,134,193,142],3004,{"slug":205,"title":206,"description":207,"pub_date":208,"tags":209,"draft":14,"word_count":210},"ai-agent-what-is","什么是 AI Agent？从 LLM 到自主执行","LLM 本身是无状态问答机，Agent 是什么让它’动’起来的？本文深入解析 Agent 的四个核心能力、ReAct 框架、工具调用原理，以及主流框架横向对比。","2026-04-30",[56,22,57],2116,{"slug":212,"title":213,"description":214,"pub_date":208,"tags":215,"draft":14,"word_count":216},"ai-agent-memory","AI Agent 的记忆系统：从上下文窗口到长期记忆","深入拆解 AI Agent 的四种记忆类型、上下文窗口压缩策略、RAG 向量检索原理，以及三种典型失败模式和工程选型建议。",[56,22,64],2052,{"slug":218,"title":219,"description":220,"pub_date":208,"tags":221,"draft":14,"word_count":225},"network-proxy-vpn-guide","代理与翻墙技术原理：从 HTTP 代理到现代协议","深入解析代理与 VPN 的本质区别，梳理从 SOCKS5 到 Shadowsocks、V2Ray\u002FXray、Hysteria2 的协议演进，以及机场订阅的技术本质。",[222,223,224],"网络","代理","协议",2148,{"slug":227,"title":228,"description":229,"pub_date":208,"tags":230,"draft":14,"word_count":143},"algorithm-binary-search","二分查找：永远写不对？记住这个模板","彻底搞清楚二分查找的边界问题：闭区间和左闭右开两套模板、三道经典 LeetCode 题目完整 C++ 实现，以及二分答案的进阶思路。",[134,231,232,193],"二分查找","leetcode",{"slug":234,"title":235,"description":236,"pub_date":208,"tags":237,"draft":14,"word_count":239},"algorithm-sliding-window","滑动窗口算法：从暴力到 O(n) 的思维跃迁","系统讲解滑动窗口算法的核心模板、适用题型，配合三道经典 LeetCode 题目的完整 C++ 实现，彻底理解双指针收缩思路。",[134,238,232,193],"滑动窗口",1943,{"slug":241,"title":242,"description":243,"pub_date":208,"tags":244,"draft":14,"word_count":247},"network-clash-config","Clash \u002F Mihomo 配置详解：规则、策略组与分流","深入解析 Clash\u002FMihomo 的核心配置结构，包括代理节点、策略组类型、规则优先级、DNS fake-ip 模式，以及一份实用的完整配置模板。",[222,245,223,246],"clash","配置",1292,{"slug":249,"title":250,"description":251,"pub_date":252,"tags":253,"draft":14,"word_count":257},"hid-hotplug","HID 设备热插拔检测：从 udev 到 node-hid","在 Linux 上用 node-hid + usb 库实现可靠的 USB HID 设备热插拔检测，踩坑记录","2026-04-28",[193,254,12,255,256],"hid","nodejs","electron",2039,{"slug":259,"title":260,"description":261,"pub_date":262,"tags":263,"draft":14,"word_count":266},"electron-ipc-types","Electron IPC 类型安全：从 any 到完全类型化","用 TypeScript 泛型封装 Electron IPC，彻底消灭 any，preload 契约集中管理","2026-04-25",[256,95,264,265],"ipc","vue",1446,{"slug":268,"title":269,"description":270,"pub_date":271,"tags":272,"draft":14,"word_count":275},"element-plus-popover-hide","手动关闭多个 el-popover（不用 v-model:visible）","通过 ref + Reflect.get 调用 hide() 方法手动关闭 Element Plus Popover，解释 Vue3 Proxy 导致无法直接调用实例方法的原因。","2024-10-25",[265,273,274],"element-plus","vue3",1321,{"slug":277,"title":278,"description":279,"pub_date":280,"tags":281,"draft":14,"word_count":285},"vite-vue3-ts-elementplus-pinia","用 Vite+（vp）从零搭建 Vue3 + TypeScript + Element Plus + Pinia + Vue Router","使用 Vite+ 统一工具链（vp）一条命令搭建 Vue3 全家桶，涵盖按需导入、Pinia store、路由配置，以及常见坑的解决方案。","2024-08-27",[265,282,95,273,283,284],"vite","pinia","vite-plus",1960,{"slug":287,"title":288,"description":289,"pub_date":290,"tags":291,"draft":14,"word_count":295},"cef-lnk2038-iterator-debug-level","CEF LNK2038：解决 _ITERATOR_DEBUG_LEVEL 不匹配错误","分析 CEF（Chromium Embedded Framework）集成时出现的 LNK2038 _ITERATOR_DEBUG_LEVEL 链接错误，从根本原因到解决方案的完整指南。","2024-05-07",[193,292,293,294],"CEF","Visual Studio","链接错误",1509,{"slug":297,"title":298,"description":299,"pub_date":300,"tags":301,"draft":14,"word_count":305},"npm-electron-install-fix","彻底解决 npm 安装 Electron 失败的问题","分析 npm install electron 失败的根本原因（下载二进制超时\u002F被墙），通过国内镜像（npmmirror）彻底解决，并介绍多种备选方案和常见错误排查。","2024-03-01",[256,302,303,304],"npm","前端工具链","国内镜像",1494,{"slug":307,"title":308,"description":309,"pub_date":310,"tags":311,"draft":14,"word_count":313},"git-out-of-memory","解决 git 报错：Fatal: Out of memory, malloc failed","分析 git 大仓库操作时出现 Out of memory malloc failed 的根本原因，通过调整 pack.windowMemory、http.postBuffer 和 git repack 彻底解决。","2024-01-31",[71,12,312],"工具",2244,{"slug":315,"title":316,"description":317,"pub_date":318,"tags":319,"draft":14,"word_count":323},"vmware-tools-install","在 VMware 虚拟机中安装 open-vm-tools 完整指南","详解 VMware Tools 的作用、open-vm-tools 与官方 VMware Tools 的区别，以及在 Ubuntu 虚拟机中安装并生效的完整步骤和常见问题排查。","2023-11-21",[320,12,321,322],"VMware","Ubuntu","虚拟机",2523,{"slug":325,"title":326,"description":327,"pub_date":328,"tags":329,"draft":14,"word_count":334},"load-balancing-algorithms","负载均衡算法完全指南：从轮询到一致性哈希","系统梳理静态与动态负载均衡算法，涵盖轮询、随机、权重、IP Hash、一致性 Hash、最少连接、最快响应等，并对比 Nginx、Dubbo、Spring Cloud LoadBalancer 的实现差异。","2023-11-15",[330,331,332,333],"分布式","负载均衡","Nginx","微服务",1764,{"slug":336,"title":337,"description":338,"pub_date":339,"tags":340,"draft":14,"word_count":344},"win-cw2a-ca2w","ATL 字符串转换：CW2A 与 CA2W 完全指南","详解 ATL 宏 CW2A\u002FCA2W 在 Unicode 与 ANSI 之间的字符串转换用法、头文件依赖、USES_CONVERSION 宏的作用与常见陷阱。","2023-06-09",[193,341,342,343],"windows","ATL","字符串",1665,{"slug":346,"title":347,"description":348,"pub_date":339,"tags":349,"draft":14,"word_count":353},"csharp-sendmessage-cpp","C# 通过 SendMessage 向 C++ 窗口发送消息与字符串","使用 P\u002FInvoke 调用 user32.dll 的 SendMessage，从 C# 发送自定义 WM_USER 消息及字符串指针给 C++ 原生窗口，并在 C++ 侧正确接收和转换。",[350,193,341,351,352],"C#","互操作","PInvoke",1554,{"slug":355,"title":356,"description":357,"pub_date":358,"tags":359,"draft":14,"word_count":361},"win-postmessage-vector","Windows PostMessage 跨线程传递 std::vector 指针","通过 PostMessage 在 Windows 消息队列中传递 std::vector 指针，使用 reinterpret_cast 将指针装入 LPARAM，并在接收方正确释放内存。","2023-05-26",[193,341,360],"WinAPI",1823,{"slug":363,"title":364,"description":365,"pub_date":358,"tags":366,"draft":14,"word_count":367},"exe-dll-single-package","将 EXE 和 DLL 打包成单一可执行文件","介绍两种将 exe 和依赖 dll 打包成单文件的方案：Enigma Virtual Box 和 WinRAR 自解压，适合发布 Windows 桌面程序时简化分发流程。",[341,193,312],1619,{"slug":369,"title":370,"description":371,"pub_date":358,"tags":372,"draft":14,"word_count":375},"cpp-random-mt19937","C++ 现代随机数生成：用 mt19937 彻底告别 rand()","深入讲解为什么 rand() 不够用，以及如何用 C++11 的 \u003Crandom> 库正确生成高质量随机数，涵盖 mt19937、各种分布和线程安全。",[193,373,374],"c++11","random",1549,{"slug":377,"title":378,"description":379,"pub_date":380,"tags":381,"draft":14,"word_count":383},"win-startup-registry","C++ 实现程序开机自启动：注册表方式详解","通过操作 Windows 注册表 Run 键实现程序开机自启动，包括 HKCU 与 HKLM 区别、完整封装代码、工作目录问题和 UAC 权限处理。","2022-12-26",[341,193,382],"registry",1201,{"slug":385,"title":386,"description":387,"pub_date":388,"tags":389,"draft":14,"word_count":391},"mfc-cstring-wparam","MFC 中 CString 与 WPARAM 之间的转换","详解 MFC 消息传递中 CString 无法直接强转为 WPARAM 的原因，以及两种正确的转换方案，并介绍结构体指针传递的正确姿势。","2022-11-25",[390,193,341],"mfc",1546,{"slug":393,"title":394,"description":395,"pub_date":396,"tags":397,"draft":14,"word_count":399},"duilib-static-build","正确编译 Duilib 静态库：避免 ATL 依赖和链接错误","详解如何用 DuiLib_Static.vcxproj 编译 Duilib 静态库，解决 VARIANT 未定义、Unicode 配置不匹配和 ATL 依赖等常见问题。","2022-08-24",[193,398,341,390],"duilib",2639,{"slug":401,"title":402,"description":403,"pub_date":404,"tags":405,"draft":14,"word_count":407},"mfc-dpi-adaptive","MFC 界面自适应不同分辨率","MFC 对话框程序实现控件和字体随分辨率自动缩放的完整方案，附 DPI Awareness 配置说明","2022-08-17",[390,193,341,406],"dpi",1414,{"slug":409,"title":410,"description":411,"pub_date":412,"tags":413,"draft":14,"word_count":414},"mfc-drag-window","MFC 无标题栏窗口客户区拖动：三种方法对比","MFC 对话框去掉标题栏后如何实现拖动移动窗口，三种方案完整实现与适用场景分析","2022-08-16",[390,193,341],1633,{"slug":416,"title":417,"description":418,"pub_date":419,"tags":420,"draft":14,"word_count":422},"algorithm-number-complement","整数的补数：位运算掩码解法","LeetCode 476 题，用掩码 XOR 实现整数补数，附 C++\u002FPython\u002FJava 三种实现及补数与补码的区别","2021-03-08",[134,421,232],"位运算",1374,[]]