Streamlining Email Integration
Why We Built Mailsnag At Mailsnag, we embarked on a mission to simplify the intricate process of managing SMTP …
As we are scaling our applications, we wanted to make sure that they are running as efficiently
as possible. This week we wanted to share with you our optimized Dockerfile
for building Ruby on Rails apps
with YJIT
, jemalloc
and bootsnap
enabled. This Dockerfile
will include libvips
for ActiveStorage
, postgresql-client
and redis-tools
for interacting with PostgreSQL and Redis.
Dockerfile
# Create base image
FROM ruby:3.2-slim-bookworm AS base
# Set ENV variables
ENV RAILS_ENV=production \
RACK_ENV=production \
NODE_ENV=production \
APP_ENV=production \
RAILS_LOG_TO_STDOUT=true \
RAILS_MAX_THREADS=10 \
RAILS_SERVE_STATIC_FILES=true
ENV GEM_HOME="/usr/local/bundle" \
BUNDLE_WITHOUT="development:test" \
BUNDLE_FROZEN="1" \
BUNDLE_JOBS="32"
ENV PATH $GEM_HOME/bin:$GEM_HOME/gems/bin:$PATH
WORKDIR /app
RUN apt-get update -qq && \
apt-get upgrade -y --no-install-recommends
RUN apt-get -y --no-install-recommends install zip gnupg tzdata curl wget libjemalloc2 libvips \
apt-transport-https apt-utils ca-certificates postgresql-client redis-tools
# Update gems and bundler
RUN gem update --system --no-document
# Clean cache
RUN apt-get clean && rm -f /var/lib/apt/lists/*_*;
RUN rm -rf /root/.local
# Create setup image
FROM base AS setup
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash -
RUN apt-get update -qq
RUN apt-get install --no-install-recommends -y build-essential libpq-dev zlib1g-dev libssl-dev libreadline-dev \
libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt-dev libcurl4-openssl-dev libffi-dev pkg-config dirmngr \
git-core nodejs yarn libvips-dev python-is-python3
# Install application gems
COPY Gemfile* ./
RUN gem install bundler --no-document
RUN bundle install
# Install JS libs
COPY package.json *yarn* ./
RUN yarn install --frozen-lockfile
# Copy application files
COPY . .
# Build assets
RUN ./bin/rails assets:precompile
# Remove extra files
RUN rm -rf node_modules
RUN rm -rf /usr/local/bundle/cache/*
# Create runtime image
FROM base AS runtime
ENV LD_PRELOAD="libjemalloc.so.2" \
MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:5000,muzzy_decay_ms:5000,narenas:2" \
RUBY_YJIT_ENABLE="1"
# Copy built artifacts: gems, application
COPY --from=setup /usr/local/bundle /usr/local/bundle
COPY --from=setup /app /app
# Precompile bootsnap code for faster boot times
RUN rm -rf /app/tmp/cache
RUN bundle exec bootsnap precompile --gemfile app/ lib/
# Initialize entrypoint
EXPOSE 3000
CMD ["./bin/rails", "server", "-p", "3000"]
This Dockerfile sets up a container for a Ruby on Rails application with specific environment variables and settings. Let’s break it down:
This stage is used to install all the dependencies required when running the application. It is also used as a base
image
for the setup
stage.
1. Base Image:
ruby:3.2-slim-bookworm
image as the base image. This means your application will run on top of the Ruby 3.2
version with a slim
variant that includes fewer pre-installed packages, making the image smaller and more efficient.2. Environment Variables:
RAILS_ENV
, RACK_ENV
, NODE_ENV
, and APP_ENV
are all set to production
. This ensures that the application runs
in a production environment, which typically involves optimizations and different settings compared to development.RAILS_LOG_TO_STDOUT
is set to true
, indicating that Rails should direct its log output to the standard output (
stdout) of the container. This is often useful for logging purposes when working with containers.RAILS_MAX_THREADS
is set to 10
, limiting the number of concurrent threads the Rails application can handle.RAILS_SERVE_STATIC_FILES
is set to true
, which enables the Rails application to serve static files directly.GEM_HOME
is set to /usr/local/bundle
, specifying where Ruby gems will be installed within the container.BUNDLE_WITHOUT
is set to development:test
, indicating that development and test dependencies should not be
installed by Bundler.BUNDLE_FROZEN
is set to 1
, which means that the bundle will not be modified after it is initially installed.BUNDLE_JOBS
is set to 32
, specifying the maximum number of parallel jobs that Bundler can use during installation.PATH
environment variable is modified to include the bin
directories of the Ruby gems and bundle. This ensures
that the commands provided by the gems and Bundler are accessible from the command line.3. Working Directory:
WORKDIR /app
sets the working directory within the container to /app
. This is where subsequent commands will be
executed.4. Package Installation:
apt-get update -qq
updates the package list on the system.apt-get upgrade -y --no-install-recommends
upgrades existing packages without installing recommended additional
packages.apt-get -y --no-install-recommends install
installs various packages:zip
: Utility for creating and managing ZIP archives.gnupg
: GNU Privacy Guard, used for encryption and digital signatures.tzdata
: Time zone data for system configuration.curl
and wget
: Tools for making HTTP requests.libjemalloc2
: Memory allocator.libvips
: Image processing library.apt-transport-https
and apt-utils
: Utilities for handling HTTPS-based APT repositories.ca-certificates
: Certificate authorities for secure communication.postgresql-client
: PostgreSQL client tools.redis-tools
: Redis client tools.5. Rubygem Update:
gem update --system --no-document
updates the RubyGems system to the latest version without generating
documentation.6. Cleanup:
apt-get clean && rm -f /var/lib/apt/lists/*_*;
cleans the package cache and removes specific files to reduce the
image size.rm -rf /root/.local
removes a directory related to local user settings used by gem install
.This stage is used to install all the dependencies required when building the application. It will also install all the gems and JS libraries required by the application, and precompile the assets.
1. New Build Stage:
FROM base AS setup
creates a new build stage named setup
based on the previously defined base
stage. This helps
to separate the setup-specific steps from the base image configuration.2. Adding Yarn Repository:
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
imports the Yarn package manager’s GPG key for
secure installation.echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
adds the Yarn
repository to the package sources.3. Adding Node.js Repository:
curl -sL https://deb.nodesource.com/setup_18.x | bash -
fetches the Node.js repository setup script and runs it,
adding the Node.js repository to the package sources.4. Updating and Installing Dependencies:
apt-get update -qq updates
the package list.apt-get install --no-install-recommends -y
installs various development libraries and tools, including:build-essential
: Essential build tools like compilers and make
.libpq-dev
: Development files for PostgreSQL.zlib1g-dev
, libssl-dev
, libreadline-dev
, libyaml-dev
, libsqlite3-dev
: Libraries needed for various aspects
of application development.libxml2-dev
, libxslt-dev
, libcurl4-openssl-dev
, libffi-dev
, pkg-config
: Additional libraries for XML,
XSLT, curl, and FFI.dirmngr
: DNS resolver for GnuPG.git-core
: Version control system.nodejs
: Node.js runtime.yarn
: Yarn package manager.libvips-dev
: Development files for the libvips
image processing library.python-is-python3
: Ensures python command refers to Python 3.5. Copying Gemfile and Installing Gems:
COPY Gemfile* ./
copies the Gemfile
and Gemfile.lock
from the host to the current directory in the container.gem install bundler --no-document
installs Bundler without generating documentation.bundle install
installs the application’s Ruby gems based on the Gemfile
.6. Copying package.json and Installing JavaScript Dependencies:
7. Copying Application Files:
COPY . .
copies all files and directories from the host to the current directory in the container. This includes the
entire application codebase.8. Building Assets:
./bin/rails assets:precompile
runs the Rails task to precompile assets. This process involves transforming and
bundling CSS, JavaScript, and other assets for production use.9. Removing Extra Files:
rm -rf node_modules
removes the node_modules directory, which contains JavaScript dependencies installed by Yarn.
These are no longer needed after assets are precompiled.rm -rf /usr/local/bundle/cache/*
cleans up the cache directory used by Bundler to store gem cache files.This final part of the Dockerfile
sets up the runtime environment for the application and configures the container to
run the application:
1. New Build Stage - Runtime:
FROM base AS runtime
creates a new build stage named runtime
based on the base
stage. This stage is focused on
preparing the runtime environment for the application.2. Environment Variables:
ENV LD_PRELOAD="libjemalloc.so.2"
sets the LD_PRELOAD
environment variable to preload the libjemalloc
memory
allocator library.MALLOC_CONF
configures various options for the memory allocator.RUBY_YJIT_ENABLE
enables the YJIT
Just-In-Time compiler for Ruby.3. Copying Built Artifacts:
COPY --from=setup /usr/local/bundle /usr/local/bundle
copies the installed Ruby gems from the setup
stage to the
same location in the runtime
stage.COPY --from=setup /app /app
copies the application files from the setup
stage to the same location in
the runtime
stage.4. Precompiling Bootsnap Code:
5. Initializing Entrypoint:
EXPOSE 3000
specifies that the application inside the container will be accessible on port 3000
.CMD ["./bin/rails", "server", "-p", "3000"]
defines the default command that will be executed when the container
starts. It starts the Rails server on port 3000
.In the end we have a docker image that is ready to be deployed to production. We tested if YJIT
and jemalloc
are
enabled by running:
$ docker run -it --rm -e "MALLOC_CONF=stats_print:true" <image_name> ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) +YJIT [aarch64-linux]
___ Begin jemalloc statistics ___
We verified that +YJIT
is present in the output as well as jemalloc
stats. Hope this helps you to get started with
running YJIT
and jemalloc
in production.
Until next time amigos 🙌
Why We Built Mailsnag At Mailsnag, we embarked on a mission to simplify the intricate process of managing SMTP …
Introduction Emails have become an essential means of communication in both personal and professional settings. But have …
No credit card required to sign up. All paid plans come with 30 day cancellation and full refund.
Get Started for Free