在 macOS 上安装 minikube 并本地运行 CFML 应用
在 macOS 上安装 minikube 并本地运行 CFML 应用
《在 Kubernetes 上运行 CFML 应用》 默认你手里已经有一个集群。这篇讲的是它的前一步:用 minikube 在自己的 Mac 上跑起一个真实的 Kubernetes 集群,并把 Lucee/CFML 应用部署上去——不需要镜像仓库,不需要云账号,也不会留下删不掉的 YAML。
下面所有操作都在 macOS 上使用 Docker 驱动完成,这条路径在 Intel 和 Apple Silicon 上表现完全一致。
第零步:前置条件
你需要 Homebrew 和一个容器运行时。Docker Desktop 最省事;Colima 也可以,而且更轻量。
# 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
brew install minikube
brew install kubectl
minikube version
kubectl version --client两者都是单个二进制文件,用 Homebrew 只是图方便。在 Apple Silicon 上,brew install minikube 会自动装 arm64 版本。
第二步:启动集群
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 会比想象中更快耗尽。
验证:
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# minikube Ready control-plane 45s v1.31.0
minikube statusminikube start 会顺手把 kubectl 的 context 指向新集群,所以 kubectl 命令可以直接用。如果你同时管着多个集群:kubectl config use-context minikube。
一些后面会用到的插件,现在就开好:
minikube addons enable ingress # nginx ingress 控制器
minikube addons enable metrics-server # kubectl top、HPA第三步:把镜像构建在集群内部
这是 macOS 上最省时间的一个技巧。minikube 自带一套独立于你 Mac 的容器运行时。你在本地构建的镜像它是看不见的,Pod 会以 ErrImagePull 失败——哪怕 docker images 明明列出了那个镜像。
解决办法是把当前 shell 的 Docker 客户端指向 minikube 的守护进程,在那里构建:
eval $(minikube docker-env) # 这个 shell 从此对接 minikube 的 Docker
docker build -t cfml-app:dev .
docker images | grep cfml-app # 集群内部可见eval 只对当前 shell 生效。新开一个终端标签页就又回到了 Mac 本地的 Docker——这是"重新构建了却不生效"的常见原因。
另一种做法,适用于所有驱动,也不会"劫持"你的 shell:
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 本身没什么特别的:
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:
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: 8888webroot 里的 health.cfm 一行就够:
<cfoutput>OK</cfoutput>
应用并观察启动过程:
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 不通。你需要一条隧道。
快捷方式:
minikube service cfml-app --url
# http://127.0.0.1:52194在 macOS 上这条命令会驻留前台来维持隧道。关掉它,URL 就失效了。让它单开一个终端标签页跑着。
日常更简单、也更可预期的做法——直接端口转发:
kubectl port-forward svc/cfml-app 8888:80
# http://localhost:8888如果你想要一个域名:Ingress
在启用了 ingress 插件的前提下:
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: 80kubectl 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 挂载进集群:
minikube mount "$PWD/webroot:/mnt/webroot" # 前台运行,保持不关然后让 Pod 指向这个挂载点:
volumeMounts:
- name: webroot
mountPath: /var/www
volumes:
- name: webroot
hostPath:
path: /mnt/webrootkubectl rollout restart deployment/cfml-app现在在编辑器里改 .cfm 文件,刷新页面就能看到效果——Lucee 会在请求时重新编译变更过的模板。这套只留给开发环境:挂载意味着运行时实时依赖你的笔记本,而镜像的意义恰恰在于生产环境自带全部代码。
注意 minikube mount 是第三个前台进程,加上隧道一共三个。三个终端标签页就是 macOS 上 minikube 的常态工作布局。
第七步:给本地开发配一个数据库
CFML 应用基本都需要数据源。把它跑在集群里而不是 Mac 上,这样连接串的形态才和生产一致:
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:
env:
- name: DB_HOST
value: "postgres" # Service 名就是主机名
- name: DB_NAME
value: "appdb"
envFrom:
- secretRef:
name: cfml-db再在 Application.cfc 里从环境变量定义数据源,任何东西都不写死:
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-forward 或 minikube service --url |
| Ingress 域名超时 | 没有开隧道 | sudo minikube tunnel,并把域名指到 127.0.0.1 |
| Docker 重启后集群起不来 | 集群状态残留 | minikube delete && minikube start——它只是开发集群,删掉没有任何代价 |
真的看不出问题时,这两条命令值得记住:
kubectl describe pod <name> # 事件:拉取失败、OOM、探针失败都在这里
minikube dashboard # 浏览器里的集群全景 UI清理
minikube stop # 保留集群,释放 CPU/内存
minikube start # 约 20 秒回来
minikube delete # 彻底删除一天工作结束时你要的是 minikube stop。而集群一旦开始"发癫",答案就是 delete——本地开发集群里没有任何值得保留的状态,重建的成本不过是一条命令加一次镜像加载。
小结
整套流程配好之后就是:
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 的慢启动和容器感知堆内存这两件事搞定,就不必等到预发布环境才发现它们。