Most of this guide is generic and can be applied to other providers, but since fly.io has a free tier that we can use to run bots it's a great way to start into deploying bots.
Setup Fly App
If you already have the app running in Fly, you can skip this section.
The free tier on fly.io allows you to have 3 machines with size shared-cpu-1x@256MB, for this example setup we'll create one for the elixir application and one for postgresql.
First we need to install the fly command utility, follow the instructions for your platform: https://fly.io/docs/hands-on/install-flyctl/
We want to use our own Dockerfile, because we have more control in how we deploy our application, here is the Dockerfile that I use:
In this example the application is called my_bot, change the path in the CMD command with your app's name
DockerfileFROM hexpm/elixir:1.16.2-erlang-26.2.3-alpine-3.19.1 as base RUN mkdir /app WORKDIR /app RUN apk --no-cache add g++ make git && mix local.hex --force && mix local.rebar --force FROM base as test COPY . /app FROM base AS app_builder ENV MIX_ENV=prod # copy only deps-related files COPY mix.exs mix.lock ./ COPY config config RUN mix deps.get --only $MIX_ENV COPY config/config.exs config/${MIX_ENV}.exs config/ RUN mix deps.compile # at this point we should have a valid reusable built cache that only changes # when either deps or config/{config,prod}.exs change COPY priv priv COPY lib lib COPY config/runtime.exs config/ # COPY rel rel # could contain rel/vm.args.eex, rel/remote.vm.args.eex, and rel/env.sh.eex RUN mix release FROM alpine:3.19.1 as app RUN apk add --no-cache bash openssl libgcc libstdc++ ncurses-libs RUN adduser -D app COPY --from=app_builder /app/_build . RUN chown -R app:app /prod USER app CMD ["./prod/rel/my_bot/bin/my_bot", "start"].dockerignore# flyctl launch added from .elixir_ls/.gitignore .elixir_ls/**/* # flyctl launch added from .gitignore # The directory Mix will write compiled artifacts to. _build # If you run "mix test --cover", coverage assets end up here. cover # The directory Mix downloads your dependencies sources to. deps # Where third-party dependencies like ExDoc output generated docs. doc # Ignore .fetch files in case you like to edit your project deps locally. .fetch # If the VM crashes, it generates a dump, let's ignore it too. **/erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). **/*.ez # Ignore package tarball (built via "mix hex.build"). **/my_bot-*.tar # Temporary files, for example, from tests. tmp # flyctl launch added from .lexical/.gitignore .lexical/**/* fly.toml
Now we'll execute fly launch --no-deploy to generate our base fly.toml.
We're about to launch your app on Fly.io. Here's what you're getting:
Organization: <Name> (fly launch defaults to the personal org)
Name: my-bot (derived from your directory name)
Region: <Region> (this is the fastest region for you)
App Machines: shared-cpu-1x, 1GB RAM (most apps need about 1GB of RAM)
Postgres: <none> (not requested)
Redis: <none> (not requested)
Sentry: false (not requested)
? Do you want to tweak these settings before proceeding? (y/N)
We want to edit this values, let's select y, this will open a tab in your browser to finish configuring your application, the values that I have changed are:
- App name: Write whatever app name you want
- VM Memory: 256MB, I want to use the free tier, so I have to use the 256MB VMs
- Postgres: Setup a postgres database, pick whatever name you want, and select the "Development" configuration in order to have only one machine and keep it in the free tier.
That's all I changed, but feel free to tweak what you want.
Now I changed the fly.toml to only have one instance of my app instead of two, and to not stop the machines when idle, but you can keep it at two:
app = <your-app>
primary_region = <your-region>
[build]
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = false
auto_start_machines = false
min_machines_running = 0
processes = ['app']
[[vm]]
size = 'shared-cpu-1x'
count = 1Now, everytime we want to deploy the application, we just need to run fly deploy.
Updating the bot to webhook
If you have the bot setup to use polling, you can already deploy the application and it will work right away, but if you want to use the benefit of having the application deployed, you will want to use the webhook mode to improve performance and use less resources.
For that, first we need to change the config files, I want to keep polling on development/testing and webhook will be used only on production.
config/config.exsimport Config config :ex_gram, adapter: ExGram.Adapter.Req config :my_bot, MyBot.Bot, token: "YOUR_BOT_TOKEN", method: :polling, polling: [allowed_updates: []] import_config "#{config_env()}.exs"config/dev.exs
import Config
config :ex_gram, token: "YOUR_BOT_TOKEN"
config :my_bot, MyBot.Bot,
token: "YOUR_BOT_TOKEN",
method: :polling,
polling: [allowed_updates: []]config/prod.exs
import Configconfig/runtime.exs
import Config
if config_env() == :prod do
config :ex_gram, token: System.get_env("BOT_TOKEN")
config :my_bot, MyBot.Bot,
token: System.get_env("BOT_TOKEN"),
method: :webhook,
webhook: [
allowed_updates: [],
drop_pending_updates: false,
max_connections: 50,
secret_token: System.get_env("WEBHOOK_SECRET_TOKEN"),
url: "https://#{System.get_env("FLY_APP_NAME")}.fly.dev/",
# path: "/custom/path" # Optional: customize the webhook path (default: "/telegram")
]
endconfig/test.exs
import Config
config :ex_gram, token: "NOTHING", adapter: ExGram.Adapter.Test
config :my_bot, MyBot.Bot,
token: "test_token",
method: :test,
username: "testbot",
setup_commands: falseThe webhook configuration is on runtime.exs, and we can see that we are using two environment variables, let's set them up in our Fly application:
fly secrets set BOT_TOKEN=YOUR_BOT_TOKEN --stage
fly secrets set WEBHOOK_SECRET_TOKEN=WHATEVER_SECRET_TOKEN_YOU_WANT --stage
Now we need to add a couple of dependencies to listen on the port we want and setup the webhook plug.
mix.exs# ... defp deps do [ # ... # Add this two: {:plug_cowboy, "~> 2.7"}, {:plug, "~> 1.15"} ] end
We need to create a router, and plug the ExGram.Plug to route the updates:
lib/my_bot/router.ex
defmodule MyBot.Router do
use Plug.Router
# If you configured a custom path in the webhook options, pass it here too:
# plug(ExGram.Plug, path: "/custom/path")
plug(ExGram.Plug)
plug(:match)
plug(:dispatch)
get("/", do: send_resp(conn, 200, "Welcome"))
match(_, do: send_resp(conn, 404, "Oops, wrong path!"))
endAnd finally we just need to update our application.ex to add the router and get the new bot config
lib/my_bot/application.ex
@impl true
def start(_type, _args) do
bot_config = Application.get_env(:my_bot, MyBot.Bot)
children = [
ExGram,
{MyBot.Bot, bot_config},
{Plug.Cowboy, scheme: :http, plug: MyBot.Router, port: 8080}
]
opts = [strategy: :one_for_one, name: MyBot.Supervisor]
Supervisor.start_link(children, opts)
end