Ruby on Rails 언어별 가이드
Ruby on Rails 언어별 가이드
이 가이드는 Docker로 Ruby on Rails 어플리케이션을 컨테이너화하는 방법을 알려줘요.
출처: 문서
본문
이 Ruby 언어별 가이드는 Docker를 사용해 Ruby on Rails 어플리케이션을 컨테이너화하는 방법을 가르쳐줘요. 이 가이드에서 다음을 배우게 돼요:
- Ruby on Rails 어플리케이션 컨테이너화하고 실행하기
- 컨테이너를 사용해 Ruby on Rails 어플리케이션을 개발할 로컬 환경 구성하기
기존 Ruby on Rails 어플리케이션을 컨테이너화하는 것부터 시작해요.
Ruby on Rails 어플리케이션 컨테이너화하기
준비 사항 (Prerequisites)
- 최신 버전의 Docker Desktop을 설치했어야 해요.
- Git 클라이언트가 필요해요. 이 섹션의 예제는 Git CLI를 보여주지만, 어떤 클라이언트를 써도 무방해요.
개요 (Overview)
이 섹션에서는 Ruby on Rails 어플리케이션을 컨테이너화하고 실행하는 과정을 살펴봐요.
Rails 7.1부터 Docker가 기본 지원됩니다. 즉, 새 Rails 어플리케이션을 만들 때 Dockerfile, .dockerignore, bin/docker-entrypoint 파일이 자동으로 생성된다는 뜻이에요.
기존 Rails 어플리케이션이 있다면 아래 예제를 바탕으로 Docker 자산을 직접 만들어야 해요.
1. Docker 자산 만들기
Tip
Gordon, Docker의 AI 어시스턴트가 프로젝트에 맞는 Docker 자산을 생성해줄 수 있어요. Gordon에게 어플리케이션에 맞춘 Dockerfile, Compose 파일,
.dockerignore를 만들어 달라고 요청해보세요.
Rails 7.1 이상은 다단계(multistage) Dockerfile을 기본으로 생성해요. 아래는 그런 파일의 두 가지 버전이에요: 하나는 Docker Hardened Images(DHIs)를 사용하고, 다른 하나는 Docker Official Image(DOIs)를 사용해요. Dockerfile이 자동으로 생성되긴 하지만, 그 목적과 기능을 이해하는 것이 중요해요. 아래 예제를 꼭 검토해보길 권해요.
Docker Hardened Images (DHIs)는 Docker가 관리하는 최소화되고 안전하며 프로덕션 준비가 된 컨테이너 베이스·어플리케이션 이미지예요. 보안을 위해 가능하면 DHI를 권장해요. 취약점을 줄이고 컴플라이언스를 단순화하도록 설계됐으며, 구독 없이 누구나 자유롭게 사용할 수 있고 사용 제한도 없고 벤더 락인(vendor lock-in)도 없어요.
다단계 Dockerfile은 빌드 의존성과 런타임 의존성을 분리해 더 작고 효율적인 이미지를 만들도록 도와줘요. 최종 이미지에 필요한 구성 요소만 포함되도록 보장하죠. 자세한 내용은 Multi-stage builds guide에서 확인할 수 있어요.
Docker Hardened Images를 pull하려면 먼저 dhi.io에 인증해야 해요. docker login dhi.io를 실행해 인증하세요.
Dockerfile (DHI 버전)
# syntax=docker/dockerfile:1
# check=error=true
# This Dockerfile is designed for production, not development.
# docker build -t app .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name app app
# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.8
FROM dhi.io/ruby:$RUBY_VERSION-dev AS base
# Rails app lives here
WORKDIR /rails
# Install base packages
# Replace libpq-dev with sqlite3 if using SQLite, or libmysqlclient-dev if using MySQL
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Set production environment
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"
# Throw-away build stage to reduce size of final image
FROM base AS build
# Install packages needed to build gems
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential curl git pkg-config libyaml-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Install JavaScript dependencies and Node.js for asset compilation
#
# Uncomment the following lines if you are using NodeJS need to compile assets
#
# ARG NODE_VERSION=18.12.0
# ARG YARN_VERSION=1.22.19
# ENV PATH=/usr/local/node/bin:$PATH
# RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \
# /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \
# npm install -g yarn@$YARN_VERSION && \
# npm install -g mjml && \
# rm -rf /tmp/node-build-master
# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile --gemfile
# Install node modules
#
# Uncomment the following lines if you are using NodeJS need to compile assets
#
# COPY package.json yarn.lock ./
# RUN --mount=type=cache,id=yarn,target=/rails/.cache/yarn YARN_CACHE_FOLDER=/rails/.cache/yarn \
# yarn install --frozen-lockfile
# Copy application code
COPY . .
# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# Final stage for app image
FROM base
# Copy built artifacts: gems, application
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails
# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp
USER 1000:1000
# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]
Dockerfile (DOI 버전)
# syntax=docker/dockerfile:1
# check=error=true
# This Dockerfile is designed for production, not development.
# docker build -t app .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name app app
# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.8
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
# Rails app lives here
WORKDIR /rails
# Install base packages
# Replace libpq-dev with sqlite3 if using SQLite, or libmysqlclient-dev if using MySQL
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Set production environment
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"
# Throw-away build stage to reduce size of final image
FROM base AS build
# Install packages needed to build gems
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential curl git pkg-config libyaml-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Install JavaScript dependencies and Node.js for asset compilation
#
# Uncomment the following lines if you are using NodeJS need to compile assets
#
# ARG NODE_VERSION=18.12.0
# ARG YARN_VERSION=1.22.19
# ENV PATH=/usr/local/node/bin:$PATH
# RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \
# /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \
# npm install -g yarn@$YARN_VERSION && \
# npm install -g mjml && \
# rm -rf /tmp/node-build-master
# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile --gemfile
# Install node modules
#
# Uncomment the following lines if you are using NodeJS need to compile assets
#
# COPY package.json yarn.lock ./
# RUN --mount=type=cache,id=yarn,target=/rails/.cache/yarn YARN_CACHE_FOLDER=/rails/.cache/yarn \
# yarn install --frozen-lockfile
# Copy application code
COPY . .
# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# Final stage for app image
FROM base
# Copy built artifacts: gems, application
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails
# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp
USER 1000:1000
# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]
위 Dockerfile은 어플리케이션 서버로 Thruster를 Puma와 함께 사용한다고 가정해요. 다른 서버를 사용한다면 마지막 세 줄을 다음과 같이 바꾸면 돼요:
# Start the application server
EXPOSE 3000
CMD ["./bin/rails", "server"]
이 Dockerfile은 컨테이너의 entrypoint로 ./bin/docker-entrypoint 스크립트를 사용해요. 이 스크립트는 데이터베이스를 준비하고 어플리케이션 서버를 실행해요. 아래는 그런 스크립트의 예시예요.
docker-entrypoint
#!/bin/bash -e
# Enable jemalloc for reduced memory usage and latency.
if [ -z "${LD_PRELOAD+x}" ]; then
LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit)
export LD_PRELOAD
fi
# If running the rails server then create or migrate existing database
if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then
./bin/rails db:prepare
fi
exec "${@}"
위 두 파일 외에도 .dockerignore 파일이 필요해요. 이 파일은 빌드 컨텍스트에서 파일과 디렉토리를 제외하는 데 사용돼요. 아래는 .dockerignore 파일의 예시예요.
.dockerignore
# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files.
# Ignore git directory.
/.git/
/.gitignore
# Ignore bundler config.
/.bundle
# Ignore all environment files.
/.env*
# Ignore all default key files.
/config/master.key
/config/credentials/*.key
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
# Ignore pidfiles, but keep the directory.
/tmp/pids/*
!/tmp/pids/.keep
# Ignore storage (uploaded files in development and any SQLite databases).
/storage/*
!/storage/.keep
/tmp/storage/*
!/tmp/storage/.keep
# Ignore assets.
/node_modules/
/app/assets/builds/*
!/app/assets/builds/.keep
/public/assets
# Ignore CI service files.
/.github
# Ignore development files
/.devcontainer
# Ignore Docker-related files
/.dockerignore
/Dockerfile*
마지막으로 선택적으로 만들 수 있는 파일은 compose.yaml이에요. Docker Compose가 어플리케이션을 구성하는 서비스를 정의하는 데 사용하죠. SQLite를 데이터베이스로 사용하므로 데이터베이스만을 위한 별도 서비스를 정의할 필요는 없어요. 필요한 서비스는 Rails 어플리케이션 그 자체뿐이에요.
compose.yaml
services:
web:
build: .
environment:
- RAILS_MASTER_KEY
ports:
- "3000:80"
이제 어플리케이션 폴더에 다음 파일들이 있어야 해요:
.dockerignorecompose.yamlDockerfilebin/docker-entrypoint
파일에 대해 더 알아보려면 다음을 참고해요:
2. 어플리케이션 실행하기
어플리케이션을 실행하려면 어플리케이션 디렉토리 안의 터미널에서 다음 명령을 실행해요.
$ RAILS_MASTER_KEY=<master_key_value> docker compose up --build
브라우저를 열고 http://localhost:3000에서 어플리케이션을 확인해요. 간단한 Ruby on Rails 어플리케이션을 볼 수 있어요.
터미널에서 ctrl+c를 눌러 어플리케이션을 중지해요.
3. 어플리케이션을 백그라운드로 실행하기
-d 옵션을 추가하면 터미널에서 분리된 상태로 어플리케이션을 실행할 수 있어요. docker-ruby-on-rails 디렉토리 안에서 터미널에 다음 명령을 실행해요.
$ docker compose up --build -d
브라우저를 열고 http://localhost:3000에서 어플리케이션을 확인해요.
간단한 Ruby on Rails 어플리케이션을 볼 수 있어요.
터미널에서 다음 명령을 실행해 어플리케이션을 중지해요.
$ docker compose down
Compose 명령에 대한 자세한 내용은 Compose CLI reference를 참고해요.
Ruby on Rails 개발에 컨테이너 사용하기
준비 사항 (Prerequisites)
Ruby on Rails 어플리케이션 컨테이너화를 완료해요.
개요 (Overview)
이 섹션에서는 컨테이너화한 어플리케이션을 위한 개발 환경을 구성하는 방법을 배워요. 여기에는 다음이 포함돼요:
- 로컬 데이터베이스를 추가하고 데이터를 유지(Persist)하기
- 코드를 수정·저장할 때 실행 중인 Compose 서비스를 자동으로 갱신하도록 Compose 구성하기
로컬 데이터베이스 추가 및 데이터 유지
컨테이너를 사용해 데이터베이스 같은 로컬 서비스를 구성할 수 있어요. 이 섹션에서는 compose.yaml 파일을 수정해 데이터베이스 서비스와 데이터를 유지할 볼륨을 정의할 거예요.
클론한 저장소 디렉토리에서 compose.yaml 파일을 IDE나 텍스트 편집기로 열어요. server 서비스에 데이터베이스 비밀번호 파일을 환경 변수로 추가하고, 사용할 시크릿 파일을 지정해야 해요.
다음은 업데이트된 compose.yaml 파일이에요.
services:
web:
build: .
command: bundle exec rails s -b '0.0.0.0'
ports:
- "3000:3000"
depends_on:
- db
environment:
- RAILS_ENV=test
env_file: "webapp.env"
db:
image: postgres:18
secrets:
- db-password
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
volumes:
- postgres_data:/var/lib/postgresql
volumes:
postgres_data:
secrets:
db-password:
file: db/password.txt
Note
Compose 파일의 지시사항에 대해 더 알아보려면 Compose file reference를 참고해요.
Compose로 어플리케이션을 실행하기 전에, 이 Compose 파일이 데이터베이스의 비밀번호를 담을 password.txt 파일을 지정한다는 점에 주의해요. 이 파일은 소스 저장소에 포함돼 있지 않으므로 직접 만들어야 해요.
클론한 저장소 디렉토리에서 db라는 새 디렉토리를 만들고, 그 안에 데이터베이스 비밀번호를 담은 password.txt 파일을 만들어요. 좋아하는 IDE나 텍스트 편집기를 사용해 password.txt 파일에 다음 내용을 추가해요.
mysecretpassword
password.txt 파일을 저장하고 닫아요. 그리고 webapp.env 파일에서 데이터베이스에 연결할 비밀번호를 바꿀 수 있어요.
이제 docker-ruby-on-rails 디렉토리에 다음 내용이 있어야 해요.
.
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app/
├── bin/
├── compose.yaml
├── config/
├── config.ru
├── db/
│ ├── development.sqlite3
│ ├── migrate
│ ├── password.txt
│ ├── schema.rb
│ └── seeds.rb
├── lib/
├── log/
├── public/
├── storage/
├── test/
├── tmp/
└── vendor
이제 다음 docker compose up 명령을 실행해 어플리케이션을 시작해요.
$ docker compose up --build
Ruby on Rails에서 db:migrate는 데이터베이스에 마이그레이션을 실행하는 데 사용하는 Rake 태스크예요. 마이그레이션이란 시간이 지나면서 일관되고 쉽게 데이터베이스 스키마의 구조를 바꾸는 방법이에요.
$ docker exec -it docker-ruby-on-rails-web-1 rake db:migrate RAILS_ENV=test
다음과 유사한 메시지를 볼 수 있어요:
== 20240710193146 CreateWhales: migrating =====================================
-- create_table(:whales)
-> 0.0126s
== 20240710193146 CreateWhales: migrated (0.0127s) ============================
브라우저에서 http://localhost:3000을 새로고침하고 whales를 추가해요.
터미널에서 ctrl+c를 눌러 어플리케이션을 중지하고 docker compose up을 다시 실행하면 whales가 유지돼 있어요.
서비스 자동 업데이트
Compose Watch를 사용하면 코드를 수정·저장할 때 실행 중인 Compose 서비스를 자동으로 갱신할 수 있어요. Compose Watch에 대한 자세한 내용은 Use Compose Watch를 참고해요.
compose.yaml 파일을 IDE나 텍스트 편집기로 연 다음 Compose Watch 지시사항을 추가해요. 다음은 업데이트된 compose.yaml 파일이에요.
services:
web:
build: .
command: bundle exec rails s -b '0.0.0.0'
ports:
- "3000:3000"
depends_on:
- db
environment:
- RAILS_ENV=test
env_file: "webapp.env"
develop:
watch:
- action: rebuild
path: .
db:
image: postgres:18
secrets:
- db-password
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
volumes:
- postgres_data:/var/lib/postgresql
volumes:
postgres_data:
secrets:
db-password:
file: db/password.txt
다음 명령을 실행해 Compose Watch로 어플리케이션을 실행해요.
$ docker compose watch
로컬 머신의 어플리케이션 소스 파일에 가한 어떤 변경이든, 이제 실행 중인 컨테이너에 즉시 반영돼요.
docker-ruby-on-rails/app/views/whales/index.html.erb를 IDE나 텍스트 편집기로 열고 Whales 문자열에 느낌표를 추가해 수정해요.
- <h1>Whales</h1>
+ <h1>Whales!</h1>
index.html.erb의 변경 사항을 저장하고 몇 초 동안 어플리케이션이 다시 빌드될 때까지 기다려요. 다시 어플리케이션으로 가서 업데이트된 텍스트가 나타나는지 확인해요.
터미널에서 ctrl+c를 눌러 어플리케이션을 중지해요.
요약 (Summary)
이 섹션에서는 Compose 파일을 구성해 로컬 데이터베이스를 추가하고 데이터를 유지하는 방법을 살펴봤어요. 그리고 Compose Watch를 사용해 코드를 업데이트할 때 컨테이너를 자동으로 다시 빌드·실행하는 방법도 배웠어요.
관련 정보: