My RoR app uses Action-Cable to update the page when messages are received from a GPS receiver. This works fine in development mode, but the page updates are not received by the browser in production mode. (BTW, this is running in a Docker container.)
The browser is producing a 404 error...
[Error] Error: 404 Not Found http://192.168.2.180/assets/channels/consumer — es-module-shims.min-69…1d2841985042ce58e1b94af5dc14ab8268b3d02e7de3d6.js:0
The header of the HTML page follows. The offending line marked with arrow. Note that the same list of imports is listed in development mode, too.
<head>
    <title>GpsMonitor</title>
    <meta name="csrf-param" content="authenticity_token"/>
    <meta name="csrf-token" content="aEStfQBfly16h-8ySZGPpHBc0gF9iQgSewdVOsG2NJXS8bVYhwz2BbcJ_g4atCcD41F5vzQN9EHO0nGWu--m0g"/>
    <link rel="stylesheet" href="/assets/application-3a0ff7f096b12b42db8b2f5812508637530e3bd029af4da12e48832434751882.css" data-turbo-track="reload"/>
    <script type="importmap" data-turbo-track="reload">{
      "imports": {
        "application": "/assets/application-f6263e77bd95d299ab5b103d0d21370b97b4da63a30ad5db93e56abbe721f313.js",
        "@hotwired/turbo-rails": "/assets/turbo.min-305f0d205866ac9fc3667580728220ae0c3b499e5f15df7c4daaeee4d03b5ac1.js",
        "@hotwired/stimulus": "/assets/stimulus.min-900648768bd96f3faeba359cf33c1bd01ca424ca4d2d05f36a5d8345112ae93c.js",
        "@hotwired/stimulus-loading": "/assets/stimulus-loading-685d40a0b68f785d3cdbab1c0f3575320497462e335c4a63b8de40a355d883c0.js",
        "@rails/actioncable": "/assets/actioncable.esm-3d92de0486af7257cac807acf379cea45baf450c201e71e3e84884c0e1b5ee15.js",
        "controllers/application": "/assets/controllers/application-368d98631bccbf2349e0d4f8269afb3fe9625118341966de054759d96ea86c7e.js",
        "controllers/gps_klass_counts_controller": "/assets/controllers/gps_klass_counts_controller-91f59961c65aee970b582c9981d193a0a28ff34b53cb15503723ddb553a0257b.js",
        "controllers/gps_messages_pps_controller": "/assets/controllers/gps_messages_pps_controller-3f740c9cae7ebf08fe8881fcf2bafe7fa56182e50bb78eefd41631dc96262cac.js",
        "controllers/gps_messages_tpv_controller": "/assets/controllers/gps_messages_tpv_controller-6a58e2bbee0bbcf3cff4c37c00d3e6883ed7dbc99ec3ae8631d274a69a1e56db.js",
        "controllers": "/assets/controllers/index-73a52b5f64ab23bee4b96cc21c0c816f2ee50e0b6b0f2447981812f996e8ff74.js",
        "controllers/process_message_data": "/assets/controllers/process_message_data-39a87fbb10ce21c1da968c836e18ad04ebaf03209e43a0d897e29c64bfb76d48.js",
---->>  "channels/consumer": "/assets/channels/consumer-4102819cda7b48d7adaef7ad1421a4d6d7d9f3b993e89e34bfdbdbcc12785a4c.js",
        "channels": "/assets/channels/index-76c0fcbc336063635d9bbf546313bf2fc042e3afb60ac41446a0c23578878bf5.js"
      }
    }</script>
    <link rel="modulepreload" href="/assets/application-f6263e77bd95d299ab5b103d0d21370b97b4da63a30ad5db93e56abbe721f313.js">
    <link rel="modulepreload" href="/assets/turbo.min-305f0d205866ac9fc3667580728220ae0c3b499e5f15df7c4daaeee4d03b5ac1.js">
    <link rel="modulepreload" href="/assets/stimulus.min-900648768bd96f3faeba359cf33c1bd01ca424ca4d2d05f36a5d8345112ae93c.js">
    <link rel="modulepreload" href="/assets/stimulus-loading-685d40a0b68f785d3cdbab1c0f3575320497462e335c4a63b8de40a355d883c0.js">
    <script src="/assets/es-module-shims.min-6982885c6ce151b17d1d2841985042ce58e1b94af5dc14ab8268b3d02e7de3d6.js" async="async" data-turbo-track="reload"></script>
    <script type="module">
    import "application"
    </script>
</head>
Associated 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_all_from "app/javascript/controllers", under: "controllers"
pin "@rails/actioncable", to: "actioncable.esm.js"
pin_all_from "app/javascript/channels", under: "channels"
The nginx config file
# nginx.conf
server {
    listen 80;
    server_name tick.rustyshamrock.local;
    root /home/app/tick/public;
    passenger_enabled on;
    passenger_user app;
    passenger_ruby /usr/bin/ruby;
    
    # see: https://github.com/phusion/passenger/issues/2409
    passenger_env_var RUBYOPT '-r bundler/setup';
 
    # Nginx has a default limit of 1 MB for request bodies, which also applies
    # to file uploads. The following line enables uploads of up to 50 MB:
    client_max_body_size 50M;
    
    location /cable {
        passenger_app_group_name /home/app/tick_action_cable;
        # passenger_app_group_name /home/app/tick/cable;
        passenger_force_max_concurrent_requests_per_process 0;
    }
}
app/channels/gps_klass_counts_channel.rb
# app/channels/gps_klass_counts_channel.rb
class GpsKlassCountsChannel < ApplicationCable::Channel
  def subscribed
    logger.debug ""
    logger.debug "******* File: #{__FILE__}"
    logger.debug "******* Subscribed params: #{params}"
    logger.debug "******* params[:channel]: #{params[:channel]}"
    logger.debug "******* params[:klass]: #{params[:klass]}"
    logger.debug "******* self.channel_name: #{self.channel_name}"
    stream_from "#{self.channel_name}"
    # stream_from "gps_klass_counts"
  end
  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end
# ******* Subscribed params: {"channel"=>"GpsKlassCountsChannel"}
# ******* params[:channel]: GpsKlassCountsChannel
# ******* params[:klass]: 
# ******* self.channel_name: gps_klass_counts
# GpsKlassCountsChannel is transmitting the subscription confirmation
# GpsKlassCountsChannel is streaming from gps_klass_counts
app/channels/gps_messages_channel.rb
# app/channels/gps_messages_channel.rb
class GpsMessagesChannel < ApplicationCable::Channel
  def subscribed
    logger.debug ""
    logger.debug "******* File: #{__FILE__}"
    logger.debug "******* Subscribed params: #{params}"
    logger.debug "******* params[:channel]: #{params[:channel]}"
    logger.debug "******* params[:klass]: #{params[:klass]}"
    logger.debug "******* self.channel_name: #{self.channel_name}"
    if params.key?(:klass)
      stream_from "#{self.channel_name}_#{params[:klass]}"
    else
      stream_from "#{self.channel_name}"
    end
  end
  def unsubscribed
    logger.debug ""
    logger.debug "******* File: #{__FILE__}"
    logger.debug "******* Subscribed params: #{params}"
    logger.debug "******* params[:channel]: #{params[:channel]}"
    logger.debug "******* params[:klass]: #{params[:klass]}"
    logger.debug "******* self.channel_name: #{self.channel_name}"
  end
end
# ******* Subscribed params: {"channel"=>"GpsMessagesChannel"}
# ******* params[:channel]: GpsMessagesChannel
# ******* params[:klass]: 
# ******* self.channel_name: gps_messages
# GpsMessagesChannel is transmitting the subscription confirmation
# GpsMessagesChannel is streaming from gps_messages
app/channels/application_cable/channel.rb
module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end
app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
  end
end
On the javascript side. 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 "controllers"
import "channels"
app/javascript/channels/consumer.js - the offending file?
// app/javascript/channels/consumer.js
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `bin/rails generate channel` command.
import { createConsumer } from "@rails/actioncable"
export default createConsumer()
app/javascript/channels/index.js
// app/javascript/channels/index.js
// Import all the channels to be used by Action Cable
// no longer used, see 'controllers'
// import "channels/gps_messages_channel"  
// import "channels/gps_klass_counts_channel"
app/javascript/controllers/application.js
import { Application } from "@hotwired/stimulus"
const application = Application.start()
// Configure Stimulus development experience
application.debug = false
window.Stimulus   = application
export { application }
app/javascript/controllers/index.js
// Import and register all your controllers from the importmap under controllers/*
import { application } from "controllers/application"
// Eager load all controllers defined in the import map under controllers/**/*_controller
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)
// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
// lazyLoadControllersFrom("controllers", application)
// append to the bottom of existing file contents
import consumer from '../channels/consumer'
application.consumer = consumer
app/javascript/controllers/gps_klass_counts_controller.js (This is representative of all the other javascript/controllers
// app/javascript/controllers/gps_klass_counts_controller.js
import { Controller } from "@hotwired/stimulus"
import * as ActionCable from '@rails/actioncable'
ActionCable.logger.enabled = false;
// import { process_message_data } from '../channels/process_message_data'
function capitalize (value) {
  return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();    
}
export default class extends Controller {
  static targets = [
    'countTable',
    'countAll',
    'countPps',
    'countVersion',
    'countSent',
    'countSky',
    'countWatch',
    'countDevices',
    'countToff',
    'countGst',
    'countTpv'
  ];
  connect() {
    var that = this;
    console.log( "Subscribing to gps_klass_counts (controller)", this.element);
    this.channel = this.application.consumer.subscriptions.create({channel: "GpsKlassCountsChannel"}, {
      connected() { console.log("Connected gps_klass_counts (controller)", that.element); },
      disconnected() { console.log("Disconnected gps_klass_counts (controller)", that.element); },
      received(data) {
        console.log("Received gps_klass_counts (controller)", data);
        
        // console.log ("that:", that);  
        // console.log ("that.element:", that.element);
        // console.log ("that.hasCountTableTarget:", that.hasCountTableTarget);
        // console.log ("that.countTableTargets: ", that.countTableTargets);
        // console.log ("that.countTableTarget: ", that.countTableTarget);
        // console.log ("that.countTableTarget.innerHTML: ", that.countTableTarget.innerHTML);
        // console.log("Processing data object: ", data.html);
        // console.log("Processing Objects: ",Object.entries(data.html));
        Object.entries(data.html).forEach(([key, value]) => {
          // console.log (`Key: ${key}, Value: ${value}`);
          if (key === "ALL"){
            if (that.hasCountAllTarget) {
              that.countAllTarget.innerHTML = value;
            } else {
              console.log("Cannot find countAllTarget")
            }
          } else {
            let cmmd1 = `that.hasCount${capitalize(key)}Target`;
            // console.log(`Cmmd1 is: ${cmmd1}`);
            if (eval( cmmd1)) {
              let cmmd2 = `that.count${capitalize(key)}Target.innerHTML = value`;
              // console.log(`Cmmd2 is: ${cmmd2}`);
              eval(cmmd2);
            }else{
              console.log(`Cannot find count${capitalize(key)}Target`)
            }
          }
        })
      
        if (that.hasCountTableTarget) {
          // that.countTableTarget.innerHTML = data.html;
        } else {
          console.log ("Target Element Not Found");
        }
        console.log("Msg Counts Processing Complete");
      }
    });
  }
  disconnect () {
    console.log( "Unsubscribing from gps_klass_counts channel (controller)");
    this.channel.unsubscribe();
    console.log( "Unsubscribed from gps_klass_counts channel (controller)");
  }
  
  countTableTargetConnected( element) {
    console.log( "***** In countsTableTargetConnected!!! *****");
  }
  
  countTableTargetDisconnected( element) {
    console.log( "***** In countsTableTargetDisconnected!!! *****", element);
  }
  countAllTargetConnected( element)  {
    console.log( "countAllTargetConnected", element);    
  }
  countAllTargetDisonnected( element)  {
    console.log( "countAllTargetDisconnected", element);        
  }
}
ActionCable.logger.enabled = false;
Sorry about the long list of source files, but Action-Cable seems to be spread out all over the place. You'll note that I have embedded a bunch of debug log messages in the channels and javascript/controlers files. These get triggered immediately on startup (in development mode), signifying that the connection has been made. They don't show up at all in production mode -- which makes sense if the browser cannot locate the appropriate files.
Sure hope somebody can see what the problem is. If any more information is needed just ask.
Thanks!
 
    