LSP 开发指南 / 第 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 章:最佳实践 — 性能优化、错误处理、发布到生态。