Kirby Aguilar
Posted on October 8, 2024
Dev work is tough. Most of the time, deployment is even tougher, by a mile.
My previous workplace was run by a team that lacked experience in getting an app from zero to production. We had a starter react + rails app in our hands, but the details of the final step--putting our app online for users to consume--was amorphous at best. Our whiteboard was inked with a "let's use Elastic Beanstalk," so I was told to do just that.
Attempting to deploy to EBS in 2024 was nothing short of a nightmare. Every step took 20 minutes, where you prayed that nothing would fail (but something would fail every time). You couldn't even read the logs!
A few days passed, and I was unable to get a starter rails API app on EBS, much less our actual app. I started looking into taking a different approach.
Enter Dokku 🐋
We looked into what was popular in 2022-2024 (hatchbox, render, heroku, etc) and Dokku stood out. It was popular, it was open source, it ran on lower-end machines, it was easy to set up, and we could stay on AWS if we put it into EC2 instances. It looked very, very promising.
Of course, despite its popularity, there were very few guides for deployment, and practically no guides for our exact setup (AWS, React, Rails). Figuring out how to fulfill the promises was something I'd have to do on my own.
What we want to achieve in this guide
- Set up an app with react/vite as a frontend, and ruby on rails as a backend. The sample app should be something minimal: one page on the frontend, with a demonstration that the frontend can call the backend, which should be connected to a database.
- Deploy said app to AWS, while highlighting considerations re: dokku and infrastructure
By the way, if you're reading this and following along, I'm assuming you have some knowledge of how to operate within AWS. I won't be going into beginner-level detail for infrastructure-related steps
The app
Details re: the app aren't the focus of this guide, so it's recommended to just clone this GitHub repository and create a new master key.
If you're interested in how the app was set up, you can take a look at the README in the source code, which also covers the prerequisite installs for doing so.
Details that you should know about the app
To connect rails and react, we leverage the fact that npm run build
compiles everything into a singular index.html
. We place this in Rails' public/
directory. Settings for this can be found within frontend/vite.config.ts
and frontend/package.json
.
All routing within the app falls under two categories:
- (1) API routes, which start with
/api
. Requests to these are handled by the appropriate rails controller - (2) Every other URL pattern is caught by the line
get '*path', to: 'static#frontend'
inroutes.rb
. Our static page controller redirects the request to the aforementionedindex.html
, after which something like react-router can take over.
Most importantly: we need to create a package.json
within the rails folder. Like Heroku, Dokku operates on buildpacks, which are sets of instructions to build your app. These work off of activation conditions, e.g. your app is a ruby app if there is a Gemfile
in the root folder.
Because our app is React + RoR, we need to use both the node and ruby buildpacks. It's surprisingly very hard to just specify that everything under the frontend
directory is the node app, so we instead "trick" Dokku by providing a package.json
in our root directory. This file tells Dokku to cd
to the frontend directory and build from there.
As for expected behavior from the app, we want our root URL to point to a page with hello world, which upon loading will query our database for a list of employees and display the JSON in the frontend. Something like this:
High level infrastructure
The infrastructure for the project is pretty simple: end users send a request to a load balancer, which routes the request to an EC2 instance within an auto scaling group. Each individual EC2 instance is powered by Dokku, with a container running our rails + react app. We also have one central RDS database that the EC2 instances connect to.
Step-by-step infrastructure setup
Create a VPC
Create a VPC along with its associated resources. Make sure to have two availability zones with one public subnet each. Do not create private subnets. Do not add NAT gateways.
Create an RDS Postgres database
Standard create -> PostgreSQL database
The free tier template and defaults should be enough. No need to connect to a specific ec2 instance because we'll connect later.
Create/edit security groups to allow communication between the RDS db and EC2 instances
First security group (db's security group):
- allow postgresql inbound from second security group
- this security group is assigned to the RDS DB
Second security group (EC2-to-db-name):
- allow postgresql outbound to first security group
- this security group is assigned to EC2 instances
Store the necessary secrets in AWS secrets manager
Secrets to store:
- RDS access credentials
- SSH key to be used with dokku
RAILS_MASTER_KEY
- Github personal access token of an account with read access to your repo
Create an IAM role to access secrets
IAM role should:
- be for the EC2 AWS service
- have the
AmazonRDSFullAccess
permission - have the
SecretsManagerReadWrite
permissions
This IAM role will be part of the launch template.
Create a security group for the EC2 instances
Inbound:
- SSH only from your IP
- HTTP from anywhere (allow web traffic)
Outbound:
- IPv4, all traffic
Create a launch template
launch template instance parameters:
- auto-scaling guidance:
true
- AMI:
ubuntu-22.04
- instance type:
t2.small
(Dokku says it runs on anything with 1GB memory and higher, but I've found that it stalls hard unless you have at least 2GB) - subnet:
do not include in launch template
- security group: use security groups from above
- auto-assign public IP:
enable
- auto-assign public IP:
- storage: 24GB of
gp2
volume type - advanced details, IAM: use IAM from earlier
- advanced details, user data: we use a shell script. The source code will be in the same directory as this README.
Create an auto scaling group
Use the prior launch template. Select the VPC you created earlier. For the subnet, select the public subnets made available through creating the VPC.
While creating an ASG, you can attach it to a new load balancer. The load balancer should be:
- An application load balancer
- Internet-facing
This process will also prompt you to create a target group.
Enable health checks, and set the health check grace period + default instance warmup to 900 seconds (15 minutes).
Scaling: minimum capacity 1, maximum capacity 4
Instance maintenance policy: use the "availability" template
Enabling HTTPS (optional)
To enable HTTPS, request a certificate from AWS certificate manager on the domain. We assume that the domain is managed through AWS's route 53.
This certificate will be attached to the load balancer. This can be done through setting the LB's default SSL/TLS certificate under the LB's listeners and rules.
Make sure to adjust security groups such that HTTPS communication is allowed.
EC2 launch script and dokku settings
The EC2 launch script goes through dokku setup (and the installation of everything else you need in the instance). This should run automatically if set in your launch template's user data
section, but I recommend booting up an instance and running things manually to see what the setup process is like.
Output for the EC2 launch script can be found in /var/log/cloud-init-output.log
.
Notes:
- Some of this may be slightly outdated (e.g. the dokku version), so feel free to use updated version of the commands
- Replace
ror-react-dokku
with your project name - Replace fields like
your-rails-master-key
with the appropriate secret ID that you provided in secrets manager - We also make some assumptions re: the structure of secrets under secrets manager, so this may need some tweaking depending on how you set yours up
#!/bin/bash
# aws cli prerequisite
sudo apt-get install unzip
# aws cli installation
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
sudo unzip awscliv2.zip
sudo ./aws/install
# confirm aws installation
aws --version
# install dokku
wget -NP . https://dokku.com/install/v0.33.8/bootstrap.sh
sudo DOKKU_TAG=v0.33.8 bash bootstrap.sh
# set up SSH key using secrets manager
aws secretsmanager get-secret-value --secret-id your-ssh-key-in-secrets-manager --query SecretString --output text | jq -r '.["ssh-key"]' | sudo dokku ssh-keys:add admin
# set dokku domain to public IP of EC2 instance
dokku domains:set-global $(curl -s http://checkip.amazonaws.com)
# create app
dokku apps:create ror-react-dokku
# enable domains
dokku domains:enable ror-react-dokku
# set buildpack
# note that the nodejs buildpack MUST come before the ruby one
dokku buildpacks:add ror-react-dokku https://github.com/heroku/heroku-buildpack-nodejs
dokku buildpacks:add ror-react-dokku https://github.com/heroku/heroku-buildpack-ruby
# set rails master key
dokku config:set ror-react-dokku RAILS_MASTER_KEY=$(aws secretsmanager get-secret-value --secret-id your-rails-master-key --query SecretString --output text | jq -r '.RAILS_MASTER_KEY')
# set NODE_ENV to development
# this is required to successfully build during deploy
dokku config:set ror-react-dokku NODE_ENV=development
# set db related environment variables
dokku config:set ror-react-dokku DB_NAME=postgres
dokku config:set ror-react-dokku DB_PORT=5432
dokku config:set ror-react-dokku DB_HOST=$(aws secretsmanager get-secret-value --secret-id your-db-credentials --query SecretString --output text | jq -r '.["host"]')
dokku config:set ror-react-dokku DB_USERNAME=$(aws secretsmanager get-secret-value --secret-id your-db-credentials --query SecretString --output text | jq -r '.["username"]')
dokku config:set ror-react-dokku DB_PASSWORD=$(aws secretsmanager get-secret-value --secret-id your-db-credentials --query SecretString --output text | jq -r '.["password"]')
# port forward http
dokku ports:add ror-react-dokku http:80:5000
# remove nginx default page and restart nginx (without this, nginx page will show on your root route)
sudo rm /etc/nginx/sites-enabled/default
dokku nginx:stop
dokku nginx:start
# configure git auth to connect to private repository
dokku git:auth github.com your-selected-github-user-username $(aws secretsmanager get-secret-value --secret-id your-personal-access-token --query SecretString --output text | jq -r '.["PAT"]')
# allow remote repository host
dokku git:allow-host github.com
# pull from the repository and build
dokku git:sync --build ror-react-dokku https://github.com/yourOrganization/ror-react-dokku main
Note that we're using git:sync
/ git deployment to get the app instead of something like git push dokku main
-- this is a more CI/CD friendly approach, where your pipeline can just ssh into each instance and run git:sync --build
after obtaining the necessary permissions
...and that should be it! Once your ASG instances are up and running (give them about 10 minutes), you can get the URL for your load balancer and see your app on the open web.
As mentioned earlier, for automated deployment, you'll preferably want to continue by configuring circleCI / github actions to obtain the ASG's instance IDs. From there, you can get their public IP addresses, SSH into them one by one and run git:sync --build
.
Recommendations for the future of this guide
- Use terraform or similar
- Elaborate re: setting up a CI/CD pipeline for this project
Final words
The contents of this guide synthesize the hardest, most engineer-y thing I did during my last position. In the span of a workweek and a half, I went from knowing very little about infrastructure and deployment to being able to set up the entire deployment process of our app.
There's a lot of value in hacking things together purely by yourself, but I also know a well-meaning guide from an internet stranger can be an absolute lifesaver. My hope is that this reaches even one person who's trying to do the same thing and not knowing how to do so.
References
- The absolutely golden Dokku official documentation
- sayadi's GitHub gist for obtaining for SSH-ing into an EC2 instance from CircleCI, for those who want to figure out CI/CD
Posted on October 8, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.