-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add guides #156
Open
rockneurotiko
wants to merge
1
commit into
master
Choose a base branch
from
add_guides
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add guides #156
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
# How to deploy a bot to Fly.io | ||
|
||
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 | ||
|
||
- `Dockerfile` | ||
``` dockerfile | ||
FROM 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` | ||
``` dockerfile | ||
# 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`. | ||
|
||
``` shell | ||
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: | ||
|
||
``` yaml | ||
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 = 1 | ||
``` | ||
|
||
Now, 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.exs` | ||
``` elixir | ||
import Config | ||
|
||
config :ex_gram, | ||
method: :polling, | ||
adapter: ExGram.Adapter.Tesla, | ||
polling: [allowed_updates: []] | ||
|
||
import_config "#{config_env()}.exs" | ||
``` | ||
|
||
- `config/dev.exs` | ||
|
||
``` elixir | ||
import Config | ||
|
||
config :ex_gram, token: "YOUR_BOT_TOKEN" | ||
``` | ||
|
||
- `config/prod.exs` | ||
|
||
``` elixir | ||
import Config | ||
``` | ||
|
||
- `config/runtime.exs` | ||
|
||
``` elixir | ||
import Config | ||
|
||
if config_env() == :prod do | ||
config :ex_gram, | ||
token: System.get_env("BOT_TOKEN"), | ||
method: :webhook, | ||
adapter: ExGram.Adapter.Tesla, | ||
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/" | ||
] | ||
end | ||
``` | ||
|
||
- `config/test.exs` | ||
|
||
``` elixir | ||
import Config | ||
|
||
config :ex_gram, token: "NOTHING", adapter: ExGram.Adapter.Test, updates: ExGram.Updates.Test | ||
``` | ||
|
||
The 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: | ||
|
||
``` shen | ||
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` | ||
``` elixir | ||
# ... | ||
|
||
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` | ||
|
||
``` elixir | ||
defmodule MyBot.Router do | ||
use Plug.Router | ||
|
||
plug(ExGram.Plug) | ||
|
||
plug(:match) | ||
plug(:dispatch) | ||
|
||
get("/", do: send_resp(conn, 200, "Welcome")) | ||
match(_, do: send_resp(conn, 404, "Oops, wrong path!")) | ||
end | ||
``` | ||
|
||
And finally we just need to update our `application.ex` to add the router and get the new config | ||
|
||
- `lib/my_bot/application.ex` | ||
|
||
``` elixir | ||
|
||
@impl true | ||
def start(_type, _args) do | ||
token = Application.get_env(:ex_gram, :token) | ||
method = Application.get_env(:ex_gram, :method) | ||
|
||
children = [ | ||
ExGram, | ||
{MyBot.Bot, method: method, token: token}, | ||
{Plug.Cowboy, scheme: :http, plug: MyBot.Router, port: 8080} | ||
] | ||
|
||
opts = [strategy: :one_for_one, name: MyBot.Supervisor] | ||
Supervisor.start_link(children, opts) | ||
end | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
# Configure multiple bots | ||
|
||
In this way we will explore different ways to configure multiple bots in the same application. | ||
|
||
In this guide, the elixir application is called `my_bot` and the bot's modules will be `MyBot.Bot1`, `MyBot.Bot2`, ... | ||
|
||
## Manually | ||
|
||
The simpler and easiest way to start different bots, is to setup in a specific configuration value the bot's configuration: | ||
|
||
``` elixir | ||
config :my_bot, | ||
bots: [ | ||
bot_name_1: [method: :polling, token: "TOKEN_BOT_1"], | ||
bot_name_2: [method: :polling, token: "TOKEN_BOT_2"] | ||
] | ||
``` | ||
|
||
NOTE: I recommend using the same name here than the one you use in your bots when doing `use ExGram.Bot, name: :bot_name_1` | ||
|
||
And now in your `application.ex`, manually configure the childs: | ||
|
||
``` elixir | ||
def start(_type, _args) do | ||
bots = Application.get_env(:my_bot, :bots) | ||
|
||
bot_config_1 = bots[:bot_name_1] | ||
bot_config_2 = bots[:bot_name_2] | ||
|
||
children = [ | ||
ExGram, | ||
{MyBot.Bot1, bot_config_1}, | ||
{MyBot.Bot2, bot_config_2} | ||
] | ||
|
||
opts = [strategy: :one_for_one, name: MyBot.Supervisor] | ||
Supervisor.start_link(children, opts) | ||
end | ||
``` | ||
|
||
## With a Dynamic Supervisor | ||
|
||
If you plan to have many bots, that you maybe want to be able to start/stop as you want, or to add/delete new bots easily, using a DynamicSupervisor will help you with it. | ||
|
||
We can keep the same configuration style, just change it to have the bot's module: | ||
|
||
``` elixir | ||
config :my_bot, | ||
bots: [ | ||
bot_name_1: [bot: MyBot.Bot1, method: :polling, token: "TOKEN_BOT_1"], | ||
bot_name_2: [bot: MyBot.Bot2, method: :polling, token: "TOKEN_BOT_2"] | ||
] | ||
``` | ||
|
||
Now we will create a bot's dynamic supervisor: | ||
|
||
- `lib/my_bot/bot_supervisor.ex` | ||
|
||
``` elixir | ||
defmodule MyBot.BotSupervisor do | ||
use DynamicSupervisor | ||
|
||
@spec start_link(any()) :: Supervisor.on_start_child() | :ignore | ||
def start_link(_init_arg) do | ||
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__) | ||
end | ||
|
||
@impl true | ||
def init(_init_arg) do | ||
DynamicSupervisor.init(strategy: :one_for_one) | ||
end | ||
|
||
def start_bots() do | ||
bots = Application.get_env(:my_bot, :bots) | ||
|
||
bots | ||
|> Enum.with_index() | ||
|> Enum.map(fn {{bot_name, bot}, index} -> | ||
%{ | ||
id: index, | ||
token: Keyword.fetch!(bot, :token), | ||
method: Keyword.fetch!(bot, :method), | ||
bot_name: bot_name, | ||
extra_info: Keyword.get(bot, :extra_info, %{}), | ||
bot: Keyword.fetch!(bot, :bot) | ||
} | ||
end) | ||
|> Enum.each(&start_bot/1) | ||
end | ||
|
||
def start_bot(bot) do | ||
name = String.to_atom("bot_#{bot.bot_name}_#{bot.id}") | ||
|
||
bot_options = [ | ||
token: bot.token, | ||
method: bot.method, | ||
name: name, | ||
id: name, | ||
bot_name: bot.bot_name, | ||
extra_info: bot.extra_info | ||
] | ||
|
||
child_spec = {bot[:bot], bot_options} | ||
|
||
{:ok, _} = DynamicSupervisor.start_child(__MODULE__, child_spec) | ||
end | ||
end | ||
``` | ||
|
||
|
||
NOTE: This sets the bot's name in line 40 (`bot_name: bot.bot_name`), this is done in order to allow to use the same bot module with different tokens, but it also implies that the name in the configuration is the one that will be used, and not the one setup in `use ExGram.Bot, name: <name>`, it only matters if you make direct calls to `ExGram` like `ExGram.send_message(..., bot: :bot_name)`, if you don't need to release different bots with the same bot's module, I recommend deleting that line. | ||
|
||
|
||
And finally, we just need to change our `application.ex` to start the supervisor and the bots: | ||
|
||
- `lib/my_bot/application.ex` | ||
``` elixir | ||
@impl true | ||
def start(_type, _args) do | ||
children = [ | ||
ExGram, | ||
MyBot.BotSupervisor, | ||
{Task, &MyBot.BotSupervisor.start_bots/0}, | ||
# ... | ||
] | ||
|
||
opts = [strategy: :one_for_one, name: MyBot.Supervisor] | ||
Supervisor.start_link(children, opts) | ||
end | ||
``` |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.