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.