← Back to posts

3 months ago

💻 Programming

How to deploy Rails/Rack app on Hetzner without Kamal/Docker with HTTPS

1. Starting a server

We will use ssh key for ssh login for our server. So first, we need to create a one:
cd ~/.ssh && ssh-keygen -f hetzner && cat hetzner.pub
Copy the contents of hetzner.pub to clipboard.

Go to console.hetzner.com, create a new project or use the Default. Select the project and go to Security → Add SSH key. Paste the content there. Next go to 
Servers and start a new server. Here's an example of server configuration:
starting server
Save the server's IP address somewhere, e.g. to bash variable:
ip=5.161.217.141

2. Point DNS to the server

Consider, that it takes time for DNS updates to propagate through the internet.
 
We need to wait until  
host sample.com
returns a correct IP address. While waiting we can do other steps.

4. Create deployer user and copy .ssh folder

Run locally:
ssh-add ~/.ssh/hetzner
ssh root@$ip
 Now on the machine from the root user run
adduser --disabled-password deployer < /dev/null
cp -r ~/.ssh /home/deployer/
cd /home/deployer/
chown -R deployer:deployer .ssh
This will allow us to login without password to deployer user

5. Make deployer an admin

 From the root run
sudo usermod -aG sudo deployer
and add at the bottom of the file
deployer ALL=(ALL) NOPASSWD:ALL
This gives sudo permissions to deployer without asking for password. Now relogin as a deployer:
ssh deployer@$ip

6. Install Ruby

First install rbenv:
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
source ~/.bashrc
rbenv -v
Then ruby-build
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
And install ruby version that you want
sudo apt install -y git curl build-essential libssl-dev libreadline-dev zlib1g-dev \
  libsqlite3-dev libyaml-dev libffi-dev libgdbm-dev libncurses5-dev \
  libncursesw5-dev libdb-dev libgmp-dev
rbenv install 3.4.5
ruby -v

7. Creating a sample app

First, let's create a sample Rails app:
rails new sample --database=postgresql
cd sample
git init
git add .
git commit -m 'initial'
 And let it be a todo list app, so let's add a Task scaffold:
rails g scaffold tasks title
rails db:create db:migrate
git add .
git commit -m 'task scaffold'
Now let's add a psql password for production
EDITOR=vim  rails credentials:edit --environment production
and add this password under, e.g.
db_password: your_secure_password
and in database.yml replace password and user name to postgres:
production:
  primary: &primary_production
    <<: *default
    username: postgres
    password: <%= Rails.application.credentials.dig(:db_password) %>
Commit and push the changes
git add .
git commit -m 'db password added'
We will use Github for storing our code and pulling from remote repository to the server, so create a repository there and then push the code to it
git push origin HEAD
Go to the server and pull the repo, notice that we use -A flag to pass github ssh agent:
ssh -A deployer@$ip
Clone the repo URL into home directory
git clone $ssh_repo_url
And move this directory on /var/www (we use sudo as /var/www doesn't belong to deployer)
sudo mv sample /var/www

8. Setting up Postgres

Now upload the production key via scp from the local machine:
scp config/credentials/production.key deployer@$ip:/var/www/sample/config/credentials/

On the server run:
sudo apt update && sudo apt install -y postgresql postgresql-contrib
Create a password postgres user:
sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD 'your_secure_password';"
And change authentication method to scram-sha-256 by replacing all md5 to scram-sha-256 in this file:
sudo vim /etc/postgresql/$(ls /etc/postgresql)/main/pg_hba.conf
Restart postgres
sudo systemctl restart postgresql
Install gems and if needed the ruby version for the project
cd /var/www/sample
rbenv install $(cat .ruby-version)
bundle install

RAILS_ENV=production rails db:create db:migrate

9. Service configuration

The app will be running at 127.0.0.1:3030 and won't be accessible from the internet, but nginx will redirect all the traffic as reverse proxy. 
 
in config/puma.rb replace
# Specifies the port that Puma will listen on to receive requests; default is 3000. 
port ENV.fetch("PORT") { 3000 } 
with 
bind "tcp://127.0.0.1:3030"
 
Push the changes and pull them on the server.

Next, create a service in
sudo vim /etc/systemd/system/sample.service 
with this content (change it to your own liking):
[Unit]
Description=My Sample Rails Application
After=network.target

[Service]
Type=simple
User=deployer
WorkingDirectory=/var/www/sample
Environment=RAILS_ENV=production
Environment=PATH=/home/deployer/.rbenv/shims:/home/deployer/.rbenv/bin:/usr/local/bin:/usr/bin:/bin
Environment=RBENV_ROOT=/home/deployer/.rbenv
ExecStart=/bin/bash -lc 'exec /home/deployer/.rbenv/shims/bundle exec puma -C config/puma.rb'
Restart=always
StandardOutput=append:/var/log/sample.log
StandardError=append:/var/log/sample.error.log

[Install]
WantedBy=multi-user.target
Next:
sudo systemctl enable sample 

sudo systemctl start sample
From now on we can check if curl returns html:
curl 127.0.0.1:3030

10. Nginx configuration

install nginx :
sudo apt install nginx
I'll use sample.karganyan.dev as a host, use your own.
 sudo vim /etc/nginx/sites-available/sample.karganyan.dev
Insert this to the file:
server {
  server_name sample.karganyan.dev;   
  listen 80;     
  listen [::]:80;    
  root /var/www/sample/public;      
  access_log /var/log/nginx/sample.access.log;     
  error_log  /var/log/nginx/sample.error.log;      
  location / {         
    proxy_pass http://127.0.0.1:3030;         
    proxy_set_header Host $host;         
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  
    proxy_set_header X-Forwarded-Proto $scheme;         
    proxy_set_header X-Forwarded-Ssl on; # needed if SSL is added later         
    proxy_redirect off;     
  }      
  location ~ ^/(assets|packs|uploads)/ {         
    expires max;         
    add_header Cache-Control public;     
  }      
  error_page 500 502 503 504 /500.html;     
  client_max_body_size 50M;     
  keepalive_timeout 10;      

} 
Install certbot
sudo apt install certbot python3-certbot-nginx -y
Delete default nginx file
sudo rm /etc/nginx/sites-enabled/default
Replace with your host
sudo certbot --nginx -d sample.karganyan.dev
Restart nginx
sudo nginx -t

sudo systemctl reload nginx

Go to your host and the site should work now.
sample.karganyan.dev/tasks

11. Redeployment

to redeploy use 
 
git push && ssh -A deployer$ip "cd /var/www/sample && git pull && PATH=/home/deployer/.rbenv/shims:/home/deployer/.rbenv/bin:/usr/local/bin:/usr/bin:/bin && RAILS_ENV=production bin/rails db:migrate && sudo systemctl restart sample"

Comments

Please Sign in to write a comment