在 macOS 上安装 minikube 并本地运行 CFML 应用

《在 Kubernetes 上运行 CFML 应用》 默认你手里已经有一个集群。这篇讲的是它的前一步:用 minikube 在自己的 Mac 上跑起一个真实的 Kubernetes 集群,并把 Lucee/CFML 应用部署上去——不需要镜像仓库,不需要云账号,也不会留下删不掉的 YAML。

下面所有操作都在 macOS 上使用 Docker 驱动完成,这条路径在 Intel 和 Apple Silicon 上表现完全一致。

第零步:前置条件

你需要 Homebrew 和一个容器运行时。Docker Desktop 最省事;Colima 也可以,而且更轻量。

bash
# Homebrew——已装可跳过
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# 容器运行时——二选一
brew install --cask docker      # Docker Desktop,装完从"应用程序"里先启动一次
# 或
brew install colima && colima start --cpus 4 --memory 8

docker ps                       # 这条命令必须成功,才能继续

给运行时留足资源。 Lucee 是跑在 Tomcat 里的 JVM 应用,Docker Desktop 默认的 2 GB 会让 Pod 以各种"看起来像 Kubernetes 问题、其实不是"的方式挂掉。在 Docker Desktop 里:Settings → Resources,至少给 4 CPU 和 8 GB 内存。

第一步:安装 minikube 与 kubectl

bash
brew install minikube
brew install kubectl

minikube version
kubectl version --client

两者都是单个二进制文件,用 Homebrew 只是图方便。在 Apple Silicon 上,brew install minikube 会自动装 arm64 版本。

第二步:启动集群

bash
minikube start --driver=docker --cpus=4 --memory=8192 --disk-size=40g

关于这几个参数:

  • --driver=docker——Docker 在运行时它就是默认值,也是我在 Apple Silicon 上唯一推荐的驱动。老的 hyperkit 驱动只支持 Intel 且已废弃;qemu 虽然能跑 arm64,但网络要额外配 socket_vmnet,在这个场景里没有任何收益。
  • --memory=8192——这是集群的预算,必须小于你给 Docker 的额度。一个 Lucee Pod 舒服跑起来要 ~1 GB;两个副本加一个本地 Postgres 再加系统组件,4 GB 很快就满了。
  • --disk-size——Lucee 镜像约 600 MB。等你构建过十几个 tag,默认的 20 GB 会比想象中更快耗尽。

验证:

bash
kubectl get nodes
# NAME       STATUS   ROLES           AGE   VERSION
# minikube   Ready    control-plane   45s   v1.31.0

minikube status

minikube start 会顺手把 kubectl 的 context 指向新集群,所以 kubectl 命令可以直接用。如果你同时管着多个集群:kubectl config use-context minikube

一些后面会用到的插件,现在就开好:

bash
minikube addons enable ingress          # nginx ingress 控制器
minikube addons enable metrics-server   # kubectl top、HPA

第三步:把镜像构建在集群内部

这是 macOS 上最省时间的一个技巧。minikube 自带一套独立于你 Mac 的容器运行时。你在本地构建的镜像它是看不见的,Pod 会以 ErrImagePull 失败——哪怕 docker images 明明列出了那个镜像。

解决办法是把当前 shell 的 Docker 客户端指向 minikube 的守护进程,在那里构建:

bash
eval $(minikube docker-env)     # 这个 shell 从此对接 minikube 的 Docker
docker build -t cfml-app:dev .
docker images | grep cfml-app   # 集群内部可见

eval 只对当前 shell 生效。新开一个终端标签页就又回到了 Mac 本地的 Docker——这是"重新构建了却不生效"的常见原因。

另一种做法,适用于所有驱动,也不会"劫持"你的 shell:

bash
docker build -t cfml-app:dev .
minikube image load cfml-app:dev

它更慢(要把镜像拷进集群),但足够显式。我迭代时用 docker-env,写脚本时用 image load

