Rails 7 app using ImportMaps fails during deploy assets:precompile "Could not find a JavaScript runtime"

Hey y’all!

I’m new to fly.io and trying to get a new Rails 7 app using ImportMap with NO js runtime.

I’ve checked lots of the other posts here about this issue and I’m not seeing the exact cross section of configurations that I am dealing with.

When I run the deploy, it gets to the assets precompile and fails as such:

 => CACHED [build 5/6] RUN bundle exec bootsnap precompile app/ lib/
 => ERROR [build 6/6] RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
------
 > [build 6/6] RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile:
1.318 bin/rails aborted!
1.318 ExecJS::RuntimeUnavailable: Could not find a JavaScript runtime. See https://github.com/rails/execjs for a list of available runtimes. (ExecJS::RuntimeUnavailable)

But with ImportMap I shouldn’t need a js runtime, right? I precompile assets locally and it works just fine with that, but not during deploy.

Here’s my Dockerfile…

# syntax = docker/dockerfile:1

# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t my-app .
# docker run -d -p 80:80 -p 443:443 --name my-app -e RAILS_MASTER_KEY=<value from config/master.key> my-app

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.3.5
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Rails app lives here
WORKDIR /rails

# Install base packages
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Set production environment
ENV RAILS_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development"

# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential libpq-dev git pkg-config && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
    bundle exec bootsnap precompile --gemfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile

# Final stage for app image
FROM base

# Copy built artifacts: gems, application
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails

# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
    chown -R rails:rails db log storage tmp
USER 1000:1000

# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start the server by default, this can be overwritten at runtime
EXPOSE 3000
CMD ["./bin/rails", "server"]

And my dockerfile.yml

# generated by dockerfile-rails

---
options:
  label:
    fly_launch_runtime: rails
  postgresql: true
  prepare: false

And my fly.toml

# fly.toml app configuration file generated for timber-tracker on 2024-11-08T09:22:55-08:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = 'timber-tracker'
primary_region = 'sea'
console_command = '/rails/bin/rails console'

[build]

[deploy]
  release_command = './bin/rails db:prepare'

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = 'stop'
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

[checks]
  [checks.status]
    port = 3000
    type = 'http'
    interval = '10s'
    timeout = '2s'
    grace_period = '5s'
    method = 'GET'
    path = '/up'
    protocol = 'http'
    tls_skip_verify = false

    [checks.status.headers]
      X-Forwarded-Proto = 'https'

[[vm]]
  memory = '1gb'
  cpu_kind = 'shared'
  cpus = 1

[[statics]]
  guest_path = '/rails/public'
  url_prefix = '/'

Thoughts?

Can you share your Gemfile?

I can confirm that Rails applications that are based on importmap do not need a JavaScript runtime. I’m guessing that there is some other gem that you are including that has a dependency on ExecJS.

Thanks for the quick response. Here’s my Gemfile…

source "https://rubygems.org"

gem "rails", "~> 7.2.1"
gem "sprockets-rails"
gem "puma", ">= 5.0"
gem "importmap-rails"
gem "turbo-rails"
gem "stimulus-rails"
gem "sassc-rails"
gem "bootstrap", "~> 5.3.3"
gem "pg"
gem "csv"
gem "roo"
gem "ruby-measurement"
gem "fractional", require: true
gem "paper_trail"
gem "revise_auth"

gem "bootsnap", require: false

group :development, :test do
  gem "brakeman", require: false
  gem "rubocop-rails-omakase", require: false
  gem "pry"
  gem "ostruct"
end

group :development do
  gem "web-console"
end

group :test do
  gem "capybara"
  gem "selenium-webdriver"
end

gem "dockerfile-rails", ">= 1.6", :group => :development

Bootstrap requires javascript.

If you wish to retain bootstrap, you can generate a new Dockerfile using:

bin/rails generate dockerfile

Thanks again for the quick response! I ran that command, which did indeed generate a new Dockerfile et al, and then the deploy got much farther along. Unfortunately it failed right at the end, as such:

--> Build Summary:  (​)
--> Building image done
image: registry.fly.io/timber-tracker:deployment-01JC6WG0RB4ZN0V1ZTWFYCVNBJ
image size: 232 MB

Watch your deployment at https://fly.io/apps/timber-tracker/monitoring

Running timber-tracker release_command: ./bin/rails db:prepare
  Updating release_command machine 9185905b101d68

-------
 ✖ Failed: error running release_command machine: error updating release_command machine: failed to update VM 9185905b101d68: invalid_argume…
-------
Error: release command failed - aborting deployment. error running release_command machine: error updating release_command machine: failed to update VM 9185905b101d68: invalid_argument: unable to update machine configured for auto destroy (Request ID: 01JC6WPP30CCTVR94E9022A2AP-sea) (Trace ID: 47f5a7d826abebc2a7db2950af377642)

Thoughts on that error?

Also, while I’ve got you, maybe you can help answer a follow-up question.

Why does having any javascript at all, require a js runtime? Like, is there a dedicated js runtime when I precompile assets manually and then run rails server? I thought including some javascript link tags in the header was pretty normal sorta stuff, which is all I am really doing with a single bootstrap js file in import map.

Thanks again for all your help!

Can you check the logs for more information?

I didn’t say that. What I said is that bootstrap requires a javascript runtime. More specifically bootstrap requires autoprefixer-rails which requires ExecJs.

ExecJS requires a JavaScript runtime.

Rails 7.1 apps that make use of TailwindCSS, StimulusJS, etc, don’t need a JavaScript runtime.

Thanks for the additional info about js runtimes. I’ll look into that more.

Also, I just deployed again, and this time it went through without issue. No problems from the command line, so that’s great!

I double checked the logs during the deploy and I did get this at the end of that process…

runner[xxx][info]Configuring firecracker
runner[xxx][info]Configuring firecracker
health[xxx][warn]Health check on port 3000 is in a 'warning' state. Your app may not be responding properly.
health[xxx][warn]Health check on port 3000 is in a 'warning' state. Your app may not be responding properly.
health[xxx][error]Health check on port 3000 has failed. Your app is not responding properly.
health[xxx][error]Health check on port 3000 has failed. Your app is not responding properly.

It just sat like that until I refreshed the app at fly.dev and then it popped back up and all seemed well and good. I am assuming that’s probably normal or expected behavior, right?

The machine started back up just fine, but there were a couple of these errors…

proxy[xxx][error][PC01] instance refused connection. is your app listening on 0.0.0.0:3000? make sure it is not only listening on 127.0.0.1 (hint: look at your startup logs, servers often print the address they are listening on)

It only happened twice and then everything as been fine right now, so, again, I’m guessing that’s probably normal behavior once an app is firing up for the first time after a deploy.

Anyhoo, I am goooood to go now. Thanks again for all your help today! Much appreciated.

Have a good weekend!