Discord is one of the premier instant communication platforms on the web right now. Free, easy to use, and just cool, Discord is one of the main hangout apps for gamers, developers, and just overall tech people. One of the coolest things about it is the ability to install bots on your servers that make some cool stuff. Bots that play music, text-based RPGs, dice rollers for RPGs (I made one), meme generators, and many more. In this blog post, I’m going to walk you through on all the steps needed to make your bot and host it for free (kinda free), provided you have some basic ruby
and git
knowledge.
Getting started
Why Ruby? Well, no reason at all. It’s just my favorite language at the moment of writing. Its also the language I have been using at work for these past few years. And, I find it’s just so simple and easy to understand for people who use it sparingly.
And why Heroku? It’s a hosting platform that integrates with Github and automatically provides a CI/CD pipeline that continuously deploys your changes. This way you can make changes to your bot, commit that with git
and your bot will just update itself on Heroku. We just need a little hack to make it work there but will get to that.
So some prerequisites:
- Discord account
- Github account
- Heroku account
- Basic Ruby knowledge
- Ruby (latest version) and Git installed and available on your path
After you create all of these accounts, make sure you create a new Discord app. For that visit their developer portal. Click New Application
on the top right corner and follow along.
After creating it, on the bot’s main page you can take note of the CLIENT ID
, we are going to need it. Then go to the Bot
tab on the side menu and. On that new screen, you can add a bot user to your application. Do it and take note of the TOKEN
value also. All the other options are not needed for now, but you’ll probably explore those later on as you develop your bot.
The bot
Now make sure your directory dedicated to this project and also don’t forget to initialize a git repo, we are going to need it. Now, like most ruby projects, let’s start with a Gemfile
.
source "https://rubygems.org"
ruby "2.7.1"
gem "discordrb"
gem "dotenv"
gem "rake"
gem "zeitwerk"
After creating and saving this file, run bundle install
to install these dependencies.
Now, what do these gems do? Let’s go through them:
discordrb
- this is a wrapper of the Discord API. We are going to need this to effectively communicate with Discorddotenv
- we are going to use this to load the secret keys that we got from Discordrake
- the classic ruby task runner. We are going to use this to start the app.zeitwerk
- a class loader. We’ll set up this soon.
Now, let’s write the code that will connect to discord and handle incoming commands. Let’s place it on src/discord_bot.rb
.
module DiscordBot
throw "Lacking required secrets!" unless ENV["TOKEN"] && ENV["CLIENT_ID"]
@bot = Discordrb::Bot.new(
token: ENV["TOKEN"],
client_id: ENV["CLIENT_ID"],
)
puts "This bot's invite URL is #{@bot.invite_url}"
puts "Click on it to invite it to your server"
@bot.message(with_text: "!ping") do |event|
event.respond "pong"
end
def self.run
@bot.run
end
def self.invite_url
@bot.invite_url
end
end
We use the discordrb
library to create a bot instance using the values from our shell environment.
Then, we define a handler for a bot command. The handler will intercept every message that matches !ping
and respond to it with a pong
message.
Then we just declare methods to run the bot and get the invite URL. The bot run
method connects to Discord via a persistent websocket connection. That’s how our bot reads all of the incoming messages from the servers it’s installed in.
To correctly set the environment, create a file called .env
and add the values you got on the Discord developer portal. The dotenv
gem will take care of loading all of those values into the environment, just when we run our application. On Heroku we will define these variables on their dashboard. But for now, the .env
file.
CLIENT_ID=the client id you got on discord dev
TOKEN=the client secret you got on discord dev
DO NOT FORGET TO ADD THIS FILE TO THE GITIGNORE OF YOUR PROJECT otherwise, you are leaking secret information to the public.
Now to wrap everything up, let’s write our Rakefile
so that we can run our discord bot.
ENV["RUBY_ENV"] ||= "development"
require "bundler/setup"
require "dotenv/load" if ENV["RUBY_ENV"] != "production"
Bundler.require(:default, ENV["RUBY_ENV"])
loader = Zeitwerk::Loader.for_gem
loader.push_dir(File.dirname(__FILE__) + "/src")
loader.setup
task :bot do
DiscordBot.run
end
We wrap the entire file in a conditional that checks if we are running the file with ruby and not requiring it from another ruby file. This is usually done on ruby scripts that are meant to be executed from the command line.
Then we require the bundler/setup
, so we can automatically require all the gems into the project. We then require dotenv/load
only if we are outside of a production environment. During production, we don’t want to load environment variables from the .env
file, we just want to load them from the Heroku configuration.
We also set up zeitwerk
, which will automatically require our own files. In this case, we have a src/discord_bot.rb
file which defines a DiscordBot
module. Check this link for more info on Zeitwerk file structure. We push the src
dir as a root dir for our code and then we run setup to load it.
Then we define the task to run the bot. Then, to start it, just run bundle exec rake bot
. Now use the invite URL that shows up in the console and try it on your server! The bot should appear on the online user list and it should respond to a !ping
message with pong
.
How to put this online
Now the bot works great and it’s even ready for deployment. You host this on any server that you might have handy. Just send this code over and run rake bot
. But, most of us don’t have handy servers there. This is where Heroku comes in.
Heroku is a hosting platform for web apps. Web apps is an important term. Meaning that Heroku expects that your app is an HTTP server of some sort, that binds to a port and responds to incoming request. If you deploy our code to Heroku, it will work for a couple of minutes before shutting down. This is because Heroku shuts down any app that does not bind to any port! So we just need to add a webserver to it.
Sinatra
We are going to spin up a very simple Sinatra webserver that will have a simple HTML page that will contain the invite URL for our bot. It’s pretty useful stuff. You just share the Heroku app URL with your friends and they’ll be able to invite the bot to their servers.
Let’s go back to our Gemfile. Just the add gem "sinatra"
to it and then run bundle install
again.
Now let’s set up our webserver. We need to create a folder at src/webserver
and then one more subfolder at src/webserver/views
. The views
folder will hold all of our HTML templates. In this case, just one.
Then we create our HTML template at src/webserver/views/index.erb
.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The greatest bot</title>
<style>
body {
margin: 40px auto;
max-width: 650px;
line-height: 1.6;
font-size: 18px;
color: #444;
padding: 0 10px;
}
</style>
</head>
<body>
<h1>Greatest Bot</h1>
<p>
I am the greatest bot. Add me to your server with my glorious
<a href=<%= @url %>>invite url</a>.
</p>
<p>
This is a good place to post instructions on how
to use your bot by the way!
</p>
</body>
</html>
Then create a src/webserver.rb
file and a src/webserver
folder. On our src/webserver.rb
we are going to define the homepage route of our web app.
class Webserver < Sinatra::Base
set :views, File.dirname(__FILE__) + "/webserver/views"
get "/" do
@url = DiscordBot.invite_url
erb :index
end
end
When we receive a request on the /
route, we set the @url
with the bot’s invite URL. Then we render the index.erb
.
Finally, we just need to add another task to our Rakefile
file to boot up the webserver and the bot at the same time. Add this to the Rakefile
.
task :web do
Rack::Server.start(
Port: ENV["PORT"] || 4567,
app: Webserver.new,
)
end
task :all do
Process.fork { Rake::Task["bot"].invoke }
Process.fork { Rake::Task["web"].invoke }
Process.waitall
end
The web task boots up a webserver that listens for requests at the port defined on our environment or 4657. By default, Heroku adds a PORT
variable that holds the port we need to bind to. On the other hand, the all
task spawns two processes that will run at the same time. One for the webserver and other for the Discord bot itself. This will allow us to run the entire thing for free, more details on that later.
During development, you can always just work on the bot or the webserver, or both at the same time.
The final step we need is to create the Procfile
. On this file, we specify what Heroku will run. In this case, we want Heroku to run everything in one dyno, the web
dyno. Heroku can run multiple dynos
of different kinds. We could even have separate dynos, one for the bot and other for the webserver, but to fit on the free tier, we can only use one without running into limits.
web: rake all
Now, to run the bot and the server at the same time do rake all
. This will start both scripts and it will display its outputs on different colors. Very useful for debugging and seeing what’s happening. Oh, and you can visit your shiny website at http://localhost:5000.
Remember, that with this setup you can always run just the bot or just the webserver locally. rake bot
or rake web
will do the trick. Just in case you don’t want to work on the entire thing.
Heroku
Now, to deploy all of this to Heroku make sure you have all of this code on a Github repo. Then create a new app on Heroku and use the Connect with Github
function. Then just follow to connect with your Github repo. Finally, click the Deploy branch
button to deploy your code. You can optionally enable automatic deploys so every time you commit to master, you trigger a new build.
Your app will probably not work right away. You need to set the correct environment variables for it to work. Go to the Settings
tab on Heroku. Then on the Config Vars
zone click reveal and then introduce the following variables:
RUBY_ENV=production
CLIENT_ID=your client id from discord dev portal
TOKEN=your secret token id from discord dev portal
You can use the same values you have on your .env
file for the discord secrets. But note that is a best practice to have different apps and different values for the development and production build. If your bot is used by many people, it’s probably a bad idea to use the same bot app during development, because it’s already running an instance on Heroku and it might introduce downtime for its users. But all it takes is just creating another app as we did earlier, and use those new credentials here on Heroku.
After setting the environment variables, Heroku should automatically re-release the app. Click the Open App
in the top-right area. It should take you to the website with the invite URL. Use it for your server, then test the bot as you did during development. By the way, the server on your own machine, in case you are using the same credentials for both production (Heroku) and development, just to make sure there are no interferences.
Disclaimer
Heroku offers a hobby tier that allows you to host an app for free, provided it runs only a few hours per day. Heroku automatically kills your app after it idles for a while. That’s based on web requests, so even if people are using the bot on Discord, that doesn’t count as traffic for Heroku. Our bot uses websockets to send and receive messages from Discord, so that doesn’t count for Heroku. More info on their docs page about the free tier.
Though, if you add a credit card to your account to validate it, you get enough hours to run a single dyno the entire month 24/7. By fitting all of our tasks into the rake all
task, we can just consume hours in a single dyno. Not ideal, but it works for our use case.
Next Steps
Now the bot is up running, but it doesn’t do much. The hardest part of getting it up and running on the outside world is done, but now it’s up to you to fill the rest in.
You should take a look at discordrb
wiki page and also their API documentation for more advanced stuff. Their API is plenty easy to work with.
You may also see a “real” bot in action using this template and these libraries. I made a bot a while back to roll dice. I use it just for fun and some online Dungeons & Dragons sessions. You can check the code here on my Github repo and you can also invite it to your server by visiting it’s home page.
A final note
This will work for hobby projects of sorts, but any bot with a significant amount of servers will perhaps need a little more horsepower to run, and the hobby tier from Heroku won’t cut it. Also, running two processes on one dyno is not the best way to go about a performant. But hey! We usually say that “premature optimization is the root of all evil”, so don’t sweat it. When things get slow, just split off the webserver and the bot in different dynos and boost them (that won’t be free anymore though).
I packaged the teachings of this blog post into a starter repo that you can use instead of following these steps. I’ll keep that repo updated but there might be the chance that the code is slightly different. People might find better ways to do things and I’ll happily accept that! Anyway, the repo’s README will get you sorted. Check it at my Github repo.
Have fun!