How should BT support Docker?
People occasionally (frequently?) ask about using Docker with Bullet Train, but we don't currently have any official stance or support.
I think that when people ask about Docker that they don't always mean the same thing. I think they sometimes mean that they want to use Docker for one (or more) of:
- Running just the BT starter repo in development mode
- locally
- in a cloud-based development environment
- Running the BT starter repo and supporting services in development mode via Docker compose
- locally
- in a cloud-based development environment
- CI/CD pipelines based on docker
- Deploying a BT app to production (and other environments)
- Heroku
- Render
- Fly
- Kubernetes
- Kamal
- Etc...
- Are there other ways that people want to use Docker?
I've used Docker enough to be able to use it when I have to, but it's not really a standard part of my go-to toolkit, so I don't have the best feel for how to navigate all of these various ways that people use Docker in these different situations.
So here are some possibly naive questions aimed at getting an idea of how we should approach Docker support:
- Are there ways that people want to use Docker not listed above?
- Can a single
Dockerfileeven theoretically cover all of the various use cases?- If so, would we have to do a bunch of conditionals that would end up making that
Dockerfilepretty gnarly and hard to read?
- If so, would we have to do a bunch of conditionals that would end up making that
- Are there reasons that it would be better to have multiple
Dockerfiles? LikeDockerfile.devandDockerfile.ciandDockerfile.production(orDockerfile.kamalor whatever). - Would using Docker compose to provide dependent services make a difference for what has to happen in the
Dockerfilefor the app? - Are there questions here that I don't even know to ask?
Some existing PRs that involve one or more Dockerfiles:
https://github.com/bullet-train-co/bullet_train/pull/1994 - Add Kamal to template repo
https://github.com/bullet-train-co/bullet_train/pull/1620 - Add Devcontainer Support
https://github.com/bullet-train-co/bullet_train/pull/464 - Dockerized development setup
https://github.com/bullet-train-co/bullet_train/pull/1720 - Update app setup to use docker for postgres/redis
I think that support for most if not all of these use cases can be built up in phases:
Phases
- Development Dockerfile: This will include lots of libraries and tools to support development. This would allow local building of a container that could run the starter repo but would require setting some environment variables on the container to point it external services (presumably already running on the developer's machine) for things like Postgres and Redis. This kind of setup generally uses a volume mount to expose the live filesystem with the local codebase into the running container so that you can edit locally and have a live-reload loop running inside the container. Now your dev machine simply requires Docker and the external services (db, redis, etc). This image can also be used in cloud-based development environments like Github Codespaces, etc.
- Docker Compose: Next we can add a Docker Compose file that references our Development Dockerfile for the main container and also spins up the required supporting services like Postgres and Redis in their own containers that are all linked together. Now your dev machine just needs Docker to get the starter repo up an running.
- Devcontainers: Can simply build on-top of the Dockerfile / Docker Compose setup
- Production Dockerfile: This will be used to build the slimmed down, hardened image of the application for production deployments. It won't include dev tools just the required runtimes and fully built code. This will be suitable for deployment anywhere that a Docker image can be deployed (Kubernetes, Kamal, Heroku, Render, etc)
Organization
When it comes to organizing multiple Dockerfiles, I generally thing that you should optimize for developer experience. As such, I would put the Development Docker file at the root of the repo and name it Dockerfile. This is the default location that most Docker-related tools will look to if you don't specify a Dockerfile in some other way. Other Dockerfiles like the Production Dockerfile can be named Dockerfile.production to clearly indicated their purpose. Since building for something other than local development is generally a very intentional (and usually automated) process, passing in the name of the specific Dockerfile that you want to use is no big deal.
CI Pipelines BT should not have any Dockerfiles specific to CI pipelines. In a Docker-based CI system, the container used in a step that builds the BT Dockerfile or executes any other build steps should be separately maintained as the requirements for image may vary wildly between CI systems.
Just some initial thoughts.
Thanks for the input on this, @jeff-french! I think that all makes sense, and definitely feels like the general shape of the approach that I had in mind, but with much more detail.
For the local dev experience do you usually use some convenience scripts to help with launching the container whether it's stand alone or via compose? Maybe something like bin/dev-docker or something?
Last time I was doing much in the way of running Docker containers locally (which was several years ago) I remember really fighting the MacOS filesystem to get anything better than absolutely miserable performance, and I never did get it to anything approaching "good". Have you ever run into that, and/or has the situation improved?
And do you have any thoughts about how we select base images? I think as far as is possible we'd want to stick to well-known and trusted container registries. And as far as is possible let the base image do some of the heavy lifting about installing run times and what not.
For the local dev experience do you usually use some convenience scripts to help with launching the container whether it's stand alone or via compose? Maybe something like bin/dev-docker or something?
I think a bin script is a good idea. Since there are likely to be a couple of different modes to be used for local dev, having a bin script with a couple of preset modes of operation would be good (e.g. full stack mode, BT only with local DBs mode, etc.). I think this warrants a separate discussion about whether it should be its own "docker" script or just baked into the bin/dev script (e.g. bin/docker full, bin/docker standalone, or bin/dev docker, bin/dev docker-standalone).
Last time I was doing much in the way of running Docker containers locally (which was several years ago) I remember really fighting the MacOS filesystem to get anything better than absolutely miserable performance, and I never did get it to anything approaching "good". Have you ever run into that, and/or has the situation improved?
TBH I haven't used Docker in this way in quite some either. I know there has been some effort in the last couple of years to improve the performance of Docker on Mac by utilizing Apple's new virtualization system and VirtioFS. These improvements should make performance a non-issue.
And do you have any thoughts about how we select base images? I think as far as is possible we'd want to stick to well-known and trusted container registries. And as far as is possible let the base image do some of the heavy lifting about installing run times and what not.
I'd explore two options here:
- Mimic / lean on the default Dockerfile that Rails 7.1+ will generate. Doing it the "rails way" is usually a good starting point. This would mean just using the official ruby-slim base images which means only ruby is going to be loaded for you and we'll need to have our dockerfile install all the dependencies that BT needs.
- Publish official Bullet Train base images. For this, we would spin up a new BT repo that would hold Dockerfiles and scripts to build and publish a variety of Bullet Train docker images (e.g.
bullet_train/bullet_train,bullet_train/bullet_train-dev,bullet_train/bulet_train-devcontainer). This would allow us to keep the Dockerfile shipped with the starter repo really short and easy to maintain and version. This would also make it easier for consumers of the starter repo to merge in upgrades since the Dockerfile would have very few lines by default. I expect that downstream consumers will often find a need to add their own deps to their Dockerfile so the less lines that the upstream file has in it the better. This would be my recommended approach and I'd be happy to help get the new repo setup and get the first cut of base images developed.
Either way, I generally recommend against choosing a "fat" base image. Choosing an official base image and tailoring the structure and content of your image usually yields the best results (no unused deps, slimmer image, better security). Choosing a base image is like choosing a gem or npm package: you need to trust the upstream publisher to not do anything silly or allow their supply chain to be compromised.
The idea of publishing our own base images sounds interesting. It would definitely be nice to abstract out as much of that kind of thing as possible. 🤔
So, if we published our own image that would mean that the Dockerfile that we ship with the starter repo might be as simple as:
FROM bullet_train/bullet_train:v1.22
COPY Gemfile Gemfile.lock .
RUN bundle install --quiet --jobs 4
COPY . .
CMD ["sh", "-c", "./bin/rails server"]
So, if we published our own image that would mean that the
Dockerfilethat we ship with the starter repo might be as simple as:FROM bullet_train/bullet_train:v1.22 COPY Gemfile Gemfile.lock . RUN bundle install --quiet --jobs 4 COPY . . CMD ["sh", "-c", "./bin/rails server"]
Where can I buy this for the Kamal PR? :D
We added initial Docker support in https://github.com/bullet-train-co/bullet_train/pull/2102.
We haven't done anything for docker-compose yet, so I'm going to leave this open for now.