# Ruby on Rails Dockerfile Generator

URL: /dockerizer/rails

Generate a production Dockerfile for a Rails app — gems installed in deployment mode, assets precompiled at build time and a non-root runtime.

## Default configuration

- `appName` (App name): rails-app — Used for the image tag, the compose service and the OCI labels.

- `port` (Port): 3000 — The port the app listens on inside the container. Keep it above 1024 so the process can bind it without root.

- `rubyVersion` (Ruby version): 3.4

- `database` (Database service): postgres — Also decides which client library the runtime image installs.

- `assets` (Precompile assets): true — Runs assets:precompile during the build so the container has nothing to write on boot.

- `redis` (Redis service): false — Adds Redis to docker-compose.yml and exposes REDIS_URL.

- `cacheMounts` (BuildKit cache mounts): true — Persists the package manager store between builds. Repeat builds skip the download entirely.

- `multiArch` (Multi-architecture build): false — Adds BUILDPLATFORM/TARGETARCH so `docker buildx build --platform linux/amd64,linux/arm64` cross-compiles natively.

- `healthcheck` (Healthcheck): true — Adds a HEALTHCHECK so orchestrators can restart an unresponsive container.

- `ociLabels` (OCI labels): false — Adds org.opencontainers.image.* metadata to the final image.

## Generated files

### Dockerfile

Multi-stage build: dependencies, compilation and the runtime image are separate, so only what the app needs at runtime ships.

```docker
# syntax=docker/dockerfile:1
# Generated by Easypanel Dockerizer — https://easypanel.io/dockerizer

# --- Base ---------------------------------------------
FROM ruby:3.4-slim AS base
WORKDIR /rails
ENV RAILS_ENV=production
ENV BUNDLE_DEPLOYMENT=1
ENV BUNDLE_PATH=/usr/local/bundle
ENV BUNDLE_WITHOUT="development:test"
ENV RAILS_LOG_TO_STDOUT=1

# --- Build --------------------------------------------
FROM base AS build

# Native gem toolchain — none of this reaches the runtime image
RUN apt-get update \
 && apt-get install -y --no-install-recommends build-essential git libpq-dev pkg-config \
 && rm -rf /var/lib/apt/lists/*

# Gemfile first: application edits must not re-resolve the bundle
COPY Gemfile Gemfile.lock ./
RUN --mount=type=cache,id=bundler-cache,target=/usr/local/bundle/cache,sharing=locked \
    bundle install && rm -rf "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git
COPY . .

# A dummy key is enough for asset compilation and never reaches the final image
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile

# --- Runtime ------------------------------------------
FROM base AS runtime

# Only the shared client libraries, not the -dev headers
RUN apt-get update \
 && apt-get install -y --no-install-recommends libpq5 \
 && rm -rf /var/lib/apt/lists/*
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails
RUN groupadd --system --gid 1001 rails \
 && useradd --system --uid 1001 --gid rails rails

# Rails writes to these at runtime, so they have to be owned by the app user
RUN mkdir -p tmp/pids log storage \
 && chown -R rails:rails db log storage tmp
USER rails
ENV PORT=3000
EXPOSE 3000
STOPSIGNAL SIGTERM

# Lets Docker, Compose and Easypanel see when the app is wedged
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD ["ruby", "-e", "require 'net/http'; exit(Net::HTTP.get_response(URI('http://127.0.0.1:3000/up')).code.to_i < 400 ? 0 : 1)"]
CMD ["./bin/rails", "server", "-b", "0.0.0.0", "-p", "3000"]

```

### .dockerignore

Keeps the build context small and stops secrets and local dependencies from reaching an image layer.

```bash
# Version control
.git
.gitignore
.github

# Secrets — never bake these into an image layer
.env
.env.*
!.env.example
*.pem
*.key

# Editor and OS noise
.vscode
.idea
.DS_Store
Thumbs.db

# Docs and local tooling
README.md
LICENSE
docs
.editorconfig
docker-compose*.yml
Dockerfile*
.dockerignore

# Bundler installs inside the image
vendor/bundle
.bundle
tmp
log
storage
!storage/.keep

```

### docker-compose.yml

Runs the image locally with its backing services, wired together and health-gated.

```yaml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      RAILS_ENV: production
      DATABASE_URL: postgres://app:app@postgres:5432/app
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres-data:

```

## Frequently asked questions

### Why is master.key excluded from the build context?

Because it decrypts config/credentials.yml.enc. Anything in the build context can end up in a layer, and layers are readable by anyone who can pull the image.

### Why does asset precompilation use a dummy secret?

Rails insists on a SECRET_KEY_BASE to boot, but asset compilation never uses it. SECRET_KEY_BASE_DUMMY=1 satisfies the check without putting a real secret anywhere near the build.

### Should I use Thruster in front of Puma?

It is worth it if you serve assets from the app. Thruster adds HTTP caching and X-Sendfile support in front of Puma — wrap the CMD with `./bin/thrust` to enable it.

## Related generators

- [Django](/dockerizer/django)

- [PHP](/dockerizer/php)

- [Laravel](/dockerizer/laravel)

- [Node.js](/dockerizer/nodejs)