一次性把开发环境配好:Go、Node 与 Docker 的安装与取舍

Go 的三种装法与取舍、Node 用 nvm/fnm + corepack 的流程、Docker 官方脚本与发行版仓库的区别、国内镜像加速与非 root 用户,最后一份「装完自检」命令清单。

换新笔记本、临时给同事复现一个 bug、给 CI 机器重装一遍工具链——一年里总有两三次要把 Go、Node 和 Docker 从头装一遍。前几次我都是边搜边装,装完能用就不管了;直到有一次在一台新机器上花了两个多小时,最后卡在 corepack enable 的一个权限报错上。所以我把现在的做法固定了下来:Go 用官方 tarball,Node 用 nvm/fnm,Docker 走官方 apt/yum 仓库。下面写清楚每一步为什么这么选、代价是什么,以及装完怎么自检。

Go:装法有三种,代价完全不同

先说结论:我自己常年用官方 tarball,多版本需求的项目里用 asdf 类工具,绝不建议用发行版自带的包——最后这条是有教训的。

**发行版包(apt install golang-go)**的诱人之处是简单,apt upgrade 还能跟着升级。代价是版本经常落后好几个小版本,而且 Debian 和 Ubuntu 的版本号完全不一样——我在两个环境里同一份 go.mod 写着 go 1.22,一个能编一个报错。更麻烦的是它会把 Go 装进 /usr/lib/go-1.22,还得靠 update-alternatives 选版本,出问题时排查路径特别绕。

官方 tarball 是我现在的主力:

# 先看当前最新版(版本会变,以官方文档和下载页为准)
curl -sL https://go.dev/dl/?mode=json | jq -r '.[0].version'

wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz

# PATH(Ubuntu/Debian 与 macOS 都是改 shell 启动文件)
echo 'export PATH=/usr/local/go/bin:$PATH' >> ~/.bashrc   # zsh 用户写 ~/.zshrc
source ~/.bashrc

代价是升级要手动重复一遍上面三步,而且必须记得先 rm -rf /usr/local/go——我偷懒直接解压覆盖过一次,结果是新旧文件混在一起,go build 报了莫名其妙的错。

macOS 上还有一条更省事的路:brew install go。它的定位介于两者之间——版本比发行版新得多,升级一条命令搞定,但 Homebrew 会在升级时顺带更新一堆依赖,偶尔会因为某个传递依赖编译失败而把 Go 卡在旧版本。想干净可控,还是 tarball。

asdf / mise 这类版本管理,适合手上有多个项目、每个项目 go.mod 要求不同版本的场景。它给每个项目一个 .tool-versions:

asdf plugin add golang https://github.com/asdf-community/asdf-golang.git
asdf install golang 1.21.13
asdf install golang 1.22.5
asdf global  golang 1.22.5     # 默认
asdf local   golang 1.21.13    # 当前目录(写进 .tool-versions)

代价是每个新 shell 都要加载一层 shim,速度上多一点点开销;另外它编译 Go 工具时会跟 asdf 的 shim 互相干扰,必须 asdf reshim golang 才能在别的目录里用。我遇到过最烦的一次是 IDE 里终端拿到了 A 版本、GUI 进程拿的是 B 版本,编译结果不一致,查了半小时。

装法拿到新版本的速度升级成本多版本切换我什么时候用
发行版包 (apt/dnf)慢,落后几个版本apt upgrade 一条命令靠 update-alternatives,麻烦只在容器镜像里图省事
官方 tarball最快,当天可用手动三步,要记得先删旧目录靠换 PATH 或写绝对路径主力开发机
brew (仅 macOS)较快brew upgrade go不擅长的部分要另找工具不想自己管路径时的折中
asdf / mise较快,等插件更新asdf install 新版本即可最好的,按目录自动切同时维护多个老项目

GOROOT / GOPATH / GOBIN 现在还用不用手动配

这是我被问得最多的问题,答案分三句:

  • GOROOT:基本不用配。 现在的 go 命令自己知道装在哪,只有当你把 Go 装在非标准位置、或者给别的工具(某些老版本 IDE、makefile 里的脚本)用时才需要显式设。手动配上错的 GOROOT 反而是"所有命令都报错"的经典原因,遇到 go: cannot find GOROOT directory 第一件事就是 unset GOROOT。
  • GOPATH:不用配,但要接受它的默认值 $HOME/go。 它的职责已经从"工作区"降级为"缓存 + 全局安装目录"。我唯一会碰它的情况是机器上 home 分区很小,想把它挪到数据盘。
  • GOBIN:只有想统一管理全局二进制时才配。 不配的话 go install 的东西落在 $GOPATH/bin,只要这个目录在 PATH 里就能直接用。我习惯配一下,把所有第三方 CLI 收在一个目录里:
go env -w GOBIN="$HOME/.local/bin"      # 记得确保它在 PATH 里
go env -w GOPATH="$HOME/go"
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOSUMDB=sum.golang.google.cn

go env -w 会把配置写进 $HOME/.config/go/env,比往 shell 里塞一堆 export 干净得多——这也是我现在的偏好:能用 go env -w 就不用 export,因为前者对 GUI 启动的 IDE 也生效,后者只对从终端启动的进程生效。

GOPROXY 这条值得单说。proxy.golang.org 在国内基本不可用,不配代理的表现是 go mod tidy 一直卡住然后超时。https://goproxy.cn,direct 里的 direct 表示代理没命中时回源直连,别只用代理不加 direct,否则私有仓库会拉不下来。私有仓库还得配 GOPRIVATE:

go env -w GOPRIVATE=git.mycompany.com,github.com/my-org/*

注意 GOPRIVATE 命中的模块会跳过校验和数据库,所以别图省事写成 *。

Node:nvm 还是 fnm,以及 corepack 的两类报错

Node 这边我的要求很明确:永远不要用系统包管理器装 Node,也不要用 sudo npm install -g。系统装的 Node 会在 /usr/lib/node_modules 下建目录,之后每一次全局安装都要 sudo,而用 sudo npm 装出来的东西又常常是 root 所有,普通用户跑起来报权限错——绕进这个循环就很难出来了。

我用过 nvm 很多年,现在换成了 fnm,主要原因是启动速度:nvm 是纯 shell 脚本,加载要几十到几百毫秒;fnm 是二进制。两者用法几乎一样,迁移成本很低。

# Linux / macOS 通用(fnm)
curl -fsSL https://fnm.vercel.app/install | bash

# 加进 shell 启动文件(zsh 就写 ~/.zshrc)
echo 'eval "$(fnm env --use-on-cd --shell zsh)"' >> ~/.zshrc
source ~/.zshrc

fnm install 22
fnm use 22
fnm default 22
node -v

第一个坑:装完了新开终端 node -v 还是 command not found。 八成是启动文件写错了——国内很多 zsh 环境用的是 ~/.zshrc,但有些工具会去读 ~/.zprofile,而 macOS 的 Terminal 默认登录 shell 读的又不一样。判断方法很直接:echo $SHELL 看当前 shell,然后确认 fnm env 那行确实在被读取的文件里。我自己的规则是只写一个文件,不重复写,重复加载会让 PATH 里堆出一长串同样的路径,排查时看着头疼。

第二个坑来自 corepack。 现在启用 pnpm 的标准做法是:

corepack enable          # 让 corepack 接管 pnpm / yarn 的 shim
corepack prepare pnpm@9 --activate
pnpm -v

但如果 Node 是系统包(或系统装的 Node 配 sudo corepack enable),常见的报错长这样:

Error: EACCES: permission denied, mkdir '/usr/lib/node_modules/corepack'

这不是 bug,是它没权限往 Node 的安装目录写 shim。三种解法,按我的推荐顺序:

  1. 换 fnm/nvm 管 Node,整个问题从根上消失——这是正解;
  2. corepack enable --install-directory ~/.local/bin,把 shim 装到自己的目录(前提是这目录在 PATH 里);
  3. 退而求其次 npm i -g pnpm(不要加 sudo,先修好 Node 的安装位置再说)。

还有一个新版 Node 带来的变化:corepack 不再信任没有哈希的 packageManager 字段。老项目 package.json 里只写了 "packageManager": "pnpm@8.15.0",运行时会报哈希不匹配并拒绝启动。修法是重新生成带哈希的字段:

corepack use pnpm@9.1.0     # 自动写入带哈希的 packageManager

不想被提示打断的话,可以 export COREPACK_ENABLE_DOWNLOAD_PROMPT=0,让 corepack 直接下载而不问。

Docker:官方脚本省事,发行版仓库省心

curl -fsSL https://get.docker.com | sh 这条命令我用了很多年,它确实快:识别发行版、加仓库、装引擎、起服务,一条龙。它的代价有两个:装完不自动配国内镜像加速(后面还得自己改 daemon.json),以及它是"跟着最新版走"的——官方脚本给的是当前 stable,而生产环境我更希望版本可控、可回滚。所以:

  • 一次性、临时的机器(CI runner、虚拟机试环境):官方脚本,接受它的不确定性;
  • 长期使用的开发机和生产机:走发行版仓库(Docker 官方维护的那份,不是 Ubuntu 自带的 docker.io),版本可以固定。
# Ubuntu / Debian:Docker 官方 apt 仓库
sudo apt update && sudo apt install -y ca-certificates curl gnupg
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
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

macOS 上是另一套:直接装 Docker Desktop(Apple Silicon 用 arm64 包),它自带虚拟机跑 Linux 内核。注意两件事:默认虚拟磁盘空间给得偏小,跑几个大镜像就满了,建议提前在设置里调到 60GB 以上;设置里的内存上限同样会被容器共用,Node 项目 pnpm build 跑到一半被杀,往往就是这个上限。

国内镜像加速取决于你从哪个源拉镜像,配置写在 /etc/docker/daemon.json:

{
  "registry-mirrors": ["https://docker.m.daocloud.io", "https://dockerproxy.com"],
  "log-driver": "json-file",
  "log-opts": { "max-size": "50m", "max-file": "3" }
}

改完要 sudo systemctl restart docker。这里有个细节值得提:log-opts 那两行我每次都加,因为 Docker 默认的 json-file 日志不轮转,一个不断打日志的容器能把磁盘写满——我踩过一次,是磁盘告警救的我。另外镜像加速站点的可用性变化很快,写进配置里的地址过一阵可能就失效了,报 TLS handshake timeout 时先去搜当前可用的。

非 root 用户用 docker 是另一件必做的小事:

sudo usermod -aG docker $USER
newgrp docker        # 或者重新登录一次
docker run --rm hello-world

不加这步,每次 docker 都要 sudo,而 sudo docker 建的挂载文件属于 root,之后 docker compose down -v 清不掉、本地目录里凭空冒出 root 所有的文件。安全上要知道:加入 docker 组等价于给了这台机器的 root 权限(能挂载任意宿主目录),自己的开发机无所谓,多人共用的机器要谨慎。

systemctl 和 service 的差异也值得记一句:现在主流发行版都是 systemd,systemctl status docker / enable / restart 是正统用法;service docker start 在 systemd 系统上只是转发到 systemctl 的兼容层,而且它不回显错误,服务起失败时你要看 journalctl -u docker -n 50 才知道原因。菜鸟时期我用 service 排查过很久,换成 journalctl 之后基本一眼就看出问题了。

装完自检:六条命令

环境装完,我固定跑这一组,全过才认为环境可用:

go version                                   # 期望:go version go1.22.5 linux/amd64
go env GOROOT GOPATH GOBIN GOPROXY GOPRIVATE # 期望:路径正确,代理已配
node -v && npm -v                            # 期望:v22.x / 10.x
pnpm -v                                      # 期望:能打印版本(corepack 未生效时会 command not found)
docker version --format '{{.Server.Version}}' # 期望:Server 段不报错(报错说明没权限或 daemon 没起)
docker run --rm hello-world                  # 期望:打印 Hello from Docker!

两个补充技巧:go env 不带参数会输出全部变量和它们的来源,排查"我的设置怎么没生效"很好用;docker version 里 Client 和 Server 分开显示,只有 Client 有输出而 Server 报 Cannot connect to the Docker daemon,说明是 daemon 没启动或当前用户不在 docker 组——这两个原因占了我遇到这类错误的九成。

这一套装完,我在新机器上的耗时从两个多小时压到了二十分钟左右,其中大部分时间是在等下载。真正省下时间的不是某条命令,而是每次做同样的选择:tarball、fnm、官方仓库。环境这种东西,最贵的不是装它的时间,是它跟你玩不一致时排查的时间。

返回博客列表