不管用哪种,都必须在清单里设置 imagePullPolicy: Never,否则 Kubernetes 会跑去 Docker Hub 拉 cfml-app:dev,然后失败。

Dockerfile 本身没什么特别的:

dockerfile
FROM lucee/lucee:6.0-tomcat

COPY ./webroot /var/www

ENV LUCEE_ADMIN_ENABLED="false"
# 让 JVM 按容器限额分配堆,而不是去猜宿主机的内存
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75"

EXPOSE 8888

第四步:部署应用

一个文件 k8s/app.yaml,装下 Deployment 和一个 NodePort Service:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cfml-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cfml-app
  template:
    metadata:
      labels:
        app: cfml-app
    spec:
      containers:
        - name: cfml-app
          image: cfml-app:dev
          imagePullPolicy: Never # 本地构建镜像的关键
          ports:
            - containerPort: 8888
          resources:
            requests:
              memory: "768Mi"
              cpu: "250m"
            limits:
              memory: "1536Mi"
              cpu: "1"
          readinessProbe:
            httpGet:
              path: /health.cfm
              port: 8888
            initialDelaySeconds: 25
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /health.cfm
              port: 8888
            initialDelaySeconds: 60
            periodSeconds: 15
---
apiVersion: v1
kind: Service
metadata:
  name: cfml-app
spec:
  type: NodePort
  selector:
    app: cfml-app
  ports:
    - port: 80
      targetPort: 8888

webroot 里的 health.cfm 一行就够:

cfml
<cfoutput>OK</cfoutput>

应用并观察启动过程:

bash
kubectl apply -f k8s/app.yaml
kubectl rollout status deployment/cfml-app
kubectl logs -f deploy/cfml-app          # Lucee 启动日志,首次约 30 秒

第一次启动很慢——Lucee 要在首启时完成编译并写下自己的配置。上面 initialDelaySeconds 给得宽松正是为此;调紧了你会得到一串看起来像应用 bug 的崩溃重启循环。

第五步:从浏览器访问应用

这里是 macOS 与 Linux 的分水岭。在 Docker 驱动下,minikube 节点的 IP 在 macOS 上是不可路由的——minikube ip 返回的地址你根本 curl 不通。你需要一条隧道。

快捷方式:

bash
minikube service cfml-app --url
# http://127.0.0.1:52194

在 macOS 上这条命令会驻留前台来维持隧道。关掉它,URL 就失效了。让它单开一个终端标签页跑着。

日常更简单、也更可预期的做法——直接端口转发:

bash
kubectl port-forward svc/cfml-app 8888:80
# http://localhost:8888

如果你想要一个域名:Ingress

在启用了 ingress 插件的前提下:

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: cfml-app
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
spec:
  ingressClassName: nginx
  rules:
    - host: cfml.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: cfml-app
                port:
                  number: 80
bash
kubectl apply -f k8s/ingress.yaml

sudo minikube tunnel        # 保持运行;会要求输入密码
echo "127.0.0.1 cfml.local" | sudo tee -a /etc/hosts
# http://cfml.local

在 Docker 驱动下,是 minikube tunnel 把 ingress 映射到 127.0.0.1,所以 cfml.local 要指向 localhost,而不是 minikube ip。搞反这一点,就是"我的 ingress 在 Mac 上不通"最常见的原因。

第六步:搭一个热编辑开发循环

每改一个 .cfm 就重建一次镜像太痛苦了。改成把 Mac 上的 webroot 挂载进集群:

bash
minikube mount "$PWD/webroot:/mnt/webroot"     # 前台运行,保持不关

然后让 Pod 指向这个挂载点:

yaml
          volumeMounts:
            - name: webroot
              mountPath: /var/www
      volumes:
        - name: webroot
          hostPath:
            path: /mnt/webroot
bash
kubectl rollout restart deployment/cfml-app

