Ruby #
Ruby sering dicap sebagai bahasa yang “berat” di Docker. Image Ruby identik dengan ratusan MB, banyak OS package, dan build yang lama. Padahal, Ruby bisa dibuat sangat ramping dan production-grade jika diperlakukan dengan disiplin yang sama seperti Java (distroless), Node.js (bundling), dan Python (build/runtime split).
Artikel ini membahas strategi Docker image Ruby yang kecil, aman, dan siap production, dengan fokus pada Rails (paling umum) dan pola umum yang berlaku untuk semua framework Ruby (Sinatra, Hanami, roda, dll).
1. Realita Ukuran Image Ruby #
| Setup | Ukuran Image |
|---|---|
ruby:latest + bundle install (semua) |
700-900 MB |
ruby:slim tanpa optimasi |
400-600 MB |
ruby:slim + multi-stage + cleanup |
180-250 MB |
ruby:alpine + multi-stage |
120-180 MB |
| Distroless + custom Ruby | 80-130 MB |
Insight: Selisih antara 900 MB dan 80 MB adalah lebih dari 10x lipat. Image Ruby yang ramping sangat mungkin dengan Dockerfile yang disiplin.
2. Kenapa Image Ruby Mudah Membengkak? #
Ruby Adalah Runtime + Toolchain #
Ruby interpreter itu sendiri sudah cukup besar (~50-100 MB). Ditambah Bundler, RubyGems, dan tool yang di-install otomatis. Tanpa pemisahan build vs runtime, semua tool ini masuk image akhir.
Native Gem #
Ruby punya banyak gem dengan native extension (C extension yang di-compile saat install):
nokogiri— XML/HTML parser (binding ke libxml2).pg— PostgreSQL driver.grpc— gRPC client/server.mysql2— MySQL driver.rdiscount— Markdown parser.rugged— Git library binding.eventmachine— network library.json(C implementation) — JSON parser.
Setiap native gem menarik compiler (gcc, make) dan library development (*-dev). Tanpa multi-stage, semua ini masuk runtime.
Rails Membawa Dunia Sendiri #
Rails (full stack) menarik banyak gem:
- ActiveRecord (ORM).
- ActionCable (WebSocket).
- ActiveStorage (file upload).
- ActionMailer (email).
- ActionText (rich text).
- ActionMailbox (incoming email).
- Bootsnap (caching).
- Image processing (binding ke libvips).
- Puma (web server).
- Rack (web server interface).
Kombinasi ini dengan mudah menghasilkan image 500+ MB jika tidak disiplin.
3. Prinsip Utama: Runtime Image Hanya Interpreter + Production Gem #
Runtime image Ruby idealnya hanya berisi: Ruby interpreter + production gem + application code.
Titik. Tidak ada Bundler, tidak ada compiler, tidak ada header library, tidak ada gem documentation, tidak ada test framework.
4. Strategi Multi-Stage Build #
4.1 Pola Dasar: Slim + Bundler #
# syntax=docker/dockerfile:1.7
# ==== Stage 1: Build ====
FROM ruby:3.3.4-slim-bookworm AS builder
WORKDIR /app
# Install build dependency
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
libxml2-dev \
libxslt1-dev \
libffi-dev \
libyaml-dev \
libssl-dev \
libvips-dev \
nodejs \
yarn \
&& rm -rf /var/lib/apt/lists/*
# Install gem
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local without 'development test' \
&& bundle config set --local deployment 'true' \
&& bundle config set --local path 'vendor/bundle' \
&& bundle install --jobs 4
# Pre-compile assets dan bootsnap
COPY . .
RUN SECRET_KEY_BASE=dummy \
RAILS_ENV=production \
bundle exec rails assets:precompile \
&& bundle exec bootsnap precompile --gemfile
# ==== Stage 2: Runtime ====
FROM ruby:3.3.4-slim-bookworm
WORKDIR /app
# Install runtime dependency
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
libxml2 \
libxslt1.1 \
libffi8 \
libyaml-0-2 \
libssl3 \
libvips42 \
tzdata \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy gem dari build stage
COPY --from=builder /app/vendor/bundle /app/vendor/bundle
COPY --from=builder /app /app
# Non-root user
RUN groupadd -r app && useradd -r -g app -d /app -s /bin/bash app
RUN chown -R app:app /app
USER app
ENV RAILS_ENV=production \
RAILS_SERVE_STATIC_FILES=true \
RAILS_LOG_TO_STDOUT=true
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
Ukuran tipikal: 200-300 MB (Rails dengan semua extension standar).
Penjelasan penting:
Stage 1: Build
ruby:3.3.4-slim-bookworm— Ruby 3.3 di Debian slim. Lebih kecil dari defaultruby:3.3(yang bawa banyak tool).- Install build dependency:
build-essential(gcc, make),libpq-dev(PostgreSQL header),libxml2-dev(Nokogiri),libvips-dev(image processing),nodejs/yarn(untuk asset Rails). bundle config set without 'development test'— skip dev/test gem saat install.bundle config set deployment 'true'— install sesuaiGemfile.lock, tidak akan modify.bundle config set path 'vendor/bundle'— install gem di local directory, bukan global.bundle install --jobs 4— paralel install untuk mempercepat.rails assets:precompile— pre-compile CSS/JS kepublic/assets.bootsnap precompile— pre-compile bootsnap cache untuk startup yang lebih cepat.
Stage 2: Runtime
ruby:3.3.4-slim-bookworm— image Ruby ramping.- Install runtime library saja (tanpa
-dev). Contoh:libpq5bukanlibpq-dev. tzdatauntuk timezone data.curl(opsional) untuk healthcheck jika image punya shell.- Copy
vendor/bundledari build stage. - Copy seluruh source code.
- Non-root user.
RAILS_LOG_TO_STDOUT=true— agar Rails log ke STDOUT.RAILS_SERVE_STATIC_FILES=true— agar Puma serve static asset (kalau tidak pakai Nginx terpisah).
Kapan pakai: Default untuk production service Ruby. Trade-off ukuran vs debugging能力 masih seimbang — slim punya shell.
4.2 Versi Alpine: Lebih Kecil, Lebih Rumit #
# ==== Stage 1: Build ====
FROM ruby:3.3.4-alpine3.20 AS builder
WORKDIR /app
RUN apk add --no-cache \
build-base \
postgresql-dev \
libxml2-dev \
libxslt-dev \
libffi-dev \
yaml-dev \
openssl-dev \
vips-dev \
nodejs \
yarn \
tzdata
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local without 'development test' \
&& bundle config set --local deployment 'true' \
&& bundle config set --local path 'vendor/bundle' \
&& bundle install --jobs 4
COPY . .
RUN SECRET_KEY_BASE=dummy \
RAILS_ENV=production \
bundle exec rails assets:precompile
# ==== Stage 2: Runtime ====
FROM ruby:3.3.4-alpine3.20
WORKDIR /app
RUN apk add --no-cache \
postgresql-client \
libxml2 \
libxslt \
libffi \
yaml \
openssl \
vips \
tzdata \
curl
COPY --from=builder /app/vendor/bundle /app/vendor/bundle
COPY --from=builder /app /app
RUN addgroup -g 1001 -S appgroup \
&& adduser -u 1001 -S appuser -G appgroup \
&& chown -R appuser:appgroup /app
USER appuser
ENV RAILS_ENV=production \
RAILS_LOG_TO_STDOUT=true
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
Ukuran tipikal: 120-180 MB.
Catatan Alpine untuk Ruby:
musl libc— beberapa native gem bisa bermasalah. Test dulu sebelum pakai.apk addlebih cepat dariapt-get install.apk add --no-cache— tidak menyimpan cache index.apk deldi akhir untuk cleanupbuild-base(tapi biasanyabuild-basehanya di build stage, tidak perlu di runtime).
4.3 Versi Distroless: Production Mature #
Ruby tidak punya distroless resmi, tapi kamu bisa build custom:
# ==== Stage 1: Install Ruby ====
FROM debian:bookworm-slim AS ruby-base
RUN apt-get update && apt-get install -y --no-install-recommends \
ruby-full \
ruby-dev \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Setup Ruby
RUN gem install bundler --no-document
# ==== Stage 2: Build ====
FROM ruby-base AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
libxml2-dev \
libxslt1-dev \
libvips-dev \
&& rm -rf /var/lib/apt/lists/*
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local without 'development test' \
&& bundle config set --local deployment 'true' \
&& bundle config set --local path 'vendor/bundle' \
&& bundle install --jobs 4
COPY . .
RUN SECRET_KEY_BASE=dummy RAILS_ENV=production \
bundle exec rails assets:precompile
# ==== Stage 3: Runtime Distroless ====
FROM gcr.io/distroless/base-debian12:nonroot
WORKDIR /app
# Copy Ruby + library dari build stage
COPY --from=ruby-base /usr/bin/ruby /usr/bin/ruby
COPY --from=ruby-base /usr/lib/ruby /usr/lib/ruby
COPY --from=ruby-base /usr/lib/x86_64-linux-gnu/libruby* /usr/lib/x86_64-linux-gnu/
COPY --from=ruby-base /usr/local/lib/ruby /usr/local/lib/ruby
COPY --from=ruby-base /usr/local/bin/bundle /usr/local/bin/bundle
COPY --from=ruby-base /usr/local/bin/bundler /usr/local/bin/bundler
# Copy runtime library OS
COPY --from=builder /usr/lib/x86_64-linux-gnu/libpq* /usr/lib/x86_64-linux-gnu/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libxml2* /usr/lib/x86_64-linux-gnu/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libxslt* /usr/lib/x86_64-linux-gnu/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libvips* /usr/lib/x86_64-linux-gnu/
# Copy app
COPY --from=builder /app/vendor/bundle /app/vendor/bundle
COPY --from=builder /app /app
USER nonroot:nonroot
ENV RAILS_ENV=production \
RAILS_LOG_TO_STDOUT=true \
PATH="/usr/local/bin:$PATH"
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
Ukuran tipikal: 80-130 MB.
Catatan penting untuk distroless Ruby:
- Sangat advanced — kamu harus paham dependency Ruby secara manual.
- Library OS harus di-copy manual dari build stage.
- Tidak ada shell — debugging interaktif tidak mungkin.
PATHharus diset eksplisit.
Kapan pakai: Production high-maturity dengan observability sangat solid.
5. Bundler Optimization #
5.1 Production Install Flag #
bundle config set --local without 'development test'
bundle config set --local deployment 'true'
bundle config set --local path 'vendor/bundle'
bundle install --jobs 4 --retry 3
Penjelasan:
without 'development test'— skip gem groupdevelopmentdantest(didefinisikan diGemfile).deployment 'true'— install sesuaiGemfile.lock, tidak akan modify.path 'vendor/bundle'— install di local directory (bukan/usr/local/bundle).--jobs 4— paralel install.--retry 3— retry saat network blip.
5.2 Disable Documentation Install #
Untuk image lebih kecil, disable RDoc dan Ri:
gem: --no-document
Taruh di ~/.gemrc atau Gemfile:
# Gemfile
install_if -> { false } do
gem 'rdoc'
gem 'psych'
end
5.3 Cache Gem yang Sering Bermasalah #
Untuk gem yang butuh waktu lama compile (nokogiri, grpc), simpan binary di cache:
# Di build stage
RUN bundle config set --local force_ruby_platform false
6. Asset Compilation (Rails) #
Rails app biasanya perlu compile asset. Lakukan di build stage agar runtime image ramping:
# Di build stage
RUN SECRET_KEY_BASE=dummy \
RAILS_ENV=production \
bundle exec rails assets:precompile
Penting:
SECRET_KEY_BASE=dummy— Rails butuh secret key untuk precompile.RAILS_ENV=production— precompile sesuai environment production.- Hasil precompile ada di
public/assets(atau sesuaiconfig.assets.prefix).
Runtime tidak boleh punya Node.js, Yarn, atau build tools. Semua asset sudah di-compile di build stage.
Untuk runtime, tambahkan:
ENV RAILS_SERVE_STATIC_FILES=true
Agar Puma serve file static dari public/. Atau lebih baik, gunakan Nginx terpisah.
7. Bootsnap Optimization #
Bootsnap adalah tool yang mempercepat startup Ruby/Rails dengan caching parsed file dan bytecode. Sangat berguna untuk container cold start.
Setup di Gemfile:
gem 'bootsnap', require: false
Di config/boot.rb:
require 'bootsnap/setup'
Di build stage, pre-compile bootsnap cache:
RUN bundle exec bootsnap precompile --gemfile \
&& bundle exec bootsnap precompile app/ config/ lib/
Cache ini akan di-copy ke runtime image, dan startup akan jauh lebih cepat.
8. Production Logging #
Rails default log ke log/development.log atau log/production.log. Untuk container, redirect ke STDOUT.
config/environments/production.rb:
config.logger = ActiveSupport::Logger.new(STDOUT)
config.log_level = ENV.fetch('LOG_LEVEL', 'info')
config.log_tags = [:request_id]
Atau pakai rails_stdout_logging gem (untuk Rails 6+):
gem 'rails_stdout_logging'
Output JSON structured log (untuk production) dengan Lograge:
gem 'lograge'
config.lograge.enabled = true
config.lograge.formatter = Lograge::Formatters::Json.new
config.lograge.base_controller_class = 'ActionController::API'
9. Healthcheck #
Buat endpoint /health di Rails:
# config/routes.rb
Rails.application.routes.draw do
get '/health', to: 'health#show'
end
# app/controllers/health_controller.rb
class HealthController < ApplicationController
def show
head :ok
end
end
Di Dockerfile (untuk alpine/slim, bukan distroless):
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
Untuk distroless: pindahkan ke orchestrator.
10. Signal Handling #
Puma (dan Unicorn) handle SIGTERM dengan benar secara default, tapi pastikan:
- Exec form di
CMD(CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]). - Worker shutdown graceful diset di
puma.rb:
# config/puma.rb
worker_timeout 60
worker_shutdown_timeout 30
worker_boot_timeout 60
preload_app!
11. Security Hardening #
Non-Root User #
# Untuk alpine
RUN addgroup -g 1001 -S appgroup \
&& adduser -u 1001 -S appuser -G appgroup
# Untuk slim
RUN groupadd -r app && useradd -r -g app -d /app -s /bin/bash app
# Untuk distroless
USER nonroot:nonroot
Secret Management #
Rails punya credentials.yml.enc yang di-encrypt. Jangan commit master key ke Git. Mount saat runtime:
# docker-compose.yml
services:
app:
secrets:
- rails_master_key
secrets:
rails_master_key:
file: ./config/master.key
Vulnerability Scanning #
- name: Build
run: docker build -t myapp:${{ github.sha }} .
- name: Scan
run: trivy image --exit-code 1 --severity CRITICAL myapp:${{ github.sha }}
12. Anti-Pattern yang Harus Dihindari #
✗ Pakai ruby:latest
#
// ✗ Build tidak deterministik, image 700+ MB
FROM ruby:latest
Solusi: Pin tag: ruby:3.3.4-slim-bookworm.
✗ Bundle Install Tanpa Filter #
// ✗ Test, development gem ikut ke production
RUN bundle install
Solusi: bundle config set without 'development test'.
✗ Build Tool di Runtime #
// ✗ gcc, make, libpq-dev ada di runtime image
FROM ruby:3.3-slim
RUN apt-get install -y build-essential libpq-dev
RUN bundle install
CMD ["rails", "server"]
Solusi: Multi-stage, build tool hanya di build stage.
✗ Asset Compilation di Runtime #
// ✗ Runtime image butuh nodejs, yarn
RUN bundle exec rails assets:precompile
CMD ["rails", "server"]
Solusi: Pre-compile asset di build stage, runtime image ramping.
✗ Logging ke File #
# ✗ Log hilang saat container restart
config.logger = Logger.new('log/production.log')
Solusi: Log ke STDOUT dengan ActiveSupport::Logger.new(STDOUT).
✗ Tag Image Tanpa Strategi #
# ✗ Tidak bisa rollback
docker build -t myapp:latest .
Solusi: Semantic version + git hash.
13. Contoh Dockerfile Rails Production-Grade #
# syntax=docker/dockerfile:1.7
# ==== Stage 1: Build ====
FROM ruby:3.3.4-slim-bookworm AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
libxml2-dev \
libxslt1-dev \
libffi-dev \
libyaml-dev \
libssl-dev \
libvips-dev \
nodejs \
yarn \
&& rm -rf /var/lib/apt/lists/*
# Cache gem
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local without 'development test' \
&& bundle config set --local deployment 'true' \
&& bundle config set --local path 'vendor/bundle' \
&& bundle install --jobs 4 --retry 3
# Build app
COPY . .
RUN SECRET_KEY_BASE=dummy \
RAILS_ENV=production \
bundle exec rails assets:precompile \
&& bundle exec bootsnap precompile --gemfile \
&& bundle exec bootsnap precompile app/ config/ lib/
# ==== Stage 2: Runtime ====
FROM ruby:3.3.4-slim-bookworm
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
libxml2 \
libxslt1.1 \
libffi8 \
libyaml-0-2 \
libssl3 \
libvips42 \
tzdata \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy app
COPY --from=builder /app/vendor/bundle /app/vendor/bundle
COPY --from=builder /app /app
# Non-root user
RUN groupadd -r app && useradd -r -g app -d /app -s /bin/bash app \
&& chown -R app:app /app
USER app
ENV RAILS_ENV=production \
RAILS_LOG_TO_STDOUT=true \
RAILS_SERVE_STATIC_FILES=true \
BUNDLE_PATH=vendor/bundle
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
14. Kapan Pakai Strategi Mana #
| Kondisi | Pilihan | Alasan |
|---|---|---|
| Rails API standar | slim + multi-stage |
Ukuran wajar, debugging能力 |
| Rails full app | slim + multi-stage |
Butuh asset pipeline, butuh tool |
| Microservice | alpine + multi-stage |
Image lebih kecil |
| Production high-maturity | Distroless + custom | Ukuran minimal, observability solid |
| Rails API ringan | alpine + multi-stage |
Cold start time penting |
| Sidekiq worker | slim + multi-stage |
Tidak perlu web server |
15. Checklist Review Dockerfile Ruby #
BASE IMAGE:
□ Tag eksplisit (ruby:3.3.4-slim-bookworm, bukan latest)
□ Runtime stage ramping (slim, alpine, atau distroless)
□ Bukan ruby:latest (terlalu besar)
BUILD:
□ Multi-stage build
□ bundle config without 'development test'
□ bundle config deployment 'true'
□ bundle install --jobs 4
□ Build tool (build-essential, *-dev) hanya di build stage
□ Asset precompile di build stage
□ bootsnap precompile di build stage
RUNTIME:
□ USER nonroot
□ Library runtime (tanpa -dev) di runtime stage
□ RAILS_LOG_TO_STDOUT=true
□ BUNDLE_PATH=vendor/bundle
□ Log ke STDOUT
□ Healthcheck
□ CMD dalam exec form
UKURAN:
□ < 300 MB untuk slim runtime
□ < 200 MB untuk alpine runtime
□ < 150 MB untuk distroless runtime
□ docker history tidak ada layer aneh
KEAMANAN:
□ Tidak ada secret di image
□ credentials.yml.enc di-encrypt, master key di-mount
□ .dockerignore tegas
□ .env di-exclude
□ Image di-scan trivy/grype
□ Non-root user
□ Base image up-to-date
FRAMEWORK:
□ Asset precompile di build stage
□ bootsnap precompile
□ Lograge untuk JSON log
□ Puma config dituning
Ringkasan #
- Image Ruby ramping sangat mungkin — Ruby bisa sekecil Java atau Node.js dengan Dockerfile yang disiplin.
- Realita ukuran: 80-130 MB (distroless), 120-180 MB (alpine), 200-300 MB (slim multi-stage), 400-600 MB (slim tanpa optimasi), 700-900 MB (ruby:latest — anti-pattern).
- Multi-stage build wajib —
build-essential,*-devpackage, dan compiler hanya di build stage. Runtime stage hanya butuh library runtime.bundle config set without 'development test'— skip test, development, dan debug gem saat install. Ini yang paling membedakan image ramping dari image gemuk.- Asset precompile di build stage —
rails assets:precompilebutuh Node.js/Yarn. Lakukan di build stage, runtime image ramping.- Bootsnap precompile — cache parse dan bytecode di build stage agar cold start lebih cepat. Sangat berguna untuk serverless dan Kubernetes.
- Library runtime (tanpa
-dev) — di build stage installlibpq-dev, di runtime cukuplibpq5. Ini kompromi penting untuk image ramping.- Rails precompile butuh
SECRET_KEY_BASE— set dummy value di Dockerfile:SECRET_KEY_BASE=dummy RAILS_ENV=production bundle exec rails assets:precompile.- Lograge untuk JSON log — Rails default log multi-line yang sulit di-parse. Lograge +
JsonFormatteruntuk production-grade structured log.RAILS_LOG_TO_STDOUT=true— agar Rails log ke STDOUT, bukan file. Wajib untuk container.- Tag eksplisit —
ruby:3.3.4-slim-bookworm, bukanlatest. Build reproducibility penting untuk audit.- Image ramping butuh observability solid — log JSON, metrics, healthcheck, dan graceful shutdown. Ruby bisa se-ramping Java dengan disiplin Dockerfile yang tepat.
- Alpine untuk microservice, slim untuk monolith — alpine lebih kecil tapi ada risiko musl compatibility issue. slim lebih besar tapi lebih aman.