Deploy Anycable with Kamal(formely MRSK)
Tags: rails, anycable, kamal, ruby
Published: 2023-08-03 13:43:17

Here we'll deploy [Anycable](https://anycable.io/) wih [Kamal(MRSK before)](https://github.com/basecamp/kamal).

We need our Rails code for anycable-rpc, so anycable-rpc will clearly be a role.
The Anycable websocket server (in our case, anycable-go) could be installed as an accessory, or as a role.

To simplify the setup, we'll utilize [--server-command](https://docs.anycable.io/ruby/cli?id=running-websocket-server-along-with-rpc) option of `anycable`. 
This will run the specified command after anycable is launched.
In our case - it'll initiate anycable-go.

```bash
bundle exec anycable --server-command "anycable-go"
```

To run anycable-go, we need to install its binary directly in our Rails app's Docker container:
```Dockerfile
# Dockerfile
# ...
RUN curl -fsSL https://github.com/anycable/anycable-go/releases/download/v1.4.1/anycable-go-linux-amd64 -o anycable-go \
    && chmod +x anycable-go
    && cp anycable-go /usr/local/bin/anycable-go
# ...
```

## MRSK config

We have websockets on the same host as our main web app. Therefore, we need to set up Traefik so that the /cable path will be handled by the anycable-go server, not the web.

To do this, we can utilize this config:
```yml
    # role's options in config/deploy.yml
    labels:
      traefik.enable: true
      traefik.http.routers.my_service-anycable.rule: 'PathPrefix(`/cable`)'
      traefik.http.routers.my_service-anycable.entrypoints: 'web'
      traefik.http.services.my_service-anycable.loadbalancer.server.port: 8080
```

In `my_service-anycable`, `my_service` is the service name in MRSK, and `anycable` is the role name in MRSK. 
This could also contain a destination, if you're using it.
This name should be the same as the role's [traefik_service](https://github.com/mrsked/mrsk/blob/299b166db7f42263552170bd8d4a2e961b86e6ee/lib/mrsk/configuration/role.rb#L113) name.

* `traefik.http.routers.my_service-anycable.rule: 'PathPrefix(``/cable``)'` adds a rule that we only want requests from the `/cable` path.

* `traefik.http.routers.my_service-anycable.entrypoints: 'web'` sets the entrypoint to the default `web` entrypoint at port `80`

* `traefik.http.services.my_service-anycable.loadbalancer.server.port: 8080` sets the port to which Traefik should send requests to our container.



Then, we can add the necessary Anycable environment variables and set up the MRSK deploy config:
```yml
# config/deploy.yml

service: my_service
servers:
  web:
    - 10.0.0.155
  # anycable-rpc together with anycable-go
  anycable:
    hosts:
      - 10.0.0.155
    cmd: bundle exec anycable --server-command "anycable-go"
    env:
      clear:
        ANYCABLE_RPC_HOST: localhost:50051
        ANYCABLE_HOST: "0.0.0.0"
        ANYCABLE_PORT: 8080
        ANYCABLE_REDIS_URL: "redis://:<%= ENV['REDIS_PASSWORD'] %>@<%= ENV['REDIS_HOST'] %>:6379/0"
        ANYCABLE_RPC_HOST: "localhost:50051"
    labels:
      traefik.enable: true
      traefik.http.routers.my_service-anycable.rule: 'PathPrefix(`/cable`)'
      traefik.http.routers.my_service-anycable.entrypoints: 'web'
      traefik.http.services.my_service-anycable.loadbalancer.server.port: 8080

env:
  clear:
    ANYCABLE_REDIS_URL: "redis://:<%= ENV['REDIS_PASSWORD'] %>@<%= ENV['REDIS_HOST'] %>:6379/0"
    REDIS_URL: "redis://:<%= ENV['REDIS_PASSWORD'] %>@<%= ENV['REDIS_HOST'] %>:6379/1"
    ANYCABLE_URL: "wss://<%= ENV['ACTIONCABLE_HOST'] %>/cable"

```

That's all! This sets up anycable-rpc with anycable-go on your server.


## Other deploy variants

If you want to separate anycable-rpc and anycable-go, you have several options to do so.

### Separate hosts

You could deploy anycable-go as an accessory on another host, then you could use a different Docker image and not install it in the Rails app's Docker image.

But in this setup, you need to publish ports for both anycable-rpc and anycable-go.


```yml
service: my_service
servers:
  web:
    - 10.0.0.155
  anycable-rpc:
    hosts:
      - 10.0.0.155
    cmd: bundle exec anycable
    port: 50051
    env:
      clear:
        ANYCABLE_RPC_HOST: 0.0.0.0:50051
    labels:
      traefik.tcp.routers.my_service-anycable-rpc.rule: 'HostSNI(`*`)'
      traefik.tcp.routers.my_service-anycable-rpc.entrypoints: rpc
      traefik.tcp.services.my_service-anycable-rpc.loadbalancer.server.port: 50051

traefik:
  options:
    publish:
      - 50051:50051
  args:
    entrypoints.web.address: ":80"
    entrypoints.rpc.address: ":50051"
    accesslog: true

accessories:
  ws:
    image: anycable/anycable-go:1.4
    host: 10.0.0.166
    port: "80:8080"
    env:
      clear:
        ANYCABLE_HOST: "0.0.0.0"
        ANYCABLE_PORT: 8080
        ANYCABLE_REDIS_URL: "redis://:<%= ENV['REDIS_PASSWORD'] %>@<%= ENV['REDIS_HOST'] %>:6379/0"
        ANYCABLE_RPC_HOST: "10.0.0.155:50051"
        ANYCABLE_DEBUG: true
```

### Setup anycable-go as accessory on the same host

You (most likely) could use anycable-go as an accessory, and write Traefik configs in the labels of the accessory.
But after doing this, you'll need to use the host's IP address or the Docker internal network (as mentioned below in other options).

### Use the host's IP address

Just publish the ports to the outside and use the host's IP address from inside the container.

### Use the host's network
Add the internal host IP as the Docker internal host.

```yml
servers:
  web:
    hosts:
      - 10.0.0.155
  anycable-rpc:
    hosts:
      - 10.0.0.155
    options:
      "add-host": host.docker.internal:host-gateway
    # ... env, cmd, etc
  anycable-go:
    hosts:
      - 10.0.0.155
    options:
      "add-host": host.docker.internal:host-gateway
    # ... env, cmd, etc
```
([source](https://github.com/mrsked/mrsk/issues/41#issuecomment-1483857362))

Then you could use `host.docker.internal` as host in anycable-rpc and anycable-go containers 

### Create docker internal network by hands and then use it

```yml
# Create the network first by ssh'ing into machine or using something like Ansible

servers:
  web:
    hosts:
      - 10.0.1.183
    options:
      "network": "my-network"

traefik:
  options:
    network: "my-network"

accessories:
  db:
    image: postgres:14
    host: 10.0.1.183
    ... etc
    options:
      network: "my-network"
```
([source](https://github.com/mrsked/mrsk/issues/41#issuecomment-1522501391))

And then use hosts from this network