Search code examples
ruby-on-railsnginxredispumaactioncable

What do I need to do to hook up ActionCable on nginx and puma?


I'm having trouble getting ActionCable hooked up in my prod environment, and related questions haven't had a working solution. I'm using an nginx+puma setup with Rails 6.1.3.2 on Ubuntu 20.04. I have confirmed that redis-server is running on port 6379, and that Rails is running as production.

Here's what I'm getting in my logs:

I, [2021-05-25T22:47:25.335711 #72559]  INFO -- : [5d1a85f7-0102-4d25-bd4e-d81355b846ee] Started GET "/cable" for 74.111.15.223 at 2021-05-25 22:47:25 +0000
I, [2021-05-25T22:47:25.336283 #72559]  INFO -- : [5d1a85f7-0102-4d25-bd4e-d81355b846ee] Started GET "/cable/"[non-WebSocket] for 74.111.15.223 at 2021-05-25 22:47:25 +0000
E, [2021-05-25T22:47:25.336344 #72559] ERROR -- : [5d1a85f7-0102-4d25-bd4e-d81355b846ee] Failed to upgrade to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: close, HTTP_UPGRADE: )
I, [2021-05-25T22:47:25.336377 #72559]  INFO -- : [5d1a85f7-0102-4d25-bd4e-d81355b846ee] Finished "/cable/"[non-WebSocket] for 74.111.15.223 at 2021-05-25 22:47:25 +0000

This happens every few seconds. You can see matching output in the browser console:

enter image description here

For one, I'm pretty sure that I need to add some sections to my nginx site config, such as a /cable section, but I haven't figured out the correct settings. Here's my current config:

server {
    root /home/rails/myapp/current/public;
    server_name myapp.com;
    index index.htm index.html;

        location ~ /.well-known {
                allow all;
        }

        location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # needed to allow serving of assets and other public files
    location ~ ^/(assets|packs|graphs)/ {
        gzip_static on;
        expires 1y;
        add_header Cache-Control public;
        add_header Last-Modified "";
        add_header ETag "";
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

server {
    if ($host = myapp.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen   80;
    server_name myapp.com;
    return 404; # managed by Certbot
}

Here's my config/cable.yml:

development:
  adapter: async

test:
  adapter: test

production:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: myapp_production

In config/environments/production.rb, I've left these lines commented out:

  # Mount Action Cable outside main process or domain.
  # config.action_cable.mount_path = nil
  # config.action_cable.url = 'wss://example.com/cable'
  # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]

I have not mounted ActionCable manually in my routes or anything. This is as standard of a setup as you can get. I'm thinking the answer lies centrally in a correct nginx configuration, but I don't know what it should be. Perhaps there are Rails config settings that are needed too, though. I don't remember having to change any when deploying with passenger, but maybe puma is a different story.

Update

I also noticed that a lot of the proposed solutions in other questions, like this one, seem to reference a .sock file in tmp/sockets/. My sockets/ directory is empty, though. Web server's running fine besides ActionCable though.

Update #2

I also noticed that changing the config.action_cable.url to something like ws://myapp.com instead of wss://myapp.com has no effect even after restarting Rails. The browser console errors still say its trying to connect to wss://myapp.com. Possibly due to how I'm set up to force redirect HTTP to HTTPS. I wonder if that has anything to do with it?


Solution

  • I got it working. Here are the settings I needed:

    nginx config

    The server section from the config in my question must be modified to include the following two sections for the / and /cable locations:

            location / {
                    proxy_pass http://localhost:3000;
                    proxy_set_header Host $host;
                    proxy_set_header X-Real-IP $remote_addr;
                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                    proxy_set_header X-Forwarded-Proto $scheme;
            }
    
            location /cable {
                    proxy_pass http://localhost:3000;
                    proxy_http_version 1.1;
                    proxy_set_header Upgrade "websocket";
                    proxy_set_header Connection "Upgrade";
                    proxy_set_header X-Real-IP $remote_addr;
                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            }
    

    config/environments/production.rb

      # Change myapp.com to your app's location
      config.action_cable.allowed_request_origins = [ 'https://myapp.com' ]
      config.hosts << "myapp.com"
      config.hosts << "localhost"
    

    Huge thanks to Lam Phan in the comments for helping me out.