The 10-Hour Journey to a 10-Second Solution: K8s Database Migrations
TL;DR
# The simple solution we ended up with:
kubectl -n mercury port-forward svc/postgres 15432:5432
DATABASE_URL="postgresql://mercury:mercury_secure_password_123@localhost:15432/mercury" pnpm migration:up:mercury
The Journey
What started as a seemingly simple task - "we need to run database migrations in Kubernetes" - turned into a 10-hour adventure through various approaches, each more complex than the last. Here's what we learned.
Attempt 1: The "Proper" Kubernetes Way
First, we tried to be "enterprise-grade" with a dedicated migrations container:
FROM node:20-slim
# ... tons of setup ...
COPY . .
CMD ["pnpm", "migration:up:mercury"]
Problems:
- Complex multi-stage builds
- Dependency management in monorepo
- TypeScript compilation issues
- Missing types and imports
- Large image size
Attempt 2: The "Dev Environment" Approach
Then we thought: "Let's just replicate our dev environment in a container!"
FROM node:20-slim
RUN apt-get update && apt-get install -y build-essential python3 git
# ... even more setup ...
Problems:
- Even larger image
- More complexity
- Still fighting with monorepo structure
- "It works on my machine" but in reverse
The Epiphany
After 10 hours, we had our "aha" moment:
- We already have a working development environment
- Kubernetes has built-in secure tunneling with
port-forward - Why not use what we know works?
The Solution
Two commands. That's it:
-
Create a secure tunnel to the database:
kubectl -n mercury port-forward svc/postgres 15432:5432 -
Run migrations from your local environment:
DATABASE_URL="postgresql://user:pass@localhost:15432/db" pnpm migration:up:mercury
Lessons Learned
- Start Simple: Don't begin with "enterprise patterns" - start with what you know works
- Use Built-in Tools: Kubernetes'
port-forwardis there for a reason - Local Development is Valid: Your local environment is often the best environment
- Security is Built-in:
port-forwardprovides a secure tunnel without exposing your database
Why This Matters
This journey exemplifies a common pattern in software development - the tendency to overcomplicate solutions before finding the elegant simple one. We went from:
- Custom Docker images
- Kubernetes Jobs
- Init containers
- Complex monorepo setups
- Multi-stage builds
To:
- Two commands
- Zero new infrastructure
- Existing tools
- Secure by default
Sometimes the best solution isn't about adding more - it's about realizing what you already have.
Bonus: Why Port 15432?
A small but important detail - we used port 15432 instead of 5432 because:
- Local Postgres often runs on 5432
- Avoiding port conflicts is easier than debugging them
- The explicit non-standard port reminds us we're working with a forwarded connection
Conclusion
Next time you're deep in the weeds of a "proper enterprise solution", take a step back and ask: "What's the simplest thing that could possibly work?"
The answer might just save you 9 hours and 50 minutes.
