Skip to main content

JRuby on Miren

JRuby runs Ruby on the JVM. Miren auto-detects standard (MRI) Ruby from a Gemfile — see Ruby on Miren — but to run on JRuby specifically you use a Dockerfile.miren. Your Ruby code and gems run unchanged; they execute on the JVM.

Let your agent do this

Ask your AI coding agent to "set up this app on JRuby on Miren" after installing the Miren agent skills. It adds the Dockerfile.miren, wires up Bundler and the server, and deploys — using this page as its reference.

Do you need a Dockerfile?

Yes — to run on JRuby specifically. (Miren's auto-detection would pick MRI Ruby.) Add a Dockerfile.miren built on the jruby image. See Using Dockerfile.miren.

Want native support?

Miren auto-detects and builds common stacks (Python, Node, Bun, Go, Ruby, Rust) without a Dockerfile. This language isn't one of them yet — if you'd like first-class support, request it.

The app

A normal Sinatra app with a Gemfile — nothing JRuby-specific:

# Gemfile
source 'https://rubygems.org'

gem 'sinatra'
gem 'puma'
gem 'rackup'
# app.rb
require 'sinatra/base'

class App < Sinatra::Base
# Sinatra 4 rejects unknown Host headers; an empty list permits any host.
set :host_authorization, permitted_hosts: []

get '/' do
content_type 'text/plain'
"Hello from JRuby on Miren!\n"
end
end
Sinatra 4 blocks the deploy host

Sinatra 4 enables Rack::Protection::HostAuthorization, which only allows requests whose Host header is on an allowlist (localhost by default). Behind Miren's router your app is reached at its route hostname, so without configuration it returns Host not permitted. Set host_authorization to an empty permitted_hosts list (allow any), or list your actual hostnames. This applies to any Sinatra 4 app, MRI included.

# config.ru
require './app'
run App

Miren injects PORT and routes to it; you bind it via the Puma command in the Procfile below (0.0.0.0 on $PORT).

The Dockerfile

Create Dockerfile.miren in your project root. Install gems with Bundler at build time:

FROM jruby:9.4
WORKDIR /app
COPY Gemfile ./
RUN bundle install
COPY . /app
EXPOSE 8080

.dockerignore

.git

Set up the app

Even with a Dockerfile.miren, Miren needs at least one service defined — it doesn't use the image's CMD as the start command. Run Puma bound to the injected port:

web: bundle exec puma -b tcp://0.0.0.0:$PORT

JRuby's JVM startup plus Bundler and Puma can take longer than the default 15-second port-wait, so raise port_timeout. Keeping one instance always warm (fixed scaling) also avoids paying that cold-start on every scale-from-zero. Set both in .miren/app.toml:

name = "jruby-bench"

[services.web]
# JVM Ruby boots slowly; give it more than the 15s default to bind.
port_timeout = "120s"

[services.web.concurrency]
mode = "fixed"
num_instances = 1
miren deploy
JVM boot vs. the port timeout

By default Miren waits 15 seconds for a service to bind its port, then reports nothing is listening after the port timeout. JRuby's JVM startup plus Bundler and Puma routinely exceeds that. Raise port_timeout (e.g. "120s") so the health check waits long enough, and use fixed scaling so a warm instance stays up.

Environment variables

Set variables with miren env set-e for plain values, -s for secrets (masked in output and logs). Read them with ENV['KEY']:

miren env set -e RACK_ENV=production
miren env set -s DATABASE_URL

See App Configuration — Environment Variables.

Agent quick reference

  • Detection: MRI Ruby is auto-detected from a Gemfile; use Dockerfile.miren to pin JRuby
  • Base image: jruby:9.4; bundle install at build time
  • Service is required: Procfile web: bundle exec puma -b tcp://0.0.0.0:$PORT — the image CMD is not used
  • Slow boot: raise port_timeout (e.g. "120s") past the 15s default, and pin num_instances = 1 to stay warm
  • Port: Puma -b tcp://0.0.0.0:$PORT
  • Env vars: miren env set -e/-s; read with ENV['KEY']

Next steps