Fly.io Deployment

🎥 Check out AnyCasts episode to learn how to deploy AnyCable applications to Fly.io: Learn to Fly.io with AnyCable and Flying multi-regionally with NATS.

The recommended way to deploy AnyCable apps to Fly.io is to have two applications: one with a Rails app and another one with anycable-go (backed by the official Docker image).

Deploying Rails app

Follow the official documentation on how to deploy a Rails app.

Then, we need to configure AnyCable broadcast adapter. For multi-node applications (i.e., if you want to scale WebSocket servers horizontally), you need a distributed pub/sub engine, such as Redis or NATS.

The quickest way to deploy AnyCable on Fly is to use embedded NATS, so we'll be using it for the rest of the article. Thus, upgrade your anycable.yml by specifying nats as a broadcast adapter:

# config/anycable.yml
production:
  <<: *default
  # Use NATS in production
  broadcast_adapter: nats

Using Redis is similar to other deployment methods, please, check the corresponding documentation on how to create a Redis instance on Fly.

Configuration

AnyCable can automatically infer sensible defaults for applications running on Fly.io. You only need to link Rails and AnyCable-Go apps with each other.

We will rely on client-side load balancing, so make sure max connection age is set to some short period (minutes). The default value of 5 minutes is a good starting point, so you shouldn't change anything in the configuration.

Standalone RPC process (default)

You can define multiple processes in your fly.toml like this:

# fly.toml
[processes]
  web = "bundle exec puma" # or whatever command you use to run a web server
  rpc = "bundle exec anycable"

Don't forget to update the services definition:

  [[services]]
-   processes = ["app"]
+   processes = ["web"]

NOTE: Keep in mind that each process is executed within its own Firecracker VM. This brings a benefit of independent scaling, e.g., fly scale count web=2 rpc=1.

Embedded RPC

You can run RPC server along with the Rails web server by using the embedded mode. This way you can reduce the number of VMs used (and hence, reduce the costs or fit into the free tier).

Just add the following to your configuration:

# fly.toml
[env]
  # ...
  ANYCABLE_EMBEDDED = "true"

Embedding the RPC server could help to reduce the overall RAM usage (since there is a single Ruby process), but would increase the GVL contention (since more threads would compete for Ruby VM).

Deploying AnyCable-Go

To deploy AnyCable-Go server, we need to create a separate Fly application. Following the official docs, we should do the following:

  • Create a .fly/applications/anycable-go folder and use it as a working directory for subsequent commands:
mkdir -p .fly/applications/anycable-go
cd .fly/applications/anycable-go
  • Run the following command:
fly launch --image anycable/anycable-go:1 --no-deploy --name my-cable
  • Create a configuration file, fly.toml:
# .fly/applications/anycable-go/fly.toml
app = "my-cable" # use the name you chose on creation
kill_signal = "SIGINT"
kill_timeout = 5
processes = []

[build]
  image = "anycable/anycable-go:1"

[env]
  PORT = "8080"

[experimental]
  allowed_public_ports = []
  auto_rollback = true

[[services]]
  http_checks = []
  internal_port = 8080
  processes = ["app"]
  protocol = "tcp"
  script_checks = []
  [services.concurrency]
    # IMPORTANT: Specify concurrency limits
    hard_limit = 10000
    soft_limit = 10000
    type = "connections"

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"
  • If you use Redis, add REDIS_URL obtained during the Rails application configuration to the cable app:
fly secrets set REDIS_URL=<url>

You can always look up your REDIS_URL by running the following command: fly redis status <name>.

Now you can run fly deploy to deploy your AnyCable-Go server.

Linking Rails and AnyCable-Go apps

Finally, we need to connect both parts to each other.

At the Rails app side, we need to provide the URL of our WebSocket server. For example:

[env]
  # ...
  CABLE_URL = "my-cable.fly.dev"

And in your production.rb (added automatically if you used rails g anycable:setup):

Rails.application.configure do
  # Specify AnyCable WebSocket server URL to use by JS client
  config.after_initialize do
    config.action_cable.url = ActionCable.server.config.url = ENV.fetch("CABLE_URL", "/cable") if AnyCable::Rails.enabled?
  end
end

When using embedded NATS or HTTP broadcast adapter, we also need to specify the AnyCable-Go application name, so it can locate WebSocket servers automatically:

# fly.toml
[env]
  ANYCABLE_FLY_WS_APP_NAME = "my-cable"

NOTE: By default, AnyCable resolves the address within the current region. For example, if you run Rails application in the lhr region, than the resulting NATS url will be nats://lhr.my-cable.internal:4222.

At the AnyCable-Go side, we must provide the name of the Rails application:

# .fly/applications/anycable-go/fly.toml
[env]
  # ...
  FLY_ANYCABLE_RPC_APP_NAME="my-app"

The name will be used, for example, to generate an RPC address: my-app -> dns:///lhr.my-app.internal:50051. NOTE: The generated RPC url points to the instances located in the same region as the AnyCable-Go server.

Authentication

The described approach assumes running two Fly applications on two separate domains. If you're using cookie-based authentication, make sure you configured your Rails cookie settings accordingly:

# session_store.rb
Rails.application.config.session_store :cookie_store,
  key: "_any_cable_session",
  domain: :all # or domain: '.example.com'

# anywhere setting cookie
cookies[:val] = {value: "1", domain: :all}

IMPORTANT: It's impossible to share cookies between .fly.dev domains, so cookie-based authentication wouldn't work. We recommend using JWT authentication instead.