Skip to content

Instantly share code, notes, and snippets.

@loftwah
Last active June 21, 2024 12:27
Show Gist options
  • Save loftwah/30911b3a41ca20b77e8ee54280c34714 to your computer and use it in GitHub Desktop.
Save loftwah/30911b3a41ca20b77e8ee54280c34714 to your computer and use it in GitHub Desktop.
Docker Ruby in 2024

Docker and Dockerfile Guide for Ruby on Rails with NodeJS and Yarn

Step-by-Step Instructions

1. Setting Up the Dockerfile

The Dockerfile is the cornerstone of your Docker setup. It defines the environment and steps needed to create a Docker image for your application.

Dockerfile for Ruby on Rails with NodeJS and Yarn

# 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"]

Dockerfile Breakdown

  1. 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.
  2. 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.
  3. 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 and libpq-dev for building gems and PostgreSQL support.
  4. Working Directory (WORKDIR /app):

    • Sets the working directory inside the container to /app. All subsequent commands will be run from this directory.
  5. 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.
  6. 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.
  7. Copy Application Code (COPY . .):

    • Copies the entire application codebase into the Docker image.
  8. Precompile Assets (RUN bundle exec rake assets:precompile):

    • Precompiles Rails assets to prepare for production deployment. This step is crucial for reducing runtime workload.
  9. 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.
  10. Install Runtime Dependencies (RUN apt-get update...):

    • Installs only the runtime dependencies needed for the application, keeping the image slim.
  11. 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.
  12. Expose Port (EXPOSE 3000):

    • Specifies that the container listens on port 3000, which is the default port for Rails.
  13. 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).

2. Docker Compose Configuration

To manage multi-container applications, like a Rails app with a PostgreSQL database, use Docker Compose.

Example docker-compose.yml for Rails and PostgreSQL

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:

docker-compose.yml Breakdown

  1. Services (services):

    • Defines the individual services that make up the application (e.g., db and web).
  2. 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.
  3. 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 and RAILS_ENV to specify the environment.
  4. Volumes (volumes):

    • Defines a Docker volume (db_data) for persistent storage of the database data.

3. Handling Private Packages

For both Ruby (Bundler) and NodeJS (Yarn), managing private packages securely is crucial.

Bundler (Ruby)

  1. 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
  2. Set Credentials:

    • Use Bundler’s configuration to securely set credentials. Avoid hardcoding credentials in the Dockerfile or Gemfile.
    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

Yarn (NodeJS)

  1. 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
    
  2. 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

4. CI/CD Considerations

Integrating Docker with CI/CD pipelines requires careful handling of secrets and environment variables, especially when using SSH.

Using --ssh default in CI/CD

  • 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 .
  • 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

5. Managing Gemfile.lock Across Environments

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
  • 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

Summary

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.

Resources for Further Reading

This comprehensive guide should serve as a solid reference for setting up and managing Dockerized Ruby on Rails applications in 2024.

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