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.
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.
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 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
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; useDockerfile.mirento pin JRuby - Base image:
jruby:9.4;bundle installat build time - Service is required:
Procfileweb: bundle exec puma -b tcp://0.0.0.0:$PORT— the imageCMDis not used - Slow boot: raise
port_timeout(e.g."120s") past the 15s default, and pinnum_instances = 1to stay warm - Port: Puma
-b tcp://0.0.0.0:$PORT - Env vars:
miren env set -e/-s; read withENV['KEY']
Next steps
- Ruby on Miren — auto-detected MRI Ruby
- TruffleRuby on Miren — Ruby on GraalVM
- Application Scaling — fixed vs. autoscaling