现在在编辑器里改 .cfm 文件,刷新页面就能看到效果——Lucee 会在请求时重新编译变更过的模板。这套只留给开发环境:挂载意味着运行时实时依赖你的笔记本,而镜像的意义恰恰在于生产环境自带全部代码。

注意 minikube mount 是第三个前台进程,加上隧道一共三个。三个终端标签页就是 macOS 上 minikube 的常态工作布局。

第七步:给本地开发配一个数据库

CFML 应用基本都需要数据源。把它跑在集群里而不是 Mac 上,这样连接串的形态才和生产一致:

bash
kubectl create secret generic cfml-db \
  --from-literal=DB_USER=app \
  --from-literal=DB_PASSWORD=devpassword

kubectl create deployment postgres --image=postgres:16
kubectl set env deployment/postgres POSTGRES_PASSWORD=devpassword POSTGRES_USER=app POSTGRES_DB=appdb
kubectl expose deployment postgres --port=5432

把凭据注入应用 Pod:

yaml
env:
  - name: DB_HOST
    value: "postgres" # Service 名就是主机名
  - name: DB_NAME
    value: "appdb"
envFrom:
  - secretRef:
      name: cfml-db

再在 Application.cfc 里从环境变量定义数据源,任何东西都不写死:

cfml
component {
    this.datasources["appdb"] = {
        class:            "org.postgresql.Driver",
        connectionString: "jdbc:postgresql://#server.system.environment.DB_HOST#/#server.system.environment.DB_NAME#",
        username:         server.system.environment.DB_USER,
        password:         server.system.environment.DB_PASSWORD
    };
}

这和 Kubernetes 指南 在生产里用的是同一套模式——变的只是注入进去的值。

排障速查

现象 原因 解决
ErrImagePull / ImagePullBackOff 镜像构建在 Mac 的 Docker 里,不在 minikube 里 eval $(minikube docker-env) 后重建,或用 minikube image load;并设置 imagePullPolicy: Never
CrashLoopBackOff,日志停在启动中途 存活探针在 Lucee 启动完成前就把它杀了 initialDelaySeconds 提到 60 以上
Pod 被 OOMKilled JVM 按宿主机内存算堆,而非容器限额 -XX:MaxRAMPercentage=75,内存 limit 不低于 1 Gi
浏览器访问 minikube ip 无响应 Docker 驱动的节点 IP 在 macOS 上不可路由 kubectl port-forwardminikube service --url
Ingress 域名超时 没有开隧道 sudo minikube tunnel,并把域名指到 127.0.0.1
Docker 重启后集群起不来 集群状态残留 minikube delete && minikube start——它只是开发集群,删掉没有任何代价

真的看不出问题时,这两条命令值得记住:

bash
kubectl describe pod <name>     # 事件:拉取失败、OOM、探针失败都在这里
minikube dashboard              # 浏览器里的集群全景 UI

清理

bash
minikube stop      # 保留集群,释放 CPU/内存
minikube start     # 约 20 秒回来
minikube delete    # 彻底删除

一天工作结束时你要的是 minikube stop。而集群一旦开始"发癫",答案就是 delete——本地开发集群里没有任何值得保留的状态,重建的成本不过是一条命令加一次镜像加载。

小结

整套流程配好之后就是:

bash
minikube start                          # 1. 集群
eval $(minikube docker-env)             # 2. Docker 指向集群
docker build -t cfml-app:dev .          # 3. 在集群内构建
kubectl apply -f k8s/                   # 4. 部署
kubectl port-forward svc/cfml-app 8888:80   # 5. 打开它

五条命令,就能在笔记本上让 CFML 应用跑在真实的 Kubernetes 里。minikube 的价值不在于它等于生产——它当然不是——而在于清单、探针、配置注入以及各种失败模式,都和你在托管集群上会遇到的一模一样。在本地就把 Lucee 的慢启动和容器感知堆内存这两件事搞定,就不必等到预发布环境才发现它们。