|
| 1 | +# Kamal |
| 2 | + |
| 3 | +[Kamal](https://kamal-deploy.org/) is a deployment tool from Basecamp that makes it easy to deploy Rails applications with Docker. This guide covers different approaches to deploying AnyCable web server and RPC servers (if required) with Kamal 2. |
| 4 | + |
| 5 | +**NOTE:** This guide assumes that the primary application framework is Ruby on Rails. However, most ideas could be applied to other frameworks and stacks. |
| 6 | + |
| 7 | +There is a number of ways you can run AnyCable with Kamal depending on your needs. Here is the table describing recommended setups based on such factors as expected load, the number of servers (machines), whether you need an RPC server or not: |
| 8 | + |
| 9 | +| Setup | Load | Servers | RPC | Recommended Approach | |
| 10 | +|-------|------|---------|--------------|---------------------| |
| 11 | +| Small | Low | 1 | No | Anycable Thruster | |
| 12 | +| Small | Low | 1 | Yes | AnyCable Thruster + Embedded gRPC or HTTP RPC | |
| 13 | +| Small | Medium | 1 | Yes | AnyCable Thruster + RPC role | |
| 14 | +| Medium | Medium | 1-2 | No | AnyCable accessory (single server) | |
| 15 | +| Medium | Medium | 1-2 | Yes | AnyCable accessory (single server) + RPC role (each server) | |
| 16 | +| Large | High | 3+ | No | AnyCable accessory (many servers) + Redis/NATS | |
| 17 | +| Large | High | 3+ | Yes | AnyCable accessory (many servers) + Redis/NATS + RPC role (each server) | |
| 18 | + |
| 19 | +### Using Thruster |
| 20 | + |
| 21 | +The simplest way to deploy AnyCable with Kamal is using the [anycable-thruster](https://github.com/anycable/thruster) gem, which allows you to run AnyCable alongside your Rails web server in a single container. |
| 22 | + |
| 23 | +Rails' default Dockerfile already uses Thruster as its proxy server, so no additional changes required. |
| 24 | + |
| 25 | +With this setup, we recommend getting started with an [embedded gRPC server](/rails/getting_started?id=embedded-grpc-server) or [HTTP RPC](https://docs.anycable.io/ruby/http_rpc), so you can keep the Kamal configuration untouched. |
| 26 | + |
| 27 | +### Deploying AnyCable as an Accessory |
| 28 | + |
| 29 | +For applications that need more control or better resource isolation, you can deploy AnyCable server separately as a Kamal _accessory_. With this approach, running `kamal setup` should be sufficient to make AnyCable server up and running. |
| 30 | + |
| 31 | +One particular benefit AnyCable benefit this approach brings is **zero-disconnect deployments** (WebSocket connections are kept between application restarts). |
| 32 | + |
| 33 | +However, there is a trade-off of having to use a separate domain name for AnyCable server (e.g., `ws.myapp.whatever`). That might require taking additional care of authentication (e.g., cookie-sharing). We recommend using AnyCable's built-in [JWT authentication](/anycable-go/jwt_identification) to not worry about that. |
| 34 | + |
| 35 | +> See this [demo PR](https://github.com/anycable/anycable_rails_demo/pull/37) for a complete configuration example. |
| 36 | +
|
| 37 | +Here is a `config/deploy.yml` example with the AnyCable accessory: |
| 38 | + |
| 39 | +```yaml |
| 40 | +# ... |
| 41 | + |
| 42 | +accessories: |
| 43 | + # ... |
| 44 | + anycable-go: |
| 45 | + image: anycable/anycable-go:1.6 |
| 46 | + host: 192.168.0.1 |
| 47 | + proxy: |
| 48 | + host: ws.demo.anycable.io |
| 49 | + ssl: true |
| 50 | + app_port: 8080 |
| 51 | + healthcheck: |
| 52 | + path: /health |
| 53 | + env: |
| 54 | + clear: |
| 55 | + ANYCABLE_HOST: "0.0.0.0" |
| 56 | + ANYCABLE_PORT: 8080 |
| 57 | + ANYCABLE_BROADCAST_ADAPTER: http |
| 58 | + ANYCABLE_HTTP_BROADCAST_PORT: 8080 |
| 59 | + secret: |
| 60 | + - ANYCABLE_SECRET |
| 61 | +``` |
| 62 | +
|
| 63 | +The important bits are: |
| 64 | +
|
| 65 | +- `proxy` configuration for `anycable` accessory; it's required to server incoming traffic via Kamal |
| 66 | + |
| 67 | +- we configure AnyCable to receive broadcast HTTP requests on the same port served by Kamal Proxy to avoid publishing any additional ports; specifying `ANYCABLE_SECRET` is required to ensure your HTTP broadcasting endpoint is secured. |
| 68 | + |
| 69 | +The example above uses HTTP broadcasting. If you want to use Redis, it will look as follows: |
| 70 | + |
| 71 | +```yaml |
| 72 | +# Name of your service defines accessory service names |
| 73 | +service: anycable_rails_demo |
| 74 | +
|
| 75 | +# ... |
| 76 | +
|
| 77 | +accessories: |
| 78 | + # ... |
| 79 | + redis: |
| 80 | + image: redis:7.0 |
| 81 | + host: 192.168.0.1 |
| 82 | + directories: |
| 83 | + - data:/data |
| 84 | + anycable-go: |
| 85 | + image: anycable/anycable-go:1.6 |
| 86 | + host: 192.168.0.1 |
| 87 | + proxy: |
| 88 | + host: ws.demo.anycable.io |
| 89 | + ssl: true |
| 90 | + app_port: 8080 |
| 91 | + healthcheck: |
| 92 | + path: /health |
| 93 | + env: |
| 94 | + clear: |
| 95 | + ANYCABLE_HOST: "0.0.0.0" |
| 96 | + ANYCABLE_PORT: 8080 |
| 97 | + ANYCABLE_REDIS_URL: "redis://anycable_rails_demo-redis:6379/0" |
| 98 | +``` |
| 99 | + |
| 100 | +Note that if you want to run AnyCable servers on multiple hosts and use Redis for pub/sub, you must provide the same static Redis address for all AnyCable accessories (and better protect it at least via a password): |
| 101 | + |
| 102 | +```yaml |
| 103 | +accessories: |
| 104 | + # ... |
| 105 | + redis: |
| 106 | + host: <%= ENV.fetch('REDIS_HOST') %> |
| 107 | + image: redis:8.0-alpine |
| 108 | + port: "6379:6379" |
| 109 | + cmd: redis-server --requirepass <%= ENV.fetch("REDIS_PASSWORD") %> |
| 110 | + volumes: |
| 111 | + - redisdata:/data |
| 112 | + anycable-go: |
| 113 | + image: anycable/anycable-go:1.6 |
| 114 | + host: <%= ENV.fetch("ANYCABLE_HOST") %> |
| 115 | + proxy: |
| 116 | + # .. |
| 117 | + env: |
| 118 | + clear: |
| 119 | + ANYCABLE_HOST: "0.0.0.0" |
| 120 | + ANYCABLE_PORT: 8080 |
| 121 | + ANYCABLE_REDIS_URL: "redis://:<%= ENV.fetch("REDIS_PASSWORD") %>@<%= ENV.fetch("REDIS_HOST") %>:6379/0" |
| 122 | +``` |
| 123 | + |
| 124 | +The example above assumes that we store various configuration parameters such as IP addresses in the `.env` file (so, the actual configuration is _parameterized_). See the full example [here](https://github.com/anycable/anycable_rails_demo/pull/39). |
| 125 | + |
| 126 | +#### Using Embedded NATS |
| 127 | + |
| 128 | +AnyCable can run with an embedded NATS server, eliminating the need for Redis: |
| 129 | + |
| 130 | +```yaml |
| 131 | +accessories: |
| 132 | + # ... |
| 133 | + anycable-go: |
| 134 | + host: <%= ENV.fetch("ANYCABLE_HOST") %> |
| 135 | + image: anycable/anycable-go:1.6.2-alpine |
| 136 | + env: |
| 137 | + clear: |
| 138 | + <<: *default_env |
| 139 | + ANYCABLE_HOST: "0.0.0.0" |
| 140 | + ANYCABLE_PORT: "8080" |
| 141 | + ANYCABLE_EMBED_NATS: "true" |
| 142 | + ANYCABLE_PUBSUB: nats |
| 143 | + ANYCABLE_BROADCAST_ADAPTER: "http" |
| 144 | + ANYCABLE_HTTP_BROADCAST_PORT: 8080 |
| 145 | + ANYCABLE_ENATS_ADDR: "nats://0.0.0.0:4242" |
| 146 | + ANYCABLE_ENATS_CLUSTER: "nats://0.0.0.0:4243" |
| 147 | + secret: |
| 148 | + - ANYCABLE_SECRET |
| 149 | + options: |
| 150 | + publish: |
| 151 | + - "4242:4242" |
| 152 | + - "4243:4243" |
| 153 | + proxy: |
| 154 | + host: <%= ENV.fetch("WS_PROXY_HOST") %> |
| 155 | + ssl: true |
| 156 | + app_port: 8080 |
| 157 | + healthcheck: |
| 158 | + path: /health |
| 159 | + interval: 1 |
| 160 | + timeout: 5 |
| 161 | +``` |
| 162 | + |
| 163 | +The complete example of deploying AnyCable with embedded NATS via Kamal can be found in [this PR](https://github.com/anycable/anycasts_demo/pull/19). |
| 164 | + |
| 165 | +## Deploying gRPC servers |
| 166 | + |
| 167 | +AnyCable RPC server using gRPC transport should be deployed as separate _server role_ (not an accessory), since it serves your application. Thus, you must add to the list of servers as follows: |
| 168 | + |
| 169 | +```yaml |
| 170 | +service: anycable_rails_demo |
| 171 | +
|
| 172 | +servers: |
| 173 | + web: |
| 174 | + - 192.168.0.1 |
| 175 | +
|
| 176 | + anycable-rpc: |
| 177 | + hosts: |
| 178 | + - 192.168.0.1 |
| 179 | + cmd: bundle exec anycable |
| 180 | + proxy: false |
| 181 | + options: |
| 182 | + network-alias: anycable_rails_demo-rpc |
| 183 | +
|
| 184 | +accessories: |
| 185 | + # ... |
| 186 | + anycable-go: |
| 187 | + # ... |
| 188 | + env: |
| 189 | + clear: |
| 190 | + ANYCABLE_HOST: "0.0.0.0" |
| 191 | + ANYCABLE_PORT: 8080 |
| 192 | + ANYCABLE_RPC_HOST: anycable_rails_demo-rpc:50051 |
| 193 | + secret: |
| 194 | + - ANYCABLE_SECRET |
| 195 | +``` |
| 196 | + |
| 197 | +The important bits are: |
| 198 | + |
| 199 | +- `proxy: false` is required to skip Kamal Proxy (it doesn't support gRPC) |
| 200 | + |
| 201 | +- `network-alias: anycable_rails_demo-rpc` allows us to use an fixed Docker service name to access the RPC server container from the accessory. |
| 202 | + |
| 203 | +### Scaling gRPC servers horizontally |
| 204 | + |
| 205 | +> See this [demo PR](https://github.com/anycable/anycable_rails_demo/pull/39) for a complete configuration example. |
| 206 | + |
| 207 | +AnyCable-Go 1.6.2+ supports the `grpc-list://` scheme to connect to multiple RPC endpoints. This way, you can spread RPC traffic accross machines: |
| 208 | + |
| 209 | +```yaml |
| 210 | +# ... |
| 211 | +servers: |
| 212 | + web: |
| 213 | + # ... |
| 214 | +
|
| 215 | + rpc: |
| 216 | + hosts: <%= ENV.fetch("RPC_HOSTS").split(",") %> |
| 217 | + cmd: bundle exec anycable |
| 218 | + env: |
| 219 | + clear: |
| 220 | + <<: *default_env |
| 221 | + ANYCABLE_RPC_HOST: "0.0.0.0:50051" |
| 222 | + options: |
| 223 | + publish: |
| 224 | + - "50051:50051" |
| 225 | + proxy: false |
| 226 | +
|
| 227 | +accessories: |
| 228 | + # ... |
| 229 | + anycable-go: |
| 230 | + host: <%= ENV.fetch("WS_HOSTS") %> |
| 231 | + image: anycable/anycable-go:1.6.2-alpine |
| 232 | + env: |
| 233 | + clear: |
| 234 | + <<: *default_env |
| 235 | + ANYCABLE_HOST: "0.0.0.0" |
| 236 | + ANYCABLE_PORT: "8080" |
| 237 | + # Using a fixed list of RPC addresses https://docs.anycable.io/deployment/load_balancing?id=using-a-fixed-list-of-rpc-addresses |
| 238 | + ANYCABLE_RPC_HOST: "grpc-list://<%= ENV.fetch("RPC_HOSTS").split(",").map { "#{_1}:50051" }.join(",") %>" |
| 239 | + proxy: |
| 240 | + # ... |
| 241 | +``` |
| 242 | + |
| 243 | +**IMPORTANT**: The setup above expose the gRPC server to the public (so it's reachable from other machines). We recommend securing access either by setting up firewall rules / virtual network within the cluster or using TLS with a private certificate for gRPC (see [configuration docs](https://docs.anycable.io/anycable-go/configuration?id=tls)). |
| 244 | + |
| 245 | +Alternatively, you may consider adding a standalone load balancer with gRPC support (this is out of scope of this guide). |
0 commit comments