# Windsurf Language Server Docker Image # # 说明: # - LS 本体只监听 127.0.0.1:,并且仅对 loopback peer 通过 CSRF 校验。 # - 为了让 LS 融入 compose 内部网络(而不是必须使用 host network), # 容器内启动一个 socat 把外部 0.0.0.0: 的流量转发到 127.0.0.1:。 # LS 收到的 peer 地址仍然是 127.0.0.1,CSRF 校验通过,同时 compose 里其它服务 # 可以直接用 `windsurf-ls:42099` 访问。 # # 构建: # docker build -t windsurf-ls -f deploy/Dockerfile.ls . # # 运行(一般不要单独 docker run,通过 compose 的 windsurf profile 启动): # docker compose --profile windsurf up -d # # LS 二进制在构建时从 Exafunction/codeium 的 latest release 下载。 # 本地已有二进制时可通过 --build-arg LS_URL=file:///path 覆盖。 FROM alpine:3.21 AS downloader RUN apk add --no-cache curl jq ARG TARGETARCH ARG LS_URL="" RUN set -e; \ if [ -n "$LS_URL" ]; then \ echo "Downloading LS from: $LS_URL"; \ curl -fL --progress-bar -o /tmp/language_server "$LS_URL"; \ else \ case "$TARGETARCH" in \ amd64) ASSET="language_server_linux_x64" ;; \ arm64) ASSET="language_server_linux_arm" ;; \ *) echo "Unsupported arch: $TARGETARCH"; exit 1 ;; \ esac; \ echo "Fetching latest Exafunction/codeium release..."; \ URL=$(curl -fsSL https://api.github.com/repos/Exafunction/codeium/releases/latest \ | jq -r --arg asset "$ASSET" '.assets[] | select(.name == $asset) | .browser_download_url'); \ if [ -z "$URL" ] || [ "$URL" = "null" ]; then \ echo "ERROR: Could not find asset $ASSET in latest release"; exit 1; \ fi; \ echo "Downloading: $URL"; \ curl -fL --progress-bar -o /tmp/language_server "$URL"; \ fi; \ chmod +x /tmp/language_server FROM debian:bookworm-slim # ca-certificates: LS 访问上游 API (HTTPS) # netcat-openbsd : healthcheck 用的 `nc -z` 探测端口 # socat : loopback 端口转发,让 compose 内部网络可直达 LS # tini : PID 1 init,正确回收 LS 子进程,转发信号 # bash : entrypoint 依赖 `wait -n`(dash/busybox 不支持) RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates netcat-openbsd socat tini bash && \ rm -rf /var/lib/apt/lists/* WORKDIR /opt/windsurf COPY --from=downloader /tmp/language_server /opt/windsurf/language_server_linux_x64 RUN mkdir -p /data/db # LS_PORT : 容器对外暴露的监听端口(socat 绑定 0.0.0.0:LS_PORT) # LS_INTERNAL_PORT : LS 本体绑定的端口(LS 实际在 0.0.0.0:LS_INTERNAL_PORT 监听, # 但 socat 发起的连接源地址为 127.0.0.1,CSRF 校验依旧通过) # 与 LS_PORT 必须不同,否则 socat 会和 LS 抢同一端口。 ENV LS_PORT=42099 \ LS_INTERNAL_PORT=42098 \ LS_CSRF_TOKEN=ad2d9f01-4e7b-8c3a-b5f6-1d8e9a0c7b2f \ LS_API_SERVER_URL=https://server.self-serve.windsurf.com \ HTTPS_PROXY="" \ HTTP_PROXY="" EXPOSE 42099 # 健康检查: socat 端口可达即视为健康(实际会触发一次 TCP 握手到 LS) HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=5 \ CMD nc -z 127.0.0.1 "${LS_PORT}" || exit 1 # tini 做 PID 1,确保 LS 子进程被正确收尾 + 信号转发。 # 用 bash 而非 /bin/sh,因为 Debian 的 /bin/sh 指向 dash,不支持 `wait -n`。 # 启动脚本逻辑: # 1. 后台拉起 LS,只绑 127.0.0.1:${LS_INTERNAL_PORT} # 2. 轮询等待 LS 真正开始监听 # 3. 后台起 socat,0.0.0.0:${LS_PORT} → 127.0.0.1:${LS_INTERNAL_PORT} # 4. `wait -n` 等任一子进程退出 → 容器一并退出,交由 compose 重启策略兜底 ENTRYPOINT ["/usr/bin/tini", "-g", "--", "/bin/bash", "-c", "\ set -e; \ /opt/windsurf/language_server_linux_x64 \ --api_server_url=\"${LS_API_SERVER_URL}\" \ --server_port=\"${LS_INTERNAL_PORT}\" \ --csrf_token=\"${LS_CSRF_TOKEN}\" \ --register_user_url=https://api.codeium.com/register_user/ \ --codeium_dir=/data \ --database_dir=/data/db \ --enable_local_search=false \ --enable_index_service=false \ --enable_lsp=false \ --detect_proxy=false & \ LS_PID=$!; \ echo \"[entrypoint] LS started pid=$LS_PID, waiting on 127.0.0.1:${LS_INTERNAL_PORT}\"; \ for i in $(seq 1 60); do \ if nc -z 127.0.0.1 \"${LS_INTERNAL_PORT}\"; then \ echo \"[entrypoint] LS is listening, starting socat forwarder\"; \ break; \ fi; \ if ! kill -0 $LS_PID 2>/dev/null; then \ echo \"[entrypoint] LS exited before listening\"; exit 1; \ fi; \ sleep 1; \ done; \ socat -d TCP-LISTEN:${LS_PORT},fork,reuseaddr,bind=0.0.0.0 TCP:127.0.0.1:${LS_INTERNAL_PORT} & \ SOCAT_PID=$!; \ echo \"[entrypoint] socat started pid=$SOCAT_PID, forwarding 0.0.0.0:${LS_PORT} -> 127.0.0.1:${LS_INTERNAL_PORT}\"; \ wait -n $LS_PID $SOCAT_PID; \ EXIT=$?; \ echo \"[entrypoint] one of LS/socat exited with $EXIT, tearing down\"; \ kill $LS_PID $SOCAT_PID 2>/dev/null || true; \ exit $EXIT\ "]