The Dockerfile is the cornerstone of your Docker setup. It defines the environment and steps needed to create a Docker image for your application.
# Stage 1: Build environment
# Use the latest Ruby base image
FROM ruby:3.3.1 AS builder
# Install NodeJS (using NodeSource) and Yarn
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& 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 \
&& apt-get update -qq \
&& apt-get install -y nodejs yarn build-essential libpq-dev
# Set the working directory inside the container
WORKDIR /app
# Copy the Gemfile and Gemfile.lock into the image
COPY Gemfile Gemfile.lock ./
# Install Ruby gems
RUN bundle install --jobs 4 --retry 3
# Copy the package.json and yarn.lock into the image
COPY package.json yarn.lock ./
# Install NodeJS dependencies using Yarn
RUN yarn install --check-files
# Copy the rest of the application code into the image
COPY . .
# Precompile Rails assets
RUN bundle exec rake assets:precompile
# Stage 2: Production environment
# Use a fresh Ruby base image for the production environment
FROM ruby:3.3.1
# Install runtime dependencies for NodeJS and Yarn
RUN apt-get update -qq && apt-get install -y nodejs yarn
# Set the working directory inside the container
WORKDIR /app
# Copy the application code from the build stage to the production stage
COPY --from=builder /app /app
# Expose the Rails server port
EXPOSE 3000
# Command to run the Rails server
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
-
Multi-Stage Builds:
- We use two stages to keep the final image clean and small. The first stage (
builder
) handles the heavy lifting: installing dependencies and precompiling assets. The second stage (production
) only includes what’s necessary to run the application.
- We use two stages to keep the final image clean and small. The first stage (
-
Base Image (
FROM ruby:3.3.1 AS builder
):- Start with the latest stable Ruby image. The
AS builder
alias is used for referencing this stage in the final production build.
- Start with the latest stable Ruby image. The
-
Install Dependencies (
RUN curl -fsSL...
):- NodeSource is used to install the latest NodeJS (version 18.x), which is suitable for most modern applications.
- Yarn is installed using its official repository. We also install
build-essential
andlibpq-dev
for building gems and PostgreSQL support.
-
Working Directory (
WORKDIR /app
):- Sets the working directory inside the container to
/app
. All subsequent commands will be run from this directory.
- Sets the working directory inside the container to
-
Copy Gemfile and Install Gems (
COPY Gemfile...
&RUN bundle install
):- Copying Gemfile and Gemfile.lock: Ensures that dependencies are cached if they haven’t changed, speeding up rebuilds.
- Bundle Install: Installs Ruby gems specified in the Gemfile.
-
Copy package.json and Install NodeJS Dependencies (
COPY package.json...
&RUN yarn install
):- Copying package.json and yarn.lock: Like Gemfile, this ensures dependencies are cached and reduces build times.
- Yarn Install: Installs NodeJS packages as defined in
package.json
.
-
Copy Application Code (
COPY . .
):- Copies the entire application codebase into the Docker image.
-
Precompile Assets (
RUN bundle exec rake assets:precompile
):- Precompiles Rails assets to prepare for production deployment. This step is crucial for reducing runtime workload.
-
Production Stage (
FROM ruby:3.3.1
):- Uses a fresh Ruby image to avoid carrying over unnecessary build tools and files from the build stage.
-
Install Runtime Dependencies (
RUN apt-get update...
):- Installs only the runtime dependencies needed for the application, keeping the image slim.
-
Copy Application from Builder Stage (
COPY --from=builder /app /app
):- Copies the precompiled and dependency-installed application from the builder stage to the production stage.
-
Expose Port (
EXPOSE 3000
):- Specifies that the container listens on port 3000, which is the default port for Rails.
-
Run Command (
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
):- Defines the command to start the Rails server, binding to all network interfaces (
0.0.0.0
).
- Defines the command to start the Rails server, binding to all network interfaces (
To manage multi-container applications, like a Rails app with a PostgreSQL database, use Docker Compose.
services:
db:
image: postgres:15
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: yourpassword
POSTGRES_USER: youruser
POSTGRES_DB: yourdatabase
web:
build: .
command: bundle exec rails server -b 0.0.0.0
volumes:
- .:/app
ports:
- "3000:3000"
depends_on:
- db
environment:
DATABASE_URL: postgres://youruser:yourpassword@db:5432/yourdatabase
RAILS_ENV: development
volumes:
db_data:
-
Services (
services
):- Defines the individual services that make up the application (e.g.,
db
andweb
).
- Defines the individual services that make up the application (e.g.,
-
Database Service (
db
):- Uses the latest PostgreSQL image (
postgres:15
). - Volumes: Maps the data directory in the container to a Docker volume (
db_data
) for data persistence. - Environment Variables: Sets database credentials and database name.
- Uses the latest PostgreSQL image (
-
Web Service (
web
):- Build: Specifies the context to build the Docker image from the Dockerfile in the current directory.
- Command: The command to run when the container starts, which starts the Rails server.
- Volumes: Maps the local directory to the container’s
/app
directory to enable live code reloading. - Ports: Maps port 3000 on the host to port 3000 on the container.
- Depends_on: Ensures the database service (
db
) starts before the web service (web
). - Environment Variables: Sets environment variables for Rails, including the
DATABASE_URL
for connecting to PostgreSQL andRAILS_ENV
to specify the environment.
-
Volumes (
volumes
):- Defines a Docker volume (
db_data
) for persistent storage of the database data.
- Defines a Docker volume (
For both Ruby (Bundler) and NodeJS (Yarn), managing private packages securely is crucial.
-
Configure Gemfile for Private Gems:
- Specify private gems in your
Gemfile
and configure credentials securely.
source 'https://rubygems.org' do gem 'public_gem' end source 'https://private-gems.example.com' do gem 'private_gem' end
- Specify private gems in your
-
Set Credentials:
- Use Bundler’s configuration to securely set credentials. Avoid hardcoding credentials in the
Dockerfile
orGemfile
.
bundle config set --local gems.example.com username:password
- In CI/CD environments, use environment variables to manage credentials.
BUNDLE_GEMS__EXAMPLE__COM=username:password
- Use Bundler’s configuration to securely set credentials. Avoid hardcoding credentials in the
-
Configure
.npmrc
for Private Packages:- Create or modify
.npmrc
in your project root to include authentication tokens for private packages.
//registry.npmjs.org/:_authToken=YOUR_NPM_TOKEN //npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
- Create or modify
-
Environment Variables for CI/CD:
- Use environment variables to inject tokens during the build process in CI/CD pipelines.
echo "//npm.pkg.github.com/:_authToken=${NPM_TOKEN}" > .npmrc
Integrating Docker with CI/CD pipelines requires careful handling of secrets and environment variables, especially when using SSH.
-
Local Development:
- You can use
--ssh default
to mount your local SSH agent during builds. This is helpful for accessing private repositories.
docker build --ssh default .
- You can use
-
CI/CD Pipelines:
-
Challenges: In CI/CD environments, you need to securely manage SSH keys and other secrets. Directly using
--ssh default
might not be feasible as it requires the presence of an SSH agent. -
Best Practices:
-
Secrets Management: Use the secrets management features of your CI/CD provider to store and inject SSH keys.
-
CI/CD Example with GitHub Actions:
jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Setup SSH for Private Repositories uses: webfactory/ssh-agent@v0.5.3 with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - name: Build Docker image run: docker build --ssh default .
-
Use
DOCKER_BUILDKIT=1
: Ensure Docker Buildkit is enabled to use advanced features like SSH forwarding.
export DOCKER_BUILDKIT=1
-
-
The Gemfile.lock
file can sometimes cause issues when moving between different operating systems, like macOS and Linux.
-
Platform-Specific Lock Files:
- If you encounter issues with the
Gemfile.lock
file, it’s often best to regenerate it within the Docker container to ensure compatibility.
docker run --rm -v $(pwd):/app -w /app ruby:3.3.1 bundle install
- If you encounter issues with the
-
Include
Gemfile.lock
in Your Docker Image:- Always include the
Gemfile.lock
in your Docker image to ensure consistent and reproducible builds.
COPY Gemfile Gemfile.lock ./ RUN bundle install
- Always include the
By following these detailed steps, you can set up a robust and modern Docker environment for a Ruby on Rails application with NodeJS and Yarn. This guide covers the essentials of Docker, Docker Compose, handling private packages, and integrating Docker into CI/CD pipelines, ensuring your setup is efficient, secure, and ready for production use.
- Docker Documentation
- Docker Compose Documentation
- Bundler Documentation
- Yarn Documentation
- GitHub Actions Documentation
This comprehensive guide should serve as a solid reference for setting up and managing Dockerized Ruby on Rails applications in 2024.