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 default ruby: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 sesuai Gemfile.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 ke public/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: libpq5 bukan libpq-dev.
  • tzdata untuk timezone data.
  • curl (opsional) untuk healthcheck jika image punya shell.
  • Copy vendor/bundle dari 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 add lebih cepat dari apt-get install.
  • apk add --no-cache — tidak menyimpan cache index.
  • apk del di akhir untuk cleanup build-base (tapi biasanya build-base hanya 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.
  • PATH harus 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 group development dan test (didefinisikan di Gemfile).
  • deployment 'true' — install sesuai Gemfile.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 sesuai config.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 wajibbuild-essential, *-dev package, 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 stagerails assets:precompile butuh 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 install libpq-dev, di runtime cukup libpq5. 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 + JsonFormatter untuk production-grade structured log.
  • RAILS_LOG_TO_STDOUT=true — agar Rails log ke STDOUT, bukan file. Wajib untuk container.
  • Tag eksplisitruby:3.3.4-slim-bookworm, bukan latest. 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.

← Sebelumnya: TypeScript   Berikutnya: Rust →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact