How to Deploy a Rails 7 Application with Capistrano, Nginx, Puma, Postgresql, LetsEncrypt on Ubuntu 20.04 / Amazon Linux 2
What are we going to do
So whenever I start a new project and want to deploy it to production, I need to research from scratch how to setup a Amazon Linux 2 (Ubuntu Server) including Firewall, how to setup Capistrano, get NGINX to work with Puma etc. That’s why I summarize everything I do to get a Project deployed.
Create the Rails project
Make sure you have Postgresql installed and running locally.
rails -v # Rails 7.0.3 rails new mysite --database=postgresql rake db:setup rails db:migrate rails s # rails s -b 0.0.0.0
Now you should be able to visit http://localhost:3000 in your browser.
Server Setup
Ubuntu 20.04 (hetzner cloud) or Amazon Linux 2 (AWS ec2 instance).
SSH Config
I always make sure to select my public key when I create the server so that one is already entered in the ~/.ssh/authorized_keys
file for the root user.
Otherwise we can generate a new key like below:
# https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent # Generating a new SSH key ssh-keygen -t ed25519 -C "your_email@example.com" # Adding your SSH key to the ssh-agent eval "$(ssh-agent -s)" # Add your SSH private key to the ssh-agent. If you created your key with a different name, or if you are adding an existing key that has a different name, replace id_ed25519 in the command with the name of your private key file. ssh-add ~/.ssh/id_ed25519
If you provide an SSH key, Hetzner will automatically disable Password authentication which provides an extra layer of security.
Make sure your /etc/ssh/sshd_config
file does not allow password authentication (#PasswordAuthentication yes
see the hash at the start of the line).
If you would need to change that, make sure to restart the ssh daemon afterwards (systemctl restart sshd
).
Set the A record
Once you purchased your server, you will receive a IPv4 Address. When you already purchased a domain for your project, make sure to point the A Record of that domain to your newly purchased Server’s IPv4 Address. I always do that before setting up my server because that might take a few minutes to propagate through the DNS Servers.
This A record needs to be fully propagated once we try to aquire LetsEncrypt certificates later on.
Update packages
Run
### Ubuntu 20.04 ### sudo apt-get update ### or for Amazon Linux 2 ### sudo yum -y update
to update your packages.
Firewall setup
Install ufw (uncomplicated firewall) and allow ssh, http and https.
If you are using Amazon Web Services (AWS) , skip this step.
apt-get install ufw ufw status ufw allow ssh ufw allow http ufw allow https ufw enable ufw status
Create the rails user
Let’s create a new user called rails
just for running the application.
adduser rails
This will create an interactive input for creating the user, set a password and just confirm the rest (ENTER).
We want to be able to ssh into our rails user. Lets copy our ~/.ssh/authorized_keys
file from the root user.
mkdir -p /home/rails/.ssh cp ~/.ssh/authorized_keys /home/rails/.ssh/ chown -R rails:rails /home/rails/.ssh/
This user needs to be a sudo user in order to restart the puma systemctl service we are going to create later on.
# TO SEE LIST OF GROUPS cat /etc/group ### Ubuntu 20.04 ### usermod -aG sudo rails ### Amazon Linux 2 ### sudo usermod -aG wheel rails vim /etc/sudoers # add the following line at the bottom rails ALL=(ALL) NOPASSWD: ALL
You should be able to ssh into your server with the rails user now.
ssh rails@mysite.com
Create ssh key as rails user to allow make connection with github and add public key to GitHub.
Create an Amazon RDS instance - AWS Documentation
Edit VPC security groups > inbound rules of RDS DB:
Add EC2 instance to security group to allow make connections between RDS and EC2 instance.
Install postgres client and create db
### Ubuntu 20.04 ### # https://wiki.postgresql.org/wiki/Apt # Import the repository key from https://www.postgresql.org/media/keys/ACCC4CF8.asc: sudo apt-get install curl ca-certificates gnupg curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - # Create /etc/apt/sources.list.d/pgdg.list. The distributions are called codename-pgdg. sudo sh -c 'echo "deb [arch=amd64] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" >> /etc/apt/sources.list.d/pgdg.list' sudo apt-get update sudo apt-get install postgresql-13-client libpq-dev ### Amazon Linux 2: postgresql10 or 11 or 12 ### sudo amazon-linux-extras install postgresql13 sudo yum install postgresql-devel ### If Postgres installed local ### sudo -u postgres psql ### FOR AMAZON RDS ### psql --host=myinstancehost.eu-west-3.rds.amazonaws.com --port=5432 --username=postgres --password create database mysite_production; create user rails with password 'mypassword'; grant all privileges on database mysite_production to rails;
Install more dependencies
Next we need to install more dependencies:
- Node
- Yarn
- RVM / RUBY
- NGINX
Node & Yarn
### LOGIN AS RAILS USER ### sudo su rails cd ### Ubuntu 20.04 ### # see https://github.com/nodesource/distributions/blob/master/README.md curl -fsSL https://deb.nodesource.com/setup_14.x | sudo -E bash - apt-get install -y nodejs ### Amazon Linux 2 ### sudo yum -y update curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash ### Activate nvm by typing the following at the command line. ### . ~/.nvm/nvm.sh ### Use nvm to install the latest version of Node.js by typing the following at the command line. ### # With "nvm install node" got error, so use: nvm install 16 ### Test that Node.js is installed and running correctly ### node -v npm install --global yarn yarn -v
RVM
su rails cd ### Ubuntu 20.04 ### # see https://rvm.io/rvm/install gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB curl -sSL https://get.rvm.io | bash -s stable --ruby ### Amazon Linux 2 ### sudo yum install gcc gpg2 --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB curl -sSL https://get.rvm.io | bash -s stable
Troubleshooting with install rvm see https://rvm.io/rvm/security. Alternatively you might want to import keys directly from our web server, although this is a less secure way:
### METHOD 1 ### # Alternative sources for keyserver: hkp://ipv4.pool.sks-keyservers.net; hkp://pgp.mit.edu; hkp://keyserver.pgp.com gpg2 --keyserver hkp://pool.sks-keyservers.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB ### OR ### gpg2 --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB ### METHOD 2: although this is a less secure way ### curl -sSL https://rvm.io/mpapis.asc | gpg --import - curl -sSL https://rvm.io/pkuczynski.asc | gpg --import - # Trust rvm keys echo 409B6B1796C275462A1703113804BB82D39DC0E3:6: | gpg2 --import-ownertrust # mpapis@gmail.com echo 7D2BAF1CF37B13E2069D6956105BD0E739499BDB:6: | gpg2 --import-ownertrust # piotr.kuczynski@gmail.com # Run verified installation curl -sSL https://get.rvm.io | bash -s stable
Check on your local machine which ruby version is used in your project (3.1.2 for me)
cd mysite cat Gemfile | grep ruby
Then install that same version on the server using RVM.
# as root /home/rails/.rvm/bin/rvm install ruby-3.1.2 su rails cd source ~/.rvm/scripts/rvm # Use rvm get head to get the latest version. rvm get head # View the available versions of Ruby. rvm list known rvm use ruby-3.1.2 rvm use --default ruby-3.1.2 rvm -v ruby -v
Install nginx
### Ubuntu 20.04 ### apt-get install nginx systemctl start nginx systemctl enable nginx ### Amazon Linux 2 ### sudo yum install nginx sudo service nginx status
Install Git
apt-get install git
Add Database info connection to Environment variables:
vim ~/.bashrc ... # DB CONNECTION INFO export RDS_DATABASE=deploy_test export RDS_USERNAME=postgres export RDS_PASSWORD=password export RDS_HOST=myhost.eu-west-3.rds.amazonaws.com export RAILS_ENV=development # OR vim /etc/environment ... DB_NAME="mysite_name_db_production" DB_USERNAME="rails" DB_PASSWORD="my_password" DB_HOST="localhost" RAILS_ENV="production"
Or just use Rails environment credentials:
EDITOR=vim rails credentials:edit # Store the below: production: RDS_DATABASE=rds_db_production RDS_USERNAME=rd_username RDS_PASSWORD=rails_jvt RDS_HOST=rds-host.eu-west-3.rds.amazonaws.com # Usage in config/database.yml database: <%= Rails.application.credentials.production[:RDS_DATABASE] %> host: <%= Rails.application.credentials.production[:RDS_HOST] %> password: <%= Rails.application.credentials.production[:RDS_PASSWORD] %> username: <%= Rails.application.credentials.production[:RDS_USERNAME] %>
Install Capistrano
Add the following gems to your group :development do
block inside the Gemfile.
gem 'capistrano', require: false gem 'capistrano-rvm', require: false gem 'capistrano-rails', require: false gem 'capistrano-bundler', require: false gem 'capistrano3-puma', require: false
To see list of available tasks use
cap -T
Install the gems and install cap
bundle install cap install
Add the following to your Capfile
require "capistrano/rails" require "capistrano/bundler" require "capistrano/rvm" require "capistrano/puma" install_plugin Capistrano::Puma install_plugin Capistrano::Puma::Systemd install_plugin Capistrano::Puma::Nginx # if you want to upload a nginx site template # install_plugin Capistrano::Puma::Workers # if you want to control the workers (in cluster mode) # install_plugin Capistrano::Puma::Jungle # if you need the jungle tasks # install_plugin Capistrano::Puma::Monit # if you need the monit tasks
Adjust yourconfig/deploy/production.rb
to be able to connect via ssh:
# Use Your IP instead of 172.00.00.00 server "172.00.00.00", user: "rails", roles: %w{web app db}
Adjust yourconfig/deploy.rb
the file to look like this:
# config valid for current version and patch releases of Capistrano lock "~> 3.17.0" # set :puma_service_unit_env_file, '/etc/environment' set :puma_service_unit_env_vars, [ "RDS_HOST=%s" % [ENV['RDS_HOST']], "RDS_PASSWORD=%s" % [ENV['RDS_PASSWORD']], "RDS_USERNAME=%s" % [ENV['RDS_USERNAME']], "RDS_DATABASE=%s" % [ENV['RDS_DATABASE']] ] # Default deploy_to directory is /var/www/my_app_name set :deploy_to, "/home/rails/deploy/jvt" set :application, "jvt" set :repo_url, "git@github.com:psc-be/JVT.git" # set :puma_conf, "{shared_path}/config/puma.rb" set :rails_env, :production set :stage, :production set :user, "rails" set :use_sudo, false #### set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.sock" set :puma_state, "#{shared_path}/tmp/pids/puma.state" set :puma_pid, "#{shared_path}/tmp/pids/puma.pid" set :puma_access_log, "#{release_path}/log/puma.access.log" set :puma_error_log, "#{release_path}/log/puma.error.log" ### NGINX, more info see below, block NGINX Setup ### if your nginx server configuration is not located in /etc/nginx, you may need to customize: ### # set :nginx_sites_available_path, "/etc/nginx/conf.d" set :nginx_sites_enabled_path, "/etc/nginx/conf.d" set :nginx_config_name, "#{fetch(:application)}_#{fetch(:stage)}.conf" # append :rvm_map_bins, 'puma', 'pumactl' # Default branch is :master # ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp set :branch, 'main' # Default value for :format is :airbrussh. # set :format, :airbrussh # You can configure the Airbrussh format using :format_options. # These are the defaults. # set :format_options, command_output: true, log_file: "log/capistrano.log", color: :auto, truncate: :auto # Default value for :pty is false # set :pty, true # Default value for :linked_files is [] # append :linked_files, "config/database.yml", "config/master.key" append :linked_files, "config/master.key" # Default value for linked_dirs is [] append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "tmp/webpacker", "public/system", "vendor/bundle", "storage" # Default value for default_env is {} # set :default_env, { path: "/opt/ruby/bin:$PATH" } # Default value for local_user is ENV['USER'] # set :local_user, -> { `git config user.name`.chomp } # Default value for keep_releases is 5 # set :keep_releases, 5 # Uncomment the following to require manually verifying the host key before first deploy. # set :ssh_options, verify_host_key: :secure
Commit and push your changes.
Copy your master.key
to the shared dir.
ssh rails@mysite.com mkdir -p apps/mysite/shared/config # back on your machine cd mysite scp config/master.key rails@mysite.com:apps/mysite/shared/config
Adjust your config/database.yml
file:
production: <<: *default database: deploy_test_production host: <%= ENV["RDS_HOST"] %> password: <%= ENV["RDS_PASSWORD"] %> username: <%= ENV["RDS_USERNAME"] %>
Test a production deploy
cap production deploy
If you are on mac os, you might encounter this error.
Your bundle only supports platforms ["x86_64-darwin-19"] but your local platform is x86_64-linux. Add the current platform to the lockfile with 'bundle lock --add-platform x86_64-linux' and try again.
In that case just run
bundle lock --add-platform x86_64-linux # commit & push
Puma project config
# shared/puma.rb cap production puma:config
Puma systemd service
The deploy should fail in the end with the message Failed to restart puma_mysite_production.service: Unit puma_mysite_production.service not found.
To generate unit file we can use the below command docs:
# for puma.service cap production puma:systemd:config puma:systemd:enable
Or let’s create this service manually:
vim /etc/systemd/system/puma_mysite_production.service
Enter the following:
[Unit] Description=Puma HTTP Server for mysite (production) After=network.target [Service] Type=simple User=rails WorkingDirectory=/home/rails/apps/mysite/current ExecStart=/home/rails/.rvm/bin/rvm default do bundle exec puma -C /home/rails/apps/mysite/shared/puma.rb ExecReload=/bin/kill -TSTP $MAINPID StandardOutput=append:/home/rails/apps/mysite/current/log/puma.access.log StandardError=append:/home/rails/apps/mysite/current/log/puma.error.log Restart=always RestartSec=1 SyslogIdentifier=puma [Install] WantedBy=multi-user.target
Create the directory for the puma sockets to live in:
mkdir apps/mysite/shared/tmp/sockets
You should be able to run the service now:
systemctl start puma_mysite_production.service systemctl enable puma_mysite_production.service systemctl status puma_mysite_production.service
Try to deploy again, this time it should work fine:
cap production deploy:initial
cap production deploy
NGINX Setup
We need a webserver to proxy http and https requests to puma. NGINX does this nicely.
Nginx doesn't have folders like sites-enabled and sites-available. We create a "sites-available" folder on production server to avoid Capistrano error when uploading the site configuration:
ssh rails@mysite.com mkdir /etc/nginx/sites-available/
And we change "nginx_sites_enabled_path" in "deploy.rb", if it is not done yet.
set :nginx_sites_enabled_path, "/etc/nginx/conf.d"
NGINX load all configuration files from the /etc/nginx/conf.d directory.
The end of the file should be .conf
.
Capistrano create a nginx config file without .conf
, so we need to specify the correct nginx_config_name
:
set :nginx_config_name, "#{fetch(:application)}_#{fetch(:stage)}.conf
Now we are ready to upload a NGINX site config (eg. /etc/nginx/sites-enabled/ ):
cap production puma:nginx_config
To customize these two templates locally before uploading use:
rails g capistrano:nginx_puma:config ### if your nginx server configuration is not located in /etc/nginx, you may need to customize: ### set :nginx_sites_available_path, "/etc/nginx/sites-available" set :nginx_sites_enabled_path, "/etc/nginx/sites-enabled" ### By default, nginx_config will be executed with :web role. But you can assign it to a different role: ### set :puma_nginx, :foo ### or define a standalone one: ### role :puma_nginx, %w{root@example.com}
Or add manually
vim /etc/nginx/sites-enabled/mysite ### or for Amazon Linux ### vim /etc/nginx/conf.d/mysite.conf
Add the following:
upstream puma { server unix:///home/rails/apps/mysite/shared/tmp/sockets/mysite-puma.sock; } server { server_name mysite.com; root /home/rails/apps/mysite/current/public; access_log /home/rails/apps/mysite/current/log/nginx.access.log; error_log /home/rails/apps/mysite/current/log/nginx.error.log info; location ^~ /assets/ { gzip_static on; expires max; add_header Cache-Control public; } try_files $uri/index.html $uri @puma; location @puma { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Ssl on; # Optional proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Host $host; proxy_redirect off; proxy_pass http://puma; } error_page 500 502 503 504 /500.html; client_max_body_size 100M; keepalive_timeout 10; }
Check if the config is valid & restart nginx
nginx -t systemctl restart nginx
In case you get a permissions error, running the following should fix that:
sudo chown rails:rails -R apps/mysite/
If you get an error like:
Error : /myproject/shared/tmp/sockets/puma.sock failed (13: Permission denied)
Change the user in nginx.cong to rails:
vim /etc/nginx/nginx.cong
Adding LetsEncrypt
You should be able to access your page now via http.
You need SSL Certificates in order to run your site via https. LetsEncrypt is free and easy to setup with NGINX.
### Ubuntu 20.04 ### sudo apt-get install certbot python3-certbot-nginx ### For Amazon Linux 2 ### sudo yum install epel-release sudo yum install certbot-nginx # Obtain a certificate sudo certbot --nginx -d mydomain.be -d www.mydomain.be
Follow the interactive installer, I always choose redirect in the end and you should probably too. Refresh your page, you should be redirect to https now.
Debugging
Errors can occur in a few places here are a few hints:
# check if nginx is running systemctl status nginx journalctl -u nginx # check if puma is running systemctl status puma_mysite_production.service journalctl -u puma_mysite_production.service # application logs tail -f apps/mysite/current/log/*
Hope you find it useful. Have a nice day :))
Comments
Post a Comment