强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 14 章:Docker 开发

14.1 Docker 化 LSP Server 的动机

在以下场景中,将 LSP Server 运行在 Docker 容器内非常有价值:

场景说明
一致的开发环境团队成员使用相同版本的语言工具链
复杂依赖项目依赖特定系统库或工具
安全隔离不信任的代码分析在沙箱中运行
远程开发Server 在远程服务器/容器中运行
CI/CD 集成在流水线中运行 LSP 检查
┌─────────────────┐     ┌──────────────────────────┐
│   编辑器         │     │   Docker 容器             │
│                 │     │                          │
│  LSP Client ────┼─TCP─┼──▶ LSP Server            │
│                 │     │     ├─ Node.js / Python   │
│                 │     │     ├─ 项目代码 (挂载)     │
│                 │     │     └─ 依赖 (镜像内置)     │
└─────────────────┘     └──────────────────────────┘

14.2 Dockerfile 编写

14.2.1 TypeScript LSP Server

# Dockerfile
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src/ src/
RUN npm run build

FROM node:20-alpine AS runtime

WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./

# 暴露端口(TCP 模式)
EXPOSE 2087

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
  CMD echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"processId":null,"rootUri":"file:///","capabilities":{}}}' | \
    node -e "const net=require('net');const s=net.connect(2087,'localhost',()=>{s.write(process.argv[1]);s.end()})" || exit 1

CMD ["node", "dist/server.js", "--stdio"]

14.2.2 Python LSP Server

FROM python:3.11-slim

WORKDIR /app

# 安装系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 2087

CMD ["python", "server.py", "--tcp", "--port", "2087"]

14.2.3 Go LSP Server(多阶段构建)

FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o lsp-server .

FROM alpine:3.18

RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /app/lsp-server .

EXPOSE 2087

CMD ["./lsp-server", "--tcp", ":2087"]

14.3 Docker Compose 配置

# docker-compose.yml
version: "3.8"

services:
  lsp-server:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "2087:2087"
    volumes:
      - ./workspace:/workspace  # 挂载项目目录
      - lsp-cache:/cache
    environment:
      - LSP_LOG_LEVEL=info
      - LSP_WORKSPACE=/workspace
      - LSP_CACHE_DIR=/cache
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "nc", "-z", "localhost", "2087"]
      interval: 30s
      timeout: 5s
      retries: 3

volumes:
  lsp-cache:

14.4 编辑器连接 Docker 内 LSP

14.4.1 VS Code Remote Containers

.devcontainer/devcontainer.json

{
  "name": "My LSP Dev Environment",
  "dockerComposeFile": "../docker-compose.yml",
  "service": "lsp-server",
  "workspaceFolder": "/workspace",
  "customizations": {
    "vscode": {
      "extensions": [
        "my-lsp-extension"
      ],
      "settings": {
        "myLsp.serverAddress": "localhost:2087"
      }
    }
  },
  "forwardPorts": [2087],
  "postCreateCommand": "npm install",
  "remoteUser": "root"
}

14.4.2 VS Code 远程 TCP 连接

// settings.json
{
  "myLsp.serverMode": "tcp",
  "myLsp.serverHost": "localhost",
  "myLsp.serverPort": 2087
}
// Client 扩展连接 TCP Server
import * as net from "net";
import { LanguageClient, StreamInfo } from "vscode-languageclient/node";

const serverOptions = (): Promise<StreamInfo> => {
  return new Promise((resolve, reject) => {
    const socket = net.connect(2087, "localhost", () => {
      resolve({
        reader: socket,
        writer: socket,
      });
    });
    socket.on("error", reject);
  });
};

const client = new LanguageClient("myLsp", "My LSP", serverOptions, clientOptions);
client.start();

14.4.3 Neovim 连接 Docker Server

-- 注册远程 LSP Server
local lspconfig = require("lspconfig")
local configs = require("lspconfig.configs")

if not configs.docker_lsp then
  configs.docker_lsp = {
    default_config = {
      cmd = { "nc", "localhost", "2087" },  -- netcat 作为 TCP 客户端
      filetypes = { "python", "javascript" },
      root_dir = lspconfig.util.root_pattern(".git"),
    },
  }
end

lspconfig.docker_lsp.setup({
  on_attach = function(client, bufnr)
    -- 标准快捷键映射
  end,
})

14.5 开发工作流

14.5.1 开发模式(热重载)

# docker-compose.dev.yml
version: "3.8"

services:
  lsp-server-dev:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "2087:2087"
      - "9229:9229"  # Node.js 调试端口
    volumes:
      - ./src:/app/src  # 挂载源码,支持热重载
      - ./workspace:/workspace
    environment:
      - NODE_ENV=development
    command: ["npx", "nodemon", "--inspect=0.0.0.0:9229", "dist/server.js", "--stdio"]
# Dockerfile.dev
FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./

# nodemon 用于热重载
RUN npm install -g nodemon

CMD ["sh", "-c", "npm run build && nodemon --watch src --exec 'npm run build && node dist/server.js' --stdio"]

14.5.2 调试配置

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach to Docker LSP",
      "type": "node",
      "request": "attach",
      "port": 9229,
      "host": "localhost",
      "restart": true,
      "sourceMaps": true,
      "outFiles": ["${workspaceFolder}/dist/**/*.js"],
      "localRoot": "${workspaceFolder}",
      "remoteRoot": "/app"
    }
  ]
}

14.6 CI/CD 集成

14.6.1 在 CI 中运行 LSP 检查

# .github/workflows/lsp-check.yml
name: LSP Analysis

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lsp-analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build LSP Server
        run: docker build -t my-lsp-server .

      - name: Run LSP Analysis
        run: |
          # 启动 LSP Server
          docker run -d --name lsp \
            -v ${{ github.workspace }}:/workspace \
            -p 2087:2087 \
            my-lsp-server

          # 等待 Server 启动
          sleep 5

          # 运行分析脚本
          node scripts/lsp-analyze.js --host localhost --port 2087 \
            --workspace /workspace \
            --output report.json

          # 检查报告
          if grep -q '"severity":1' report.json; then
            echo "Errors found!"
            cat report.json
            exit 1
          fi

14.6.2 分析脚本

// scripts/lsp-analyze.ts
import * as net from "net";
import * as fs from "fs";

interface Diagnostic {
  file: string;
  line: number;
  severity: number;
  message: string;
}

async function analyzeWorkspace(host: string, port: number, workspace: string): Promise<Diagnostic[]> {
  const socket = net.connect(port, host);
  const diagnostics: Diagnostic[] = [];

  return new Promise((resolve, reject) => {
    let buffer = "";

    socket.on("data", (chunk) => {
      buffer += chunk.toString();
      // 解析消息...
      // 收集 diagnostics
    });

    socket.on("error", reject);

    // 发送初始化
    sendRequest(socket, "initialize", {
      processId: process.pid,
      rootUri: `file://${workspace}`,
      capabilities: {},
    }).then(() => {
      sendNotification(socket, "initialized", {});
      // 分析所有文件...
    });
  });
}

const args = parseArgs(process.argv);
analyzeWorkspace(args.host, args.port, args.workspace)
  .then((diagnostics) => {
    fs.writeFileSync(args.output, JSON.stringify(diagnostics, null, 2));
    process.exit(diagnostics.some((d) => d.severity === 1) ? 1 : 0);
  })
  .catch(console.error);

14.7 性能优化

14.7.1 镜像体积优化

策略效果
多阶段构建减少 50-80% 镜像大小
Alpine 基础镜像基础镜像仅 ~5MB
.dockerignore排除 node_modules、测试文件
npm ci –omit=dev只安装生产依赖
# .dockerignore
node_modules
dist
test
*.md
.git
.github
.vscode

14.7.2 容器资源限制

# docker-compose.yml
services:
  lsp-server:
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 2G
        reservations:
          cpus: "0.5"
          memory: 512M

14.7.3 网络优化

// 客户端连接优化:使用连接池和重试
class DockerLSPConnection {
  private maxRetries = 3;
  private retryDelay = 1000;

  async connect(host: string, port: number): Promise<net.Socket> {
    for (let i = 0; i < this.maxRetries; i++) {
      try {
        const socket = net.connect(port, host);
        await new Promise<void>((resolve, reject) => {
          socket.once("connect", resolve);
          socket.once("error", reject);
        });
        return socket;
      } catch (err) {
        if (i < this.maxRetries - 1) {
          await new Promise((r) => setTimeout(r, this.retryDelay));
        } else {
          throw err;
        }
      }
    }
    throw new Error("Failed to connect");
  }
}

⚠️ 注意事项

问题建议
容器启动慢使用轻量基础镜像(Alpine)
文件同步延迟使用 bind mount 而非 volume
权限问题挂载目录时指定正确的用户/组
内存不足设置合理的 memory 限制
网络超时实现健康检查和重连机制
路径映射注意容器内外路径转换

🔗 扩展阅读


下一章第 15 章:最佳实践 — 性能优化、错误处理、发布到生态。