Rails 7 + Tailwindcss Build Fails

When trying to deploy a new Rails 7.0.4.3 app using Tailwindcss and importmaps, the build fails with the following error:

 => ERROR [stage-3 7/7] RUN bin/rails fly:build                                                                                                                        4.1s
------
 > [stage-3 7/7] RUN bin/rails fly:build:
#20 3.966 
#20 3.966 Rebuilding...
#20 4.028 Error: Cannot find module '@tailwindcss/forms'
#20 4.028 Require stack:
#20 4.028 - /app/config/tailwind.config.js
#20 4.028     at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
#20 4.028     at Function._resolveFilename (pkg/prelude/bootstrap.js:1955:46)
#20 4.028     at Function.resolve (node:internal/modules/cjs/helpers:108:19)
#20 4.028     at _resolve (/snapshot/tailwindcss/node_modules/jiti/dist/jiti.js:1:241066)
#20 4.028     at jiti (/snapshot/tailwindcss/node_modules/jiti/dist/jiti.js:1:243188)
#20 4.028     at /app/config/tailwind.config.js:46:5
#20 4.028     at jiti (/snapshot/tailwindcss/node_modules/jiti/dist/jiti.js:1:245663)
#20 4.028     at /snapshot/tailwindcss/lib/lib/load-config.js:35:30
#20 4.028     at loadConfig (/snapshot/tailwindcss/lib/lib/load-config.js:37:6)
#20 4.028     at Object.loadConfig (/snapshot/tailwindcss/lib/cli/build/plugin.js:133:49) {
#20 4.028   code: 'MODULE_NOT_FOUND',
#20 4.028   requireStack: [ '/app/config/tailwind.config.js' ]
#20 4.028 }
#20 4.039 rails aborted!
#20 4.039 Command failed with exit 1: /app/vendor/bundle/ruby/3.2.0/gems/tailwindcss-rails-2.0.27-x86_64-linux/exe/x86_64-linux/tailwindcss
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/tailwindcss-rails-2.0.27-x86_64-linux/lib/tasks/build.rake:7:in `system'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/tailwindcss-rails-2.0.27-x86_64-linux/lib/tasks/build.rake:7:in `block (2 levels) in <main>'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:281:in `block in execute'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:281:in `each'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:281:in `execute'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:219:in `block in invoke_with_call_chain'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:199:in `synchronize'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:199:in `invoke_with_call_chain'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:243:in `block in invoke_prerequisites'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:241:in `each'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:241:in `invoke_prerequisites'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:218:in `block in invoke_with_call_chain'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:199:in `synchronize'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:199:in `invoke_with_call_chain'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:243:in `block in invoke_prerequisites'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:241:in `each'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:241:in `invoke_prerequisites'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:218:in `block in invoke_with_call_chain'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:199:in `synchronize'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:199:in `invoke_with_call_chain'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/task.rb:188:in `invoke'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:160:in `invoke_task'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:116:in `block (2 levels) in top_level'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:116:in `each'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:116:in `block in top_level'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:125:in `run_with_threads'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:110:in `top_level'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/railties-7.0.4.3/lib/rails/commands/rake/rake_command.rb:24:in `block (2 levels) in perform'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:186:in `standard_exception_handling'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/railties-7.0.4.3/lib/rails/commands/rake/rake_command.rb:24:in `block in perform'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/rake-13.0.6/lib/rake/rake_module.rb:59:in `with_application'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/railties-7.0.4.3/lib/rails/commands/rake/rake_command.rb:18:in `perform'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/railties-7.0.4.3/lib/rails/command.rb:51:in `invoke'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/railties-7.0.4.3/lib/rails/commands.rb:18:in `<main>'
#20 4.039 /app/vendor/bundle/ruby/3.2.0/gems/bootsnap-1.16.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
#20 4.039 Tasks: TOP => fly:build => assets:precompile => tailwindcss:build
#20 4.040 (See full trace by running task with --trace)
------
Error failed to fetch an image or build from source: error building: executor failed running [/bin/bash -o pipefail -c ${BUILD_COMMAND}]: exit code: 1

I’m using the default Dockerfile generated by fly launch:

# syntax = docker/dockerfile:experimental

# Dockerfile used to build a deployable image for a Rails application.
# Adjust as required.
#
# Common adjustments you may need to make over time:
#  * Modify version numbers for Ruby, Bundler, and other products.
#  * Add library packages needed at build time for your gems, node modules.
#  * Add deployment packages needed by your application
#  * Add (often fake) secrets needed to compile your assets

#######################################################################

# Learn more about the chosen Ruby stack, Fullstaq Ruby, here:
#   https://github.com/evilmartians/fullstaq-ruby-docker.
#
# We recommend using the highest patch level for better security and
# performance.

ARG RUBY_VERSION=3.2.0
ARG VARIANT=jemalloc-slim
FROM quay.io/evl.ms/fullstaq-ruby:${RUBY_VERSION}-${VARIANT} as base

LABEL fly_launch_runtime="rails"

ARG BUNDLER_VERSION=2.4.6

ARG RAILS_ENV=production
ENV RAILS_ENV=${RAILS_ENV}

ENV RAILS_SERVE_STATIC_FILES true
ENV RAILS_LOG_TO_STDOUT true

ARG BUNDLE_WITHOUT=development:test
ARG BUNDLE_PATH=vendor/bundle
ENV BUNDLE_PATH ${BUNDLE_PATH}
ENV BUNDLE_WITHOUT ${BUNDLE_WITHOUT}

RUN mkdir /app
WORKDIR /app
RUN mkdir -p tmp/pids

RUN gem update --system --no-document && \
    gem install -N bundler -v ${BUNDLER_VERSION}

#######################################################################

# install packages only needed at build time

FROM base as build_deps

ARG BUILD_PACKAGES="git build-essential libpq-dev wget vim curl gzip xz-utils libsqlite3-dev"
ENV BUILD_PACKAGES ${BUILD_PACKAGES}

RUN --mount=type=cache,id=dev-apt-cache,sharing=locked,target=/var/cache/apt \
    --mount=type=cache,id=dev-apt-lib,sharing=locked,target=/var/lib/apt \
    apt-get update -qq && \
    apt-get install --no-install-recommends -y ${BUILD_PACKAGES} \
    && rm -rf /var/lib/apt/lists /var/cache/apt/archives

#######################################################################

# install gems

FROM build_deps as gems

COPY Gemfile* ./
RUN bundle install && rm -rf vendor/bundle/ruby/*/cache

#######################################################################

# install deployment packages

FROM base

ARG DEPLOY_PACKAGES="postgresql-client file vim curl gzip libsqlite3-0"
ENV DEPLOY_PACKAGES=${DEPLOY_PACKAGES}

RUN --mount=type=cache,id=prod-apt-cache,sharing=locked,target=/var/cache/apt \
    --mount=type=cache,id=prod-apt-lib,sharing=locked,target=/var/lib/apt \
    apt-get update -qq && \
    apt-get install --no-install-recommends -y \
    ${DEPLOY_PACKAGES} \
    && rm -rf /var/lib/apt/lists /var/cache/apt/archives

# copy installed gems
COPY --from=gems /app /app
COPY --from=gems /usr/lib/fullstaq-ruby/versions /usr/lib/fullstaq-ruby/versions
COPY --from=gems /usr/local/bundle /usr/local/bundle

#######################################################################

# Deploy your application
COPY . .

# Adjust binstubs to run on Linux and set current working directory
RUN chmod +x /app/bin/* && \
    sed -i 's/ruby.exe\r*/ruby/' /app/bin/* && \
    sed -i '/^#!/aDir.chdir File.expand_path("..", __dir__)' /app/bin/*

# The following enable assets to precompile on the build server.  Adjust
# as necessary.  If no combination works for you, see:
# https://fly.io/docs/rails/getting-started/existing/#access-to-environment-variables-at-build-time
ENV SECRET_KEY_BASE 1
# ENV AWS_ACCESS_KEY_ID=1
# ENV AWS_SECRET_ACCESS_KEY=1

# Run build task defined in lib/tasks/fly.rake
ARG BUILD_COMMAND="bin/rails fly:build"
RUN ${BUILD_COMMAND}

# Default server start instructions.  Generally Overridden by fly.toml.
ENV PORT 8080
ARG SERVER_COMMAND="bin/rails fly:server"
ENV SERVER_COMMAND ${SERVER_COMMAND}
CMD ${SERVER_COMMAND}

running ./bin/rake assets:precompile locally works just fine.

What could be causing this?

Here are some other related files:

config/tailwind.config.js

module.exports = {
  content: [
    './node_modules/flowbite/**/*.js',
    './public/*.html',
    './app/helpers/**/*.rb',
    './app/javascript/**/*.js',
    './app/views/**/*.{erb,haml,html,slim}'
  ],
  safelist: [
    'w-64',
    'w-1/2',
    'rounded-l-lg',
    'rounded-r-lg',
    'bg-gray-200',
    'grid-cols-4',
    'grid-cols-7',
    'h-6',
    'leading-6',
    'h-9',
    'leading-9',
    'shadow-lg',
    'bg-opacity-50',
    'dark:bg-opacity-80'
  ],
  darkMode: "class",
  theme: {
    extend: {
      colors: {
        primary: { "50": "#eff6ff", "100": "#dbeafe", "200": "#bfdbfe", "300": "#93c5fd", "400": "#60a5fa", "500": "#3b82f6", "600": "#2563eb", "700": "#1d4ed8", "800": "#1e40af", "900": "#1e3a8a" }
      },
      fontFamily: {
        'sans': ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'system-ui', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'Noto Sans', 'sans-serif', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'],
        'body': ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'system-ui', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'Noto Sans', 'sans-serif', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'],
        'mono': ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', 'monospace']
      },
      transitionProperty: {
        'width': 'width'
      },
      textDecoration: ['active'],
      minWidth: {
        'kanban': '28rem'
      },
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/aspect-ratio'),
    require('@tailwindcss/typography'),
    require('@tailwindcss/container-queries'),
    require('flowbite/plugin'),
  ]
}

config/importmap.rb

# Pin npm packages by running ./bin/importmap

pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin "flowbite", to: "https://cdnjs.cloudflare.com/ajax/libs/flowbite/1.6.4/flowbite.turbo.min.js"
pin "flowbite-datepicker", to: "https://cdnjs.cloudflare.com/ajax/libs/flowbite/1.6.4/datepicker.turbo.min.js"
pin_all_from "app/javascript/controllers", under: "controllers"

package.json

{
  "dependencies": {
    "flowbite": "^1.6.4"
  }
}

yarn.lock

# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


"@popperjs/core@^2.9.3":
  version "2.11.7"
  resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.7.tgz#ccab5c8f7dc557a52ca3288c10075c9ccd37fff7"
  integrity sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==

flowbite@^1.6.4:
  version "1.6.4"
  resolved "https://registry.yarnpkg.com/flowbite/-/flowbite-1.6.4.tgz#ea86f8f90e8d1279e2611c25838f6d112777e7d3"
  integrity sha512-//kSaigwERCcE3udumadB3+xfOXeN/ZQzcub2qVCHuB7uscB4XIm3yP9R8zL0XsEOnYG2lnroq/dydQZbzvESw==
  dependencies:
    "@popperjs/core" "^2.9.3"
    mini-svg-data-uri "^1.4.3"

mini-svg-data-uri@^1.4.3:
  version "1.4.4"
  resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939"
  integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==

You seem to be using import maps AND node, which is rather unusual. And you seem to have launched with a version of flyctl from January or earlier.

Let’s focus on the first question first. Can you explain why you have a package.json, yarn.lock and presumably a node_modules directory? What seems to be going on here is that fly launch detected one or more of these and produced a Dockerfile assuming you were going the node route which obviously doesn’t work for you.

Once.I have an understanding of what you want to achieve, I can walk you through updating your Dockerfiles, etc.

package.json and the requirement for node comes from using the Flowbite open-source component library for Tailwindcss as described in their installation guide:

The Dockerfile was generated today, but could the version mismatch be caused by an out of date flyctl installation?
I’ve noticed that every fly command prints a warning:

Update available 0.0.437 -> 0.0.506.

but I was unable to upgrade to the latest version.

➜  helheim_rb git:(main) ✗ fly version update
Update available 0.0.437 -> 0.0.506.
Run "fly version update" to upgrade.
/bin/zsh -c
Running automatic update [brew upgrade flyctl]
Warning: flyctl 0.0.437 already installed
➜  helheim_rb git:(main) ✗ brew upgrade flyctl
Warning: flyctl 0.0.437 already installed

As it turns out, I needed to run brew update before brew was able to install the latest version of flyctl.

Regenerating the Dockerfile with the newest version seems to solve the problem with the asset precompilation:

# syntax = docker/dockerfile:1

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
ARG RUBY_VERSION=3.2.0
FROM ruby:$RUBY_VERSION-slim as base

LABEL fly_launch_runtime="rails"

# Rails app lives here
WORKDIR /rails

# Set production environment
ENV RAILS_ENV="production" \
    BUNDLE_WITHOUT="development:test" \
    BUNDLE_DEPLOYMENT="1"

# Update gems and bundler
RUN gem update --system --no-document && \
    gem install -N bundler


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

# Install packages needed to build gems and node modules
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential curl libpq-dev node-gyp pkg-config python-is-python3

# Install JavaScript dependencies
ARG NODE_VERSION=19.8.1
ARG YARN_VERSION=1.22.19
ENV PATH=/usr/local/node/bin:$PATH
RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \
    /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \
    npm install -g yarn@$YARN_VERSION && \
    rm -rf /tmp/node-build-master

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

# Install node modules
COPY --link package.json yarn.lock ./
RUN yarn install --frozen-lockfile

# Copy application code
COPY --link . .

# 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 ./bin/rails assets:precompile


# Final stage for app image
FROM base

# Install packages needed for deployment
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y postgresql-client && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Run and own the application files as a non-root user for security
RUN useradd rails --home /rails --shell /bin/bash
USER rails:rails

# Copy built artifacts: gems, application
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build --chown=rails:rails /rails /rails

# Deployment options
ENV RAILS_LOG_TO_STDOUT="1" \
    RAILS_SERVE_STATIC_FILES="true"

# 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"]

Now however I’m getting a runtime error when trying to load application-specific assets:

GET
	https://helheim.fly.dev/assets/sidebar
Status
404
Not Found
VersionHTTP/2
Transferred340 B (0 B size)
Referrer Policystrict-origin-when-cross-origin

    	
    content-encoding
    	gzip
    content-type
    	text/html; charset=UTF-8
    date
    	Wed, 05 Apr 2023 22:13:47 GMT
    fly-cache-status
    	MISS
    fly-request-id
    	01GX9QQCYH552JREK8E1BMY01T-fra
    server
    	Fly/7d34da95 (2023-04-04)
    via
    	2 fly.io
    X-Firefox-Spdy
    	h2
    x-request-id
    	6048c599-2f3d-4dca-a9a3-c02493119c72
    x-runtime
    	0.004091
    	
    Accept
    	*/*
    Accept-Encoding
    	gzip, deflate, br
    Accept-Language
    	en-US,en;q=0.5
    Connection
    	keep-alive
    Cookie
    	_helheim_session=Qsja87gbdiIUXdyabZxvRthmfkyMVWXa6682uBnRuI0WzWNwpzWeFhJORQqLS%2Be0gcIvj7DLVwonl3dXXoxcSGNKRZDQctZmgF%2BIkeaBrrPCNAoIBNMilh%2FK6C0no1d%2BNcszQzGl6maer1s488wjWgDxDzAG2CyQxh8L268MQGO1JL2%2F5YBBalinS9F3EfhBJ04C66E1RyOuPDC1ndiV4LsZDVkVYVXHtiBqIPqGGfUQa3z5kXFOs09RxXiepVMBtZeoJQ2tH9bDi29aw6I6O%2FfOHK6MfmEQ--pRAkaw6FP71I3G9X--8Nq0mdqG1ZDQ6wemcKkCpg%3D%3D
    DNT
    	1
    Host
    	helheim.fly.dev
    Referer
    	https://helheim.fly.dev/assets/application-ea7cc7c40da4ec9eb302114e2f8a054f5329da0205af85906eb583319a016125.js
    Sec-Fetch-Dest
    	script
    Sec-Fetch-Mode
    	cors
    Sec-Fetch-Site
    	same-origin
    TE
    	trailers
    User-Agent
    	Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0

This file is located in app/javascript/sidebar.js and is included in app/javascript/application.js:

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "flowbite";
import "flowbite-datepicker";
import "./sidebar"
import "./dark_mode"
import "controllers"

Again, running the application locally with ./bin/dev loads this file without issue.
As far as I can tell from the generated Dockerfile, serving of static assets should be enabled.
Am I missing something here?

I ended up just rewriting the two javascript files I had as Stimulus Controllers, which seem to have circumvented the problem.

Cool! Some insight on what is going on. Running in development mode and production are different. Rerun assets:precompile locally and then look at the public/assets directory. You will find your stimulus controllers there - with a lengthy hex string inserted into the middle of the file name.

Then run ./bin/rails server -e production, and visit any page. Use view source in your browser, and look for <script type="importmap" data-turbo-track="reload">. What follows is the mapping from the names you are importing to the output of the assets precompilation.