3 min read
A New Machine Needs Docker and Nothing Else
The development loop runs in one container
Part eight of the boring stack. The app is Go, with templ and Tailwind generating code and CSS. That’s three toolchains before you write a line. This post is about how I keep them off my machine.
Every project I build has one supported way to run it in development:
task dev
That runs docker compose up --build. Then I open the app in the browser and start working.
What runs in the container
The development image contains Go, Node (only for the Tailwind command-line tool) and everything else the project needs. The project folder is mounted into the container, so I edit files on my machine with my usual editor.
When the container starts, it:
- installs the npm dependencies if they’re missing,
- builds the CSS once,
- starts the Tailwind watcher in the background,
- runs
templ generate --watch, which regenerates Go code when a template changes, restarts the server and reloads the browser through a small proxy.
Change a template and the page reloads. Change a Go file and the server restarts. Change a class and the CSS rebuilds.
Why only one way
The point isn’t Docker. The point is that there’s exactly one way, and it’s written down in the repository.
- A new machine is ready in minutes. Install Docker, clone, run
task dev. No Go version manager, no Node version manager, no “works on my machine”. - Versions come from the project. Go’s version is pinned in
go.mod, the tools are pinned with Go’stooldirective, and npm packages come from the lock file. My machine’s versions don’t matter. - A project I haven’t touched for a year still starts. The container has the toolchain the project expects, not whatever I’ve upgraded to since.
Details that matter
- Caches in named volumes. Go’s module and build caches live in Docker volumes, so they survive
docker compose downand the second start is fast. node_modulesnever touches the host. It contains platform-specific binaries. A folder built for Linux in the container breaks on macOS and the other way round. A dedicated volume keeps it inside the container.- Generated files are committed. The Go code templ generates is in the repository, so
go buildandgo testalso work directly on the host if Go happens to be installed. The container is the supported way, not the only possible one.
The same commands everywhere
A Taskfile.yml holds four tasks, identical across all my projects: dev, test, lint, down. Whichever project I open, the commands are the same. The linter runs in CI before anything is built, so a lint error stops a deploy.
What it costs
- Docker Desktop on macOS uses memory and battery. It’s noticeable on a laptop.
- File watching through a mount is slower than native. For the size of my projects, it’s not a problem.
- One more layer when something breaks. Occasionally the question is “is it my code or the container?” The answer is almost always my code.
When I’d choose differently
For a single small Go service without templates or CSS, go run is enough and Docker would be overhead. As soon as a second toolchain joins, the container pays for itself.
Next: where it all runs.