Running the Production Dockerfile Locally with Tilt
I do not like maintaining two versions of an app setup.
One path for local development, another path for production, then a third set of notes explaining why they are different. That is where the usual “works on my machine” bug comes from.
Tilt helps by running the real Dockerfile locally, then syncing code changes into the running container.
Why I reach for Tilt
This is the common split:
local: npm run dev
production: Dockerfile + Kubernetes That means different Node versions, different system packages, different file paths, and sometimes different permissions.
The app can pass locally and still fail in the image.
Start with the Tiltfile
The first version can stay small:
# Tiltfile
docker_build('my-app', '.', dockerfile='Dockerfile')
k8s_yaml('k8s/deployment.yaml')
k8s_resource('my-app', port_forwards='3000:3000') Then run:
tilt up Your app runs locally through the same image path production uses.
Add live updates
Rebuilding the full Docker image on every save is too slow.
The useful part is live_update:
docker_build(
'my-app',
'.',
dockerfile='Dockerfile',
live_update=[
sync('./src', '/app/src'),
run('npm install', trigger=['package.json', 'package-lock.json']),
]
) Now normal source changes sync into the container. Dependency changes still run the install step.
A small full example
# Tiltfile
k8s_yaml('k8s/postgres.yaml')
k8s_yaml('k8s/redis.yaml')
docker_build(
'my-app',
'.',
dockerfile='Dockerfile',
live_update=[
sync('./src', '/app/src'),
run('npm install', trigger=['package.json', 'package-lock.json']),
]
)
k8s_yaml('k8s/deployment.yaml')
k8s_resource(
'my-app',
port_forwards='3000:3000',
resource_deps=['postgres', 'redis']
) The Dockerfile stays the source of truth:
FROM node:20-alpine
RUN apk add --no-cache python3 make g++
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "src/index.js"] Bugs this finds earlier
This is where the setup pays off for me:
- Missing Alpine packages.
- File path casing that works on macOS but fails on Linux.
- Permission problems from
USER node. - Lockfile problems from
npm ci. - Environment assumptions hidden in shell profiles.
That is the main reason I use it.
The tradeoff
Tilt is not as instant as a pure framework dev server.
For a small UI-only project, npm run dev is simpler. I reach for Tilt when the app depends on the production image, native packages, Kubernetes resources, or multiple services.
What I run
brew install tilt-dev/tap/tilt
tilt up After that, every system package goes into the Dockerfile first.
If production needs a package, I add it there. Local development follows the same path.