After reading Deployment From Scratch by Josef Strzibny and moving a small Rails application off of heroku with this new knowledge, I eventually ran into the first wall of not having a plugin work out-of-the-box a la heroku way when I implemented a small Turbo Stream feature (obligatory Hotwired-is-awesome plug).

Meme representing Apache HTTPD and Rails working together via reverse proxy.
Old technology needs love & maintenance lest we lose it.

A few reasons made this difficult to figure out:

  1. Everyone uses Nginx
  2. The DFS book covers Nginx but I managed to get an Apache VHost to mimic most of the configuration
  3. My Rails app is running as a service, albeit managed and proxied to via a Unix socket, not the common HTTP reverse proxy configuration, i.e., HTTP servers communicating via TCP sockets
  4. I couldn’t find anything online for the WebSocket connection upgrade when reverse proxying to a Unix socket! I’m sure I’m not the only one who’s done it but DuckDuckGo didn’t get me anywhere (not to mention AdSearch G00gle)

There are several posts about setting up a reverse proxy and then adding a RewriteRule to change the URL’s scheme to ws instead of http because Apache will override the request’s Connection header and simply not pass the Upgrade header to the origin server.

So, without further ado:

Apache VHost Configuration

# websocket_app.conf

<VirtualHost *:443>
  ServerAdmin admin@websocket-app.com
  ServerName websocket-app.com
  DocumentRoot /srv/websocket_app/rails/app/public
  ProxyRequests off

  <Directory /srv/websocket_app/rails/app/public>
    AllowOverride all
    Require all granted
  </Directory>

  <Location />
    ProxyPass unix:/srv/websocket_app/rails/app/tmp/puma.sock|http://%{HTTP_HOST}/
    ProxyPassReverse unix:/srv/websocket_app/rails/app/tmp/puma.sock|http://%{HTTP_HOST}/
    ProxyPassReverseCookieDomain "websocket-app.com" "websocket-app.com"
    ProxyPassReverseCookiePath  "/"  "/"
    RequestHeader set X-Forwarded-Proto expr=%{REQUEST_SCHEME}
    RequestHeader set X-Forwarded-SSL expr=%{HTTPS}
  </Location>

  # URL where ActionCable is mounted
  <Location /cable>
    ProxyPass unix:/srv/websocket_app/rails/app/tmp/puma.sock|ws://%{HTTP_HOST}/cable # `/cable` at the end here is important
    ProxyPassReverse unix:/srv/websocket_app/rails/app/tmp/puma.sock|ws://%{HTTP_HOST}/cable
    ProxyPassReverseCookieDomain "websocket-app.com" "websocket-app.com"
    ProxyPassReverseCookiePath  "/"  "/"
    RequestHeader set X-Forwarded-Proto expr=%{REQUEST_SCHEME}
    RequestHeader set X-Forwarded-SSL expr=%{HTTPS}
  </Location>
</VirtualHost>

Basically, it boils down to adding a specific proxy to the URL where ActionCable is mounted, /cable by default, and instead of using a RewriteRule we reverse proxy using ws protocol instead of http.