Skip to content

Instantly share code, notes, and snippets.

@kylev
Last active September 21, 2022 00:43
Show Gist options
  • Save kylev/71b61acf72711042d03b4b374aeb05f2 to your computer and use it in GitHub Desktop.
Save kylev/71b61acf72711042d03b4b374aeb05f2 to your computer and use it in GitHub Desktop.
A complete Alpine docker-compose Rails workflow with PostgreSQL and Redis
version: "3"
services:
database:
image: postgres:11-alpine
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:5-alpine
volumes:
- redis-data:/data
web:
build:
context: .
args:
WITHOUT_BUNDLE: "yes"
environment:
REDIS_URL: redis://redis/
DB_HOST: database
ports:
- "3000:3000"
volumes:
- bundle-cache:/usr/local/bundle
- .:/opt/app
depends_on:
- database
- redis
volumes:
bundle-cache:
postgres-data:
redis-data:
FROM ruby:2.6-alpine
# Additional runtime facilities provided by the OS.
RUN apk add --no-cache libxml2 libxslt postgresql-libs tzdata
# Tell bundler how to use the OS.
RUN gem install bundler -v '~> 1.17' && \
bundle config build.nokogiri --use-system-libraries
WORKDIR /opt/app
RUN mkdir -p tmp/pids tmp/cache
ARG WITHOUT_BUNDLE="no"
ARG WITHOUT_GROUPS="development test"
COPY Gemfile Gemfile.lock ./
# A single layer of the bundle installation. If we perform the bundle
# install, it is assumed to be final, so we rip out the development
# libraries.
RUN apk add --no-cache --virtual .bundler-installdeps \
build-base git libxml2-dev libxslt-dev postgresql-dev && \
if [ $WITHOUT_BUNDLE = 'no' ]; then \
bundle install --jobs=3 --retry=3 --without=$WITHOUT_GROUPS && \
rm -rf /usr/local/bundle/cache && \
apk del .bundler-installdeps; \
fi
COPY . ./
ENTRYPOINT ["bundle", "exec"]
CMD ["rails", "server", "-b", "0.0.0.0"]
EXPOSE 3000
.PHONY: build build-release bundle bundle-install bundle-update console db-setup db-migrate \
down exec-shell logs up
default: bundle-install
build: # Build the docker image.
docker-compose build
build-release: # Build the docker image.
docker build --no-cache -t example-rails-app .
bundle: bundle-install # Alias of bundle-install
bundle-install: build # Install with bundler
docker-compose run --rm --entrypoint bundle web install --jobs=3 --retry=3
bundle-update: build # Update with bundler
docker-compose run --rm --entrypoint bundle web update
console: # Start a rails console
docker-compose run --rm web rails c
db-setup: # Run rake db:setup
docker-compose run --rm web rake db:setup
db-migrate: # Run rake db:migrate
docker-compose run --rm web rake db:migrate
down: # Bring all services down
docker-compose down
exec-shell: # Exec a shell in the running web container.
docker-compose exec web sh
logs: # Show the logs from the running containers.
docker-compose logs
up: # Bring all services up
docker-compose up -d
@kylev
Copy link
Author

kylev commented Jul 13, 2019

The aim of this gist is to emulate a rbenv-like environment using docker-compose. Its default docker build is highly optimized and small (138MB for my test rails 5.2 API project with nokogiri and pg gem dependencies).

I used make to replace the native rails, rake, and bundler workflow. If you were to check out a project set up like this, rather than checking rbenv had the right version and running bundle install, you'd simply run make bundle, which establishes the entire development stack in docker. From there you'd use some of the usual rake targets (via make db-seed et. al.). To bring everything up and start working, make up boots the app along side docker-compose managed Redis and PostgreSQL.

The centerpiece of this approach is the Dockerfile. By default (and the make target build-release) it will generate an exceptionally small, Alpine Linux based Docker image with only the necessary production bundler groups, ready for deployment on ECS, Kubernetes, Heroku, or any other Docker deployment system.

The central trick I learned about Alpine Linux while doing this was to install runtime requirements (like libxml2) separate from the development headers (in a package like libxml2-dev). I coalesced the adding development packages, bundle installing, and removal of the packages into a single RUN statement in order to to collapse the layer during the docker build. Leaving the development headers in place or removing them in a different layer will bloat the image by ~220MB.

I chose to add a little complexity to the Dockerfile via ARGs to facilitate happy development and testing. Driving this as a developer with make uses bundler steps in the Docker build: we skip gem installation via WITHOUT_BUNDLE and run the bundle install with the bundle-cache mounted where bundler will place the gems. This means that changes to the Gemfile don't require a slow "build the whole image and bundle from scratch" step. Instead, a quicker differential bundle install of what isn't already on bundle-cache takes a moments.

For CI or other non-deployment docker scenarios, passing --build-arg WITHOUT_GROUPS=none to a docker build will result in an "omnibus" whole-bundle build, ready to execute your test suite or be used in another developer workflow.

@michalg-
Copy link

That helps me a lot, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment