# Setting up a new server and deploy Ruby on Rails project with Capistrano on VPS (Digital Ocean/Hetzner/etc) This use Rails 5 or 6, RVM and Puma. If you want to deploy with RBenv and Passenger, take a look at Phyl Smy script https://github.com/philsmy/cable-guy-example/blob/main/SetUpServer.txt and his video where he recorded each step https://www.youtube.com/watch?v=CZtYDplotiI&t=1s If you need to deploy to a EC2 on AWS, check this video of how to create the server first: https://www.youtube.com/watch?v=M0avxObh8J8 # Script ## Dependencies ### Safety This is based on this article: https://medium.com/@sysrex/my-first-10-minutes-on-a-server-d79ea273809b ```console ssh root@' ``` Add a root password ```curl 'https://www.random.org/passwords/?num=2&len=24&format=plain&rnd=new'``` ``` passwd``` security upgrades later on: ```console apt-get update apt-get upgrade ``` ### Deployer user Create a **deployer** user: You should never be logging on to a server as root. ``` useradd deployer mkdir /home/deployer mkdir /home/deployer/.ssh chmod 700 /home/deployer/.ssh ``` ##### Setup your prefered shell for the deployer user, here we use bash: ```usermod -s /bin/bash deployer``` ##### Set password and add deployer user to **sudo** and **root** groups: ```curl 'https://www.random.org/passwords/?num=2&len=24&format=plain&rnd=new'``` ```passwd deployer``` ```addgroup deployer root``` ```adduser deployer sudo``` Now access the Sudoers file to give permission to deployer run commands without system ask for password: ```console vim /etc/sudoers or nano /etc/sudoers or visudo ``` Add to **last line** (/etc/sudoers): ```console deployer ALL=(ALL) NOPASSWD: ALL ``` Copy SSH permissions from root to deployer: ```console rsync --archive --chown=deployer:deployer ~/.ssh /home/deployer ``` You can add more SSH keys: ```console vim /home/deployer/.ssh/authorized_keys ```` Let’s set the right permissions based on the Linux security principal of least privilege: ``` chmod 400 /home/deployer/.ssh/authorized_keys chown deployer:deployer /home/deployer -R ``` Exit server as a root and log in again as a deployer . ``` exit ssh deployer@xxx.xxx.xxx.xxx ``` Or just type: ```exec su -l deployer``` Remove root login with password (only SSH will work). Run ```sudo vim /etc/ssh/sshd_config``` and add/change: ```console PermitRootLogin no PasswordAuthentication no ``` If you have a VPN/STATIC IP, you can restrict login to olny that IP, adding to this file: ```console AllowUsers deployer@(your-VPN-or-static-IP) AddressFamily inet ``` Enable all these rules by restarting the ssh service. You’ll probably need to reconnect (do so by using your deploy user!) ```sudo service ssh restart``` ### Setting up a firewall First we’ll want to make sure that we are supporting IPv6. Just open up the config file. ```vim /etc/default/ufw``` Set IPv6 to yes. ```IPV6=yes``` For the rest of the ports that we’re going to open up, we can just use the ufw tool from command line which is very handy. ``` sudo ufw allow 22 sudo ufw allow 80 sudo ufw allow 443 sudo ufw disable sudo ufw enable ``` Or, if you use Static IP: ``` sudo ufw allow from {your-ip} to any port 22 sudo ufw allow 80 sudo ufw allow 443 sudo ufw disable sudo ufw enable ``` ##### Automated security updates I like these. They’re not perfect, but it’s better than missing patches as they come out. ```sudo apt-get install unattended-upgrades``` acess ```sudo vim /etc/apt/apt.conf.d/10periodic``` Update this file to match this: ``` APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Download-Upgradeable-Packages "1"; APT::Periodic::AutocleanInterval "7"; APT::Periodic::Unattended-Upgrade "1"; ``` #### Fail2Ban ```sudo apt-get install fail2ban``` ### Rails dependencies (You can use the Go Rails tutorial. I use Puma and RVM. Go Rails use Passenger and Rbenv) After that, install dependencies: ### Adding Node.js 12 repository ``` curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - ``` *Utilize uma versao diferente se necessário.* ### Adding Yarn repository ```console curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - ``` ```console echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list ``` ### Redis ```console sudo add-apt-repository ppa:chris-lea/redis-server ``` ### Refresh our packages list with the new repositories ```console sudo apt-get update ``` ### Install our dependencies for compiiling Ruby along with Node.js and Yarn ```console sudo apt-get install git-core curl zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev software-properties-common libffi-dev dirmngr gnupg apt-transport-https ca-certificates nodejs yarn redis-server redis-tools -y ``` ### Install ImageMagick if necessary ```console sudo apt install imagemagick -y ``` ### Nginx ```console sudo apt install nginx -y ``` Testing Nginx: ```console systemctl status nginx ``` Or access your server IP onm browser and see if you get a nginx default message. ### Lets create the application folder: ```console sudo chown deployer:deployer /home/deployer ``` ```console mkdir apps ``` ```console sudo chown deployer:deployer apps ``` ### RVM Install rvm to manage ruby versions: ```console curl -sSL https://get.rvm.io | bash ``` ```console source /home/deployer/.rvm/scripts/rvm ``` ### Ruby Install ruby (change x.x.x for your version version): ```console rvm install 2.X.X --default ``` Check if the ruby version is correct: ```console ruby -v ``` ### Bundler Install bundler (change x.x.x for your version version): ```console gem install bundler -v x.x.x --no-document ``` ### Rails Install Rails (change x.x.x for your version version): ```console gem install rails -v 6.X.X.X --no-document ``` Check your rails version: ```console rails -v ``` ## Postgres For Postgres, we're going to start by installing the Postgres server and libpq which will allow us to compile the pg rubygem. Then, we're going to become the postgres linux user who has full access to the database and use that account to create a new database user for our apps. We'll call that user deployer. And finally, the last command will create a database called myapp and make the deployer user owner. Make sure to change **myapp_database_name** to the name of your application. ```console sudo apt-get install postgresql postgresql-contrib libpq-dev -y sudo su - postgres createuser --superuser --pwprompt deployer createdb -O deployer **myapp_database_name/changeme** exit ``` You can manually connect to your database anytime by running: `psql -U deployer -W -h 127.0.0.1 -d myapp_databas_name`. Make sure to use 127.0.0.1 when connecting to the database instead of localhost. ## Capistrano Check if you have in your Gemfile at **development** group: ```console gem "capistrano", "~> 3.12", require: false gem "capistrano-rails", require: false gem "capistrano-yarn" gem 'capistrano-rvm' gem 'capistrano3-puma' ``` At your **local machine**, in app folder, run: ```console cap install ``` Update the created files: - Capfile ```ruby # Load DSL and set up stages require "capistrano/setup" # Include default deployment tasks require "capistrano/deploy" # Include tasks from other gems included in your Gemfile # require 'capistrano/rails' require 'capistrano/bundler' # Rails needs Bundler, right? require 'capistrano/rails/assets' require 'capistrano/rails/migrations' require 'capistrano/rvm' require 'capistrano/yarn' # require "whenever/capistrano" require 'capistrano/delayed_job' # Load the SCM plugin appropriate to your project: require "capistrano/scm/git" install_plugin Capistrano::SCM::Git require 'capistrano/puma' install_plugin Capistrano::Puma install_plugin Capistrano::Puma::Workers install_plugin Capistrano::Puma::Nginx # Whenever gem config # set :whenever_identifier, ->{ "#{fetch(:application)}_#{fetch(:stage)}" } #Load custom tasks from `lib/capistrano/tasks` if you have any defined Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } ``` - config/deploy.rb Update the git repo Add at :linked_dirs the folders that have to be shared among the deploy versions. If you use a local Active Storage (without AWS) add a "storage" folder at append :linked_dirs ```ruby # config valid for current version and patch releases of Capistrano lock "~> 3.12.1" set :application, "NOME-DA-APLICACAO" # Update repo address set :repo_url, "git@github.com:xxxxxxxx/xxxxxxxxxxx.git" # Update folder set :deploy_to, "/home/deployer/apps/app-name" #change master.key for secrets.yml, if needed. append :linked_files, "config/database.yml", "config/master.key"#, "config/local_env.yml", "config/sidekiq.yml" append :linked_dirs, "log", "tmp" set :keep_releases, 5 set :migration_role, :app set :puma_pid, "#{shared_path}/tmp/pids/puma.pid" set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.sock" set :puma_access_log, "#{shared_path}/log/puma_access.log" set :puma_error_log, "#{shared_path}/log/puma_error.log" set :nginx_sites_available_path, "/etc/nginx/sites-available" set :nginx_sites_enabled_path, "/etc/nginx/sites-enabled" #Change ruby version set :rvm_type, :user set :rvm_ruby_version, '2.X.X' namespace :puma do desc 'Create Puma dirs' task :create_dirs do on roles(:app) do execute "mkdir #{shared_path}/tmp/sockets -p" execute "mkdir #{shared_path}/tmp/pids -p" end end desc "Restart Nginx" task :nginx_restart do on roles(:app) do execute "sudo service nginx restart" end end before :start, :create_dirs after :start, :nginx_restart end ``` - config/deploy/production.rb ```ruby set :branch, 'master' # Update user set :user, 'deployer' # Update server IP set :server_address, 'xx.xx.xxx.xxx' ask(:password, nil, echo: false) server fetch(:server_address), user: fetch(:user), roles: %w{app db web} set :nginx_server_name, fetch(:server_address) set :puma_preload_app, true ``` Log in on server and generate a SSH key (I usually do that without the password) ```console ssh-keygen ``` Copy the SSH key ```console cat /home/deployer/.ssh/id_rsa.pub ``` Add this key to your repo at Github, on "deploy keys" Log out of server and check at your **local machine ** if the deploy is ok: ```console cap production deploy:check ``` Capistrano will tell that a linked file does no exist. Let's create the database.yml and the others linked files needed. Log at server, go to /apps/app-name/shared/config, and create the database.yml: ```console sudo vim apps/YOUR-APP-NAME/shared/config/database.yml ``` ```ruby default: &default adapter: postgresql pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 username: deployer password: YOUR DATABASE PASSWORD production: <<: *default database: APP-NAME ``` Now copy your master.key (or create it): ```console scp config/master.key deployer@xxxx:/home/deployer/apps/xxxxxx/shared/config/ ``` *if you do not have a master.key you can generate one running: ```EDITOR=vim rails credentials:edit``` Exit server and check deploy again: ```console cap production deploy:check ``` Configure Puma: ```console cap production puma:config ``` Configure Nginx: ```console cap production puma:nginx_config ``` To finalize, run the deploy!!!: ```console cap production deploy ``` ## SSL ### Certbot **On remote server** Add repository ```console sudo apt install ca-certificates ``` When you see the question: 'Press [ENTER] to continue or Ctrl-c to cancel adding it', press ENTER Update ```console sudo apt-get update ``` Install Certbot for Nginx ```console sudo apt-get install python-certbot-nginx -y ``` Insert the data on nginx ```console sudo nano /etc/nginx/sites-available/nomedaaplicacao_production ``` ```console server_name xxx.xxx.xx.xxx yourdomain.com; ``` If you use Devise for authentication, to avoid 'ActionController::InvalidAuthenticityToken' error, add below 'proxy_set_header X-Forwarded-Proto http': ```console proxy_set_header X-Forwarded-Ssl on; proxy_set_header X-Forwarded-Port 443; proxy_set_header X-Forwarded-Host $host; ``` (see: https://github.com/rails/rails/issues/22965) Restart Nginx ```console sudo service nginx reload ``` Get SSL ```console sudo certbot --nginx -d yourdomain.com ``` Certbot will ask your email. When asked: "Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access." Choose "2: Redirect" Check on the end of file etc/nginx/sites-avaliable/ if certbot conf were added: ```console cat /etc/nginx/sites-available/NOMEDAAPLICACAO_production ``` ## App online After Certbot run again the deploy: ```cap production deploy``` Congrats!!! # Other things ### Set hourly database backup Use a S3 bucket https://github.com/huacnlee/gobackup Run on servidor: ```curl -sSL https://git.io/gobackup | bash``` test: ```gobackup -h``` create: ```sudo vim ~/.gobackup/gobackup.yml ``` add on this file: ``` models: my-app: compress_with: type: tgz store_with: type: s3 keep: 120 bucket: region: us-east-1 path: backups access_key_id: secret_access_key: databases: my-app: database: type: postgresql host: localhost username: deployer password: ``` Run with ```gobackup perform ``` To schedule: ```crontab -e``` Add (example for backups at each 6 hours): ``` # Execute GoBackup at each 6 hours 30 0,6,12,18 * * * /usr/local/bin/gobackup perform >> ~/.gobackup/gobackup.log ``` Or each 1 hour: ``` # Execute GoBackup hourly 0 * * * * /usr/local/bin/gobackup perform >> ~/.gobackup/gobackup.log ``` And after a hour, you can check up the execute status by ~/.gobackup/gobackup.log. To restore database (test it!) ```psql -U postgres -d your-database -f /path/to/your-backup.sql``` ## Sidekiq Set Redis no Ubuntu: https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-18-04-pt or https://linuxize.com/post/how-to-install-and-configure-redis-on-ubuntu-20-04/ http://www.enquerer.com/run-sidekiq-in-digital-ocean-production-droplet-for-capistrano-deployed-rails-application/ https://github.com/mperham/sidekiq/blob/master/examples/systemd/sidekiq.service https://www.codewithjason.com/restart-sidekiq-automatically-deployment/ Add a sidekiq.yml to config: ```yml --- :verbose: false :concurrency: 10 :queues: - mailers - default - low production: :pidfile: /home/deployer/apps/application-name/shared/tmp/pids/sidekiq.pid :concurrency: 25 staging: :pidfile: /home/deploy/apps/application-name/shared/tmp/pids/sidekiq.pid :concurrency: 15 ``` At config/deploy.rb add the sidekiq to shared files ```ruby append :linked_files, "config/database.yml", "config/master.key", "config/local_env.yml", "config/sidekiq.yml" ``` create pid on server: sudo touch /home/deployer/apps/my-application-name/shared/tmp/pids/sidekiq.pid sudo chmod 777 /home/deployer/apps/my-application-name/shared/tmp/pids/sidekiq.pid In Order to start the Sidekiq as service while booting the server we need to make a Sidekiq service in our ubuntu server. So to do so, run the following in ubuntu server: ```sudo nano /lib/systemd/system/sidekiq.service``` https://github.com/mperham/sidekiq/blob/master/examples/systemd/sidekiq.service **change patch** - 'WorkingDirectory=/home/deployer/apps/my-application-name/current' - ExecStart=/bin/bash -lc 'bundle exec sidekiq -e production -C config/sidekiq.yml' ``` # # This file tells systemd how to run Sidekiq as a 24/7 long-running daemon. # # Customize this file based on your bundler location, app directory, etc. # Customize and copy this into /usr/lib/systemd/system (CentOS) or /lib/systemd/system (Ubuntu). # Then run: # - systemctl enable sidekiq # - systemctl {start,stop,restart} sidekiq # # This file corresponds to a single Sidekiq process. Add multiple copies # to run multiple processes (sidekiq-1, sidekiq-2, etc). # # Use `journalctl -u sidekiq -rn 100` to view the last 100 lines of log output. # [Unit] Description=sidekiq # start us only once the network and logging subsystems are available, # consider adding redis-server.service if Redis is local and systemd-managed. After=syslog.target network.target # See these pages for lots of options: # # https://www.freedesktop.org/software/systemd/man/systemd.service.html # https://www.freedesktop.org/software/systemd/man/systemd.exec.html # # THOSE PAGES ARE CRITICAL FOR ANY LINUX DEVOPS WORK; read them multiple # times! systemd is a critical tool for all developers to know and understand. # [Service] # # !!!! !!!! !!!! # # As of v6.0.6, Sidekiq automatically supports systemd's `Type=notify` and watchdog service # monitoring. If you are using an earlier version of Sidekiq, change this to `Type=simple` # and remove the `WatchdogSec` line. # # !!!! !!!! !!!! # Type=notify # If your Sidekiq process locks up, systemd's watchdog will restart it within seconds. WatchdogSec=10 WorkingDirectory=/home/deployer/apps/my-application-name/current # If you use rbenv: # ExecStart=/bin/bash -lc 'exec /home/deploy/.rbenv/shims/bundle exec sidekiq -e production' # If you use the system's ruby: ExecStart=/bin/bash -lc 'bundle exec sidekiq -e production -C config/sidekiq.yml' # If you use rvm in production, don't. # Use `systemctl kill -s TSTP sidekiq` to quiet the Sidekiq process # !!! Change this to your deploy user account !!! User=deployer Group=sudo UMask=0002 # Greatly reduce Ruby memory fragmentation and heap usage # https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/ Environment=MALLOC_ARENA_MAX=2 # if we crash, restart RestartSec=1 Restart=on-failure # output goes to /var/log/syslog (Ubuntu) or /var/log/messages (CentOS) StandardOutput=syslog StandardError=syslog # This will default to "bundler" if we don't specify it SyslogIdentifier=sidekiq [Install] WantedBy=multi-user.target ``` Step 5: To start Sidekiq when the server boots up, we need to create a symlink. In order to do so, run the below command. ```sudo systemctl enable sidekiq.service``` Step 6: Now we can start the Sidekiq service. ```sudo service sidekiq start``` Step 7: You can check the system log for errors. ```sudo cat /var/log/syslog ``` And check if Sidekiq started by ```sudo ps aux | grep sidekiq ``` If the Sidekiq is running you can see something like… ```user 2609 0.7 3.3 1352776 136960 ? Ssl 06:18 0:15 sidekiq 5.2.7 application-name[0 of 25 busy]``` Here it is the Sidekiq running successfully as service in your ubuntu droplet. Now whenever the server boots up the Sidekiq server will also restart. You can manually stop, start, or restart the server by, ```sudo service sidekiq start/stop/restart``` Update the namespace :puma block on capistrano (config/deploy.rb) to restart sidekiq after each deploy: ```ruby namespace :sidekiq do desc "Restart Sidekiq" task :restart do on roles(:app) do execute "sudo service sidekiq restart" end end end namespace :deploy do desc "Restart Sidekiq" task :restart_sidekiq do on roles(:app) do execute "sudo service sidekiq restart" end end after :finishing, :restart_sidekiq end ``` ## Configuring Logrotate For Rails Production Logs https://gorails.com/guides/rotating-rails-production-logs-with-logrotate You might be surprised at just how easy to setup logrotate Rails logs is. The reason it is so handy is that a bunch of your operating system software is already using it. We just have to plug in our configuration and we’re set! The first step is to open up /etc/logrotate.conf using vim or nano. Jump to the bottom of the file an add the following block of code. You’ll want to change the first line to match the location where your Rails app is deployed. Mine is under the deploy user’s home directory. Make sure to point to the log directory with the *.log bit on the end so that we rotate all the log files. ```console /home/deploy/APPNAME/current/log/*.log { daily missingok rotate 7 compress delaycompress notifempty copytruncate } ``` or ``` # Rotate Rails application logs based on file size # Rotate log if file greater than 20 MB /path/to/your/rails/applicaton/log/*.log { size=20M missingok rotate 52 compress delaycompress notifempty copytruncate } ``` or ``` # Rotate Rails application logs weekly /path/to/your/rails/applicaton/log/*.log { weekly missingok rotate 52 compress delaycompress notifempty copytruncate } ``` **How It Works** This is fantastically easy. Each bit of the configuration does the following: - daily – Rotate the log files each day. You can also use weekly or monthly here instead. - missingok – If the log file doesn’t exist, ignore it - rotate 7 – Only keep 7 days of logs around - compress – GZip the log file on rotation - delaycompress – Rotate the file one day, then compress it the next day so we can be sure that it won’t interfere with the Rails server - notifempty – Don’t rotate the file if the logs are empty - copytruncate – Copy the log file and then empties it. This makes sure that the log file Rails is writing to always exists so you won’t get problems because the file does not actually change. If you don’t use this, you would need to restart your Rails application each time. Running Logrotate Since we just wrote this configuration, you’ll want to test it. To run logrotate manually, we just do: `sudo /usr/sbin/logrotate -f /etc/logrotate.conf` You’re going to want to run it a **second time** to make sure the delaycompress option is working and to actually compress the log. Here’s an example of what you’ll see if you ```ls``` the log folder after running logrotate twice: You can see that the production.log still exists, production.log.1 is a copy of the logs between the first and second run of logrotate, and production.log.2.gz is the 300MB behemoth of a log file that we had before compressed nicely with Gzip. Once we get up to 7 log files, the next time logrotate runs, it will delete the oldest one so that we only have 7 days worth of logs. If you want to keep all the logs around, you can remove the rotate line from the configuration. Plus, since we just edited the main logrotate.conf file, the cron job will automatically execute the logrotate Rails logs daily! ### Adding ENV Create fiel .env ```sudo vim /etc/.env``` and add the vars you need, for example: ``` SENDGRID_USERNAME=myuser SENDGRID_PASSWORD=xxxxxxxxxxxxxxxx ``` Add on bash, run: ```sudo vim ~/.bashrc ``` And add on first line: ``` source /etc/.env ``` # Possible errors ##### Webpacker not compiling - error when run rake assets:precompile on rails 6 https://dev.to/tcgumus/rails-6-webpacker-settings-for-production-1f1e ```