The mental model (what you’re aiming for)
For many functions, I strongly prefer Tilt because you can script/loop resources easily.
1) Make each function “hot-reloadable” in a pod
Recommended repo layout
repo/
functions/
simple-email/
src/
package.json
send-email-link/
src/
package.json
packages/
shared/
pnpm-workspace.yaml
pnpm-lock.yaml
Dockerfile.dev
k8s/
knative/
simple-email.yaml
send-email-link.yaml
Tiltfile
functions/<fn>/package.json
Make sure each function has a dev script that restarts on changes:
JS
{
"scripts": {
"dev": "nodemon --legacy-watch --watch src --ext js,json --exec node src/index.js",
"test": "vitest run"
},
"devDependencies": {
"nodemon": "^3.0.0",
"vitest": "^2.0.0"
}
}
TS (nice default)
{
"scripts": {
"dev": "tsx watch src/index.ts",
"test": "vitest run"
},
"devDependencies": {
"tsx": "^4.0.0",
"vitest": "^2.0.0"
}
}
File watching in containers can be flaky → polling helps. We’ll set env vars in the Knative Service.
2) Use a dev Docker image that can run any function
Dockerfile.dev (monorepo-friendly, pnpm)
FROM node:20-alpine
WORKDIR /app
RUN corepack enable
# Install deps (cached)
COPY pnpm-lock.yaml package.json pnpm-workspace.yaml ./
COPY functions ./functions
COPY packages ./packages
RUN pnpm install --frozen-lockfile
# Default; each Knative Service will override workingDir/command
CMD ["node", "-e", "console.log('dev image ready')"]
This image contains the whole repo so that live-sync can just overwrite files in-place.
3) Knative Service per function (dev-friendly)
Key points:
- minScale: "1" so the pod stays running (hot reload needs a running process)
- optional cluster-local visibility to simplify calling from tests/jobs
- set polling env vars
Example: k8s/knative/simple-email.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: simple-email
namespace: interweb
annotations:
networking.knative.dev/visibility: cluster-local
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/minScale: "1"
spec:
containers:
- image: fn-simple-email-dev
workingDir: /app/functions/simple-email
command: ["pnpm", "dev"]
env:
- name: NODE_ENV
value: development
- name: CHOKIDAR_USEPOLLING
value: "true"
- name: WATCHPACK_POLLING
value: "true"
ports:
- containerPort: 8080
If your function listens on 3000, change containerPort. Knative generally expects your app to listen on $PORT (often 8080). If you can, have your server read process.env.PORT.
4) Hot sync into the right function pods (Tilt approach)
Tiltfile (one image per function; sync only its folder)
# Tiltfile
allow_k8s_contexts('kind-kind', 'docker-desktop')
FUNCTIONS = [
'simple-email',
'send-email-link',
# add more...
]
for fn in FUNCTIONS:
img = f"fn-{fn}-dev"
docker_build(
img,
context='.',
dockerfile='Dockerfile.dev',
live_update=[
# sync only this function’s code
sync(f'functions/{fn}', f'/app/functions/{fn}'),
# if you have shared libs, also sync them
sync('packages/shared', '/app/packages/shared'),
# restart process if needed (tsx/nodemon usually handles it;
# but this is a reliable fallback)
restart_container(),
],
)
k8s_yaml(f'k8s/knative/{fn}.yaml')
k8s_resource(fn) # resource name matches Knative Service metadata.name
Run:
Now the loop is:
- edit
functions/simple-email/src/...
- Tilt syncs only that folder into
/app/functions/simple-email
tsx watch / nodemon restarts immediately
kind vs Docker Desktop note
- Docker Desktop K8s: local images “just work”.
- kind: nodes use containerd; Tilt handles loading images into kind for you (that’s a big reason Tilt is pleasant here).
5) How to test (fast + realistic)
A) Unit tests (fastest)
Run locally with pnpm filters:
# single function
pnpm -C functions/simple-email test
# all functions
pnpm -r --filter ./functions/** test
If you want watch mode:
pnpm -C functions/simple-email vitest
B) Integration tests inside the cluster (best signal)
Because your Knative services are cluster-local, you can hit them by DNS:
http://simple-email.interweb.svc.cluster.local
Create a test Job that runs a Node test runner inside the cluster.
k8s/tests/integration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: integration-tests
namespace: interweb
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: tests
image: node:20-alpine
workingDir: /work
command: ["sh", "-lc"]
args:
- |
corepack enable
apk add --no-cache git
# simplest: bake a tests image instead of cloning
# but for local/dev, you can mount or clone
echo "TODO: run your integration tests here"
Better (what I recommend): build a small tests-runner image from your repo (same Dockerfile.dev pattern) that contains your tests/ package, then the Job just runs pnpm -C tests/integration test.
C) Quick manual “does it work” test
Run a curl pod in-cluster (no ingress/domain pain):
kubectl -n interweb run -it --rm curl \
--image=curlimages/curl --restart=Never -- \
sh -lc 'curl -i http://simple-email.interweb.svc.cluster.local'
6) Common gotchas with Knative hot reload
-
Scale-to-zero kills hot reload → set autoscaling.knative.dev/minScale: "1".
-
Port mismatch → make sure your app listens on $PORT (Knative sets it), or align containerPort.
-
File watching → use polling env vars if you don’t see reloads.
-
Shared code (packages/shared) → either:
- sync it into every function container (Tilt
sync('packages/shared', ...)), or
- accept rebuilds when shared changes.
If you paste (or describe) your current functions/ layout + how each Knative Service is generated (manual YAML vs your spec generator), I can adapt this into:
- an auto-discovered Tiltfile (no manual list),
- per-function sync rules (only
src/**, not node_modules),
- and a clean “tests runner” job/image that you can also reuse in CI with kind.
The mental model (what you’re aiming for)
Repo:
functions/<fn-name>/...Cluster: one Knative Service per function
Dev loop:
Testing:
http://<ksvc>.<ns>.svc.cluster.localFor many functions, I strongly prefer Tilt because you can script/loop resources easily.
1) Make each function “hot-reloadable” in a pod
Recommended repo layout
functions/<fn>/package.jsonMake sure each function has a
devscript that restarts on changes:JS
{ "scripts": { "dev": "nodemon --legacy-watch --watch src --ext js,json --exec node src/index.js", "test": "vitest run" }, "devDependencies": { "nodemon": "^3.0.0", "vitest": "^2.0.0" } }TS (nice default)
{ "scripts": { "dev": "tsx watch src/index.ts", "test": "vitest run" }, "devDependencies": { "tsx": "^4.0.0", "vitest": "^2.0.0" } }2) Use a dev Docker image that can run any function
Dockerfile.dev (monorepo-friendly, pnpm)
This image contains the whole repo so that live-sync can just overwrite files in-place.
3) Knative Service per function (dev-friendly)
Key points:
Example: k8s/knative/simple-email.yaml
4) Hot sync into the right function pods (Tilt approach)
Tiltfile (one image per function; sync only its folder)
Run:
Now the loop is:
functions/simple-email/src/.../app/functions/simple-emailtsx watch/nodemonrestarts immediatelykind vs Docker Desktop note
5) How to test (fast + realistic)
A) Unit tests (fastest)
Run locally with pnpm filters:
If you want watch mode:
B) Integration tests inside the cluster (best signal)
Because your Knative services are cluster-local, you can hit them by DNS:
http://simple-email.interweb.svc.cluster.localCreate a test Job that runs a Node test runner inside the cluster.
k8s/tests/integration-job.yaml
Better (what I recommend): build a small
tests-runnerimage from your repo (same Dockerfile.dev pattern) that contains yourtests/package, then the Job just runspnpm -C tests/integration test.C) Quick manual “does it work” test
Run a curl pod in-cluster (no ingress/domain pain):
kubectl -n interweb run -it --rm curl \ --image=curlimages/curl --restart=Never -- \ sh -lc 'curl -i http://simple-email.interweb.svc.cluster.local'6) Common gotchas with Knative hot reload
Scale-to-zero kills hot reload → set
autoscaling.knative.dev/minScale: "1".Port mismatch → make sure your app listens on
$PORT(Knative sets it), or align containerPort.File watching → use polling env vars if you don’t see reloads.
Shared code (
packages/shared) → either:sync('packages/shared', ...)), orIf you paste (or describe) your current
functions/layout + how each Knative Service is generated (manual YAML vs your spec generator), I can adapt this into:src/**, not node_modules),