Capistrano Setup: Initial & Continuous Deployment Guide
Hey guys! Today, we're diving deep into setting up Capistrano for automated deployments, specifically focusing on two crucial deployment types: initial setup and continuous deployment. Our goal is to create a seamless process that takes a brand new EC2 instance from launch to a fully running application with just a single command. We'll also cover continuous deployment strategies for pushing out new features and enhancements. Let's get started!
Initial Deployment (deploy:initial
)
Our primary objective with the initial deployment (deploy:initial
) is to automate the entire setup process for a fresh EC2 instance. This includes everything from creating the deploy user and setting up permissions to installing dependencies, configuring Nginx, setting up symlinks, and integrating rbenv, Ruby, and Passenger. Think of it as a one-shot operation that transforms a bare-bones EC2 instance into a fully functional application server. This is super important because it saves time and reduces the risk of human error during the initial setup.
Setting Up the Deploy User and Permissions
First, we need to create a dedicated user for deployment. This is a security best practice, as it isolates the deployment process from other system users. We'll create a user, say deploy
, and grant it the necessary permissions to deploy our application. This typically involves adding the user to a group that has write access to the deployment directory. The following snippet illustrates how we can achieve this using Capistrano tasks:
namespace :deploy do
desc 'Create deploy user'
task :create_deploy_user do
on roles(:all) do
execute "sudo adduser deploy --disabled-password --gecos 'Deploy User'"
execute "sudo usermod -aG sudo deploy"
end
end
desc 'Set up SSH key for deploy user'
task :setup_ssh_key do
on roles(:all) do
execute "sudo mkdir -p /home/deploy/.ssh"
execute "sudo chown -R deploy:deploy /home/deploy/.ssh"
upload! StringIO.new(File.read("~/.ssh/id_rsa.pub")), "/tmp/id_rsa.pub"
execute "sudo mv /tmp/id_rsa.pub /home/deploy/.ssh/authorized_keys"
execute "sudo chown -R deploy:deploy /home/deploy/.ssh/authorized_keys"
execute "sudo chmod 600 /home/deploy/.ssh/authorized_keys"
end
end
before :check, :create_deploy_user
before :check, :setup_ssh_key
end
This code snippet creates a deploy
user, adds it to the sudo
group, and sets up SSH key authentication. Security is paramount, and using SSH keys instead of passwords significantly enhances the security of our deployment process.
Installing Dependencies
Next up, we need to ensure that all the necessary dependencies are installed on the server. This includes system-level dependencies like git
, nginx
, and passenger
, as well as Ruby-specific dependencies managed by Bundler. We can leverage Capistrano tasks to automate the installation of these dependencies. For instance, we can use apt-get
(for Debian-based systems) or yum
(for Red Hat-based systems) to install system-level packages.
namespace :deploy do
desc 'Install system dependencies'
task :install_dependencies do
on roles(:all) do
execute "sudo apt-get update"
execute "sudo apt-get install -y git nginx passenger libnginx-mod-http-passenger"
end
end
before :check, :install_dependencies
end
This task updates the package list and installs git
, nginx
, and passenger
along with the necessary Nginx module. Automation is key here; by automating dependency installation, we ensure consistency across all our environments.
Setting Up Nginx Configuration
Configuring Nginx is a critical step in setting up our application server. We need to create an Nginx configuration file that tells Nginx how to handle requests for our application. This typically involves setting up a server block that listens on port 80 (or 443 for HTTPS) and proxies requests to our application server (Passenger in this case). We can use Capistrano to upload the Nginx configuration file to the server and create a symlink to enable it.
namespace :deploy do
desc 'Upload Nginx configuration'
task :upload_nginx_config do
on roles(:web) do
template 'config/nginx.conf.erb', '/tmp/nginx.conf'
execute "sudo mv /tmp/nginx.conf /etc/nginx/sites-available/#{fetch(:application)}"
execute "sudo ln -nfs /etc/nginx/sites-available/#{fetch(:application)} /etc/nginx/sites-enabled/#{fetch(:application)}"
execute "sudo rm /etc/nginx/sites-enabled/default"
execute "sudo systemctl restart nginx"
end
end
before :deploy, :upload_nginx_config
end
This task uploads an Nginx configuration file (using a template), moves it to the appropriate directory, creates a symlink, and restarts Nginx. Proper configuration is crucial for ensuring our application is accessible and performs optimally.
Setting Up rbenv, Ruby, and Passenger
rbenv is a Ruby version manager that allows us to easily switch between different Ruby versions. Passenger is a web application server that integrates seamlessly with Nginx and provides a robust environment for running Ruby applications. Setting these up correctly is vital for our application's performance and stability. We'll use Capistrano tasks to install rbenv, install the required Ruby version, and configure Passenger.
namespace :deploy do
desc 'Install rbenv and Ruby'
task :install_rbenv do
on roles(:all) do
execute "sudo apt-get install -y rbenv ruby-build"
execute "rbenv install #{fetch(:ruby_version)}"
execute "rbenv global #{fetch(:ruby_version)}"
execute "gem install bundler"
end
end
desc 'Install Passenger'
task :install_passenger do
on roles(:all) do
execute "gem install passenger"
execute "passenger-install-nginx-module --auto --auto-download --prefix=/opt/nginx"
end
end
before :deploy, :install_rbenv
before :deploy, :install_passenger
end
These tasks install rbenv, Ruby, Bundler, and Passenger. Using version managers like rbenv helps us maintain consistency across our development and production environments.
Setting Up Let's Encrypt SSL Certificates
To secure our application with HTTPS, we'll use Let's Encrypt to obtain SSL certificates. Let's Encrypt provides free SSL certificates and an automated process for obtaining and renewing them. We can use the certbot
tool to automate this process. Here’s how we can set it up:
namespace :deploy do
desc 'Install Let's Encrypt certbot'
task :install_certbot do
on roles(:web) do
execute "sudo apt-get install -y certbot"
end
end
desc 'Obtain Let's Encrypt certificate'
task :obtain_certificate do
on roles(:web) do
execute "sudo certbot --nginx -d #{fetch(:domain)}"
end
end
before :deploy, :install_certbot
after :deploy, :obtain_certificate
end
This task installs certbot
and obtains an SSL certificate for our domain. HTTPS is essential for modern web applications, and Let's Encrypt makes it easy to implement.
Enhancing Security Against Modern Web Application Vulnerabilities
Security is a top priority, so we need to implement measures to protect against common web application vulnerabilities. This includes setting appropriate HTTP headers, configuring firewalls, and regularly updating our dependencies. Here are some enhancements we can add:
- HTTP Headers: Set security-related HTTP headers like
Strict-Transport-Security
,X-Frame-Options
, andContent-Security-Policy
in our Nginx configuration. - Firewall: Configure a firewall (like
ufw
on Ubuntu) to restrict access to our server. - Regular Updates: Ensure that we regularly update our system packages and Ruby gems to patch security vulnerabilities.
namespace :deploy do
desc 'Set security headers in Nginx config'
task :set_security_headers do
on roles(:web) do
# Modify Nginx config to include security headers
end
end
desc 'Configure firewall'
task :configure_firewall do
on roles(:all) do
execute "sudo ufw enable"
execute "sudo ufw allow 'Nginx Full'"
execute "sudo ufw allow OpenSSH"
end
end
before :deploy, :set_security_headers
before :deploy, :configure_firewall
end
Proactive security measures are crucial for protecting our application and users.
Continuous Deployment (deploy
)
Once our initial setup is complete, we need a streamlined process for deploying new features and enhancements. This is where continuous deployment (deploy
) comes in. This deployment type should handle tasks such as updating gems and Ruby (if applicable), applying changes, and running migrations. Here’s how we can set it up:
Updating Gems and Ruby
Keeping our gems and Ruby version up-to-date is crucial for both security and performance. We can use Capistrano tasks to automate this process. This involves checking for updates, installing new versions, and ensuring that our application uses the latest gems.
namespace :deploy do
desc 'Update gems'
task :update_gems do
on roles(:app) do
within release_path do
execute :bundle, :install, '--without development test', '--deploy', '--jobs 4', '--quiet' # Use bundle install for faster deployment without development and test dependencies. Add other arguments for specific needs.
end
end
end
desc 'Update Ruby version'
task :update_ruby do
on roles(:app) do
execute "rbenv install #{fetch(:ruby_version)}"
execute "rbenv global #{fetch(:ruby_version)}"
end
end
after :updated, :update_gems # It is crucial to update gems after the code has been updated.
before :deploy, :update_ruby # Update ruby before deploying new code. Note: This should be used cautiously to avoid unnecessary updates.
end
The bundle install
command installs the necessary gems, and the rbenv
commands update the Ruby version. Regular updates ensure we're using the latest features and security patches.
Applying Changes and Running Migrations
Deploying changes involves updating the application code and running any necessary database migrations. Capistrano makes this process straightforward with its built-in tasks. We can use the deploy:migrate
task to run migrations and ensure our database schema is up-to-date.
namespace :deploy do
desc 'Run migrations'
task :migrate do
on roles(:db) do
within release_path do
execute :rails, 'db:migrate', 'RAILS_ENV=production' # Add your preferred way to execute Rails commands.
end
end
end
after :deploy, :migrate # Run migrations after deploying new code.
end
This task runs the rails db:migrate
command, ensuring our database schema is in sync with our application code. Database migrations are a critical part of the deployment process, ensuring our application can function correctly with the latest code.
Complete Capistrano Configuration Example
To give you a clearer picture, here’s a complete example of a deploy.rb
file that incorporates all the tasks we’ve discussed:
# config valid for current version and patch releases of Capistrano
lock '~> 3.11'
set :application, 'qalab'
set :repo_url, '[email protected]:your-repo/qalab.git' # Replace with your repository URL.
set :deploy_to, '/var/www/qalab'
set :branch, ENV['BRANCH'] || :master
set :ruby_version, '2.7.0' # Replace with your desired Ruby version.
set :domain, 'your-domain.com' # Replace with your domain.
append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', '.bundle', 'public/system', 'public/uploads'
namespace :deploy do
desc 'Create deploy user'
task :create_deploy_user do
on roles(:all) do
execute "sudo adduser deploy --disabled-password --gecos 'Deploy User'"
execute "sudo usermod -aG sudo deploy"
end
end
desc 'Set up SSH key for deploy user'
task :setup_ssh_key do
on roles(:all) do
execute "sudo mkdir -p /home/deploy/.ssh"
execute "sudo chown -R deploy:deploy /home/deploy/.ssh"
upload! StringIO.new(File.read("~/.ssh/id_rsa.pub")), "/tmp/id_rsa.pub"
execute "sudo mv /tmp/id_rsa.pub /home/deploy/.ssh/authorized_keys"
execute "sudo chown -R deploy:deploy /home/deploy/.ssh/authorized_keys"
execute "sudo chmod 600 /home/deploy/.ssh/authorized_keys"
end
end
desc 'Install system dependencies'
task :install_dependencies do
on roles(:all) do
execute "sudo apt-get update"
execute "sudo apt-get install -y git nginx passenger libnginx-mod-http-passenger"
end
end
desc 'Upload Nginx configuration'
task :upload_nginx_config do
on roles(:web) do
template 'config/nginx.conf.erb', '/tmp/nginx.conf'
execute "sudo mv /tmp/nginx.conf /etc/nginx/sites-available/#{fetch(:application)}"
execute "sudo ln -nfs /etc/nginx/sites-available/#{fetch(:application)} /etc/nginx/sites-enabled/#{fetch(:application)}"
execute "sudo rm /etc/nginx/sites-enabled/default"
execute "sudo systemctl restart nginx"
end
end
desc 'Install rbenv and Ruby'
task :install_rbenv do
on roles(:all) do
execute "sudo apt-get install -y rbenv ruby-build"
execute "rbenv install #{fetch(:ruby_version)}"
execute "rbenv global #{fetch(:ruby_version)}"
execute "gem install bundler"
end
end
desc 'Install Passenger'
task :install_passenger do
on roles(:all) do
execute "gem install passenger"
execute "passenger-install-nginx-module --auto --auto-download --prefix=/opt/nginx"
end
end
desc 'Install Let's Encrypt certbot'
task :install_certbot do
on roles(:web) do
execute "sudo apt-get install -y certbot"
end
end
desc 'Obtain Let's Encrypt certificate'
task :obtain_certificate do
on roles(:web) do
execute "sudo certbot --nginx -d #{fetch(:domain)}"
end
end
desc 'Set security headers in Nginx config'
task :set_security_headers do
on roles(:web) do
# Modify Nginx config to include security headers
end
end
desc 'Configure firewall'
task :configure_firewall do
on roles(:all) do
execute "sudo ufw enable"
execute "sudo ufw allow 'Nginx Full'"
execute "sudo ufw allow OpenSSH"
end
end
desc 'Update gems'
task :update_gems do
on roles(:app) do
within release_path do
execute :bundle, :install, '--without development test', '--deploy', '--jobs 4', '--quiet' # Use bundle install for faster deployment without development and test dependencies. Add other arguments for specific needs.
end
end
end
desc 'Run migrations'
task :migrate do
on roles(:db) do
within release_path do
execute :rails, 'db:migrate', 'RAILS_ENV=production' # Add your preferred way to execute Rails commands.
end
end
end
before :check, :create_deploy_user
before :check, :setup_ssh_key
before :check, :install_dependencies
before :deploy, :upload_nginx_config
before :deploy, :install_rbenv
before :deploy, :install_passenger
before :deploy, :install_certbot
before :deploy, :set_security_headers
before :deploy, :configure_firewall
after :deploy, :obtain_certificate
after :updated, :update_gems # It is crucial to update gems after the code has been updated.
after :deploy, :migrate # Run migrations after deploying new code.
end
And here’s an example of a nginx.conf.erb
template:
server {
listen 80;
server_name <%= fetch(:domain) %>;
root /var/www/<%= fetch(:application) %>/current/public;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';";
# Pass all requests to Passenger
passenger_enabled on;
passenger_user deploy;
rails_env production;
location ~ ^/assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
# Redirect HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name <%= fetch(:domain) %>;
root /var/www/<%= fetch(:application) %>/current/public;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';";
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/<%= fetch(:domain) %>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<%= fetch(:domain) %>/privkey.pem;
# Pass all requests to Passenger
passenger_enabled on;
passenger_user deploy;
rails_env production;
location ~ ^/assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
}
This setup ensures our application is served over HTTPS with essential security headers. Using templates allows us to dynamically generate configuration files, making our deployments more flexible and maintainable.
Running the Deployments
With everything configured, you should be able to set the EC2_HOST
environment variable or add the host to targets.rb
and then run:
bundle exec cap production deploy:initial
This command will perform the initial setup. For continuous deployments, you can use:
bundle exec cap production deploy
This will deploy the latest changes to your application. Simple commands for complex processes make deployments much easier and less error-prone.
Conclusion
Setting up Capistrano for automated deployments can seem daunting at first, but it’s a worthwhile investment. By automating our deployment process, we reduce the risk of errors, save time, and ensure consistency across our environments. We’ve covered both initial setup and continuous deployment, along with security enhancements to protect our application. Now you're equipped to streamline your deployment workflow! Keep coding, guys!