caput 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +92 -0
- data/exe/caput +5 -0
- data/lib/caput/cli.rb +36 -0
- data/lib/caput/config.rb +72 -0
- data/lib/caput/local.rb +82 -0
- data/lib/caput/remote.rb +64 -0
- data/lib/caput/server.rb +248 -0
- data/lib/caput/teardown.rb +34 -0
- data/lib/caput/version.rb +3 -0
- data/lib/caput.rb +14 -0
- metadata +103 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8f70afda0863b53a3471a330a939a2a16e6c11bf54bf091cde7f868ec8d24af8
|
|
4
|
+
data.tar.gz: 18b1cad9051efcaaa9f98f8f9cfb3f34283b94e02d8a49451bcb8df7264ba43f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c68f1efe34c294399396b0b04b2fd5284ee88e08b9c36a4696dcd11a979e21eab145e8f76152d65f35580b3f86edc20e784d26d9b481172b9537ecbbf477ec85
|
|
7
|
+
data.tar.gz: 670fcae494a9817033ab253ec75eade086399cf314691417a0ceb1326c7a217a7fd3e94ae1a2dd3bbdb958604322b40c92714496ac4c203ca5f110a14aadb112
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2025 Martin Bergek
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
|
|
2
|
+
# Caput
|
|
3
|
+
|
|
4
|
+
**Caput** is a Ruby gem that helps you prepare a vanilla Ubuntu server to host one or more Rails applications. It automates the steps required to make an application ready for deployment with standard Capistrano commands.
|
|
5
|
+
|
|
6
|
+
**Note:** Caput does **not deploy the application itself**. Instead, it ensures that the server and the local repository are properly configured so that deployments can be done reliably with minimal manual steps.
|
|
7
|
+
|
|
8
|
+
Deploying Rails applications to a server can be surprisingly complex. While tools like Kamal provide a modern “official” deployment approach, they often require a public container registry and additional infrastructure that many teams do not want or need. On the other hand, Capistrano has been the tried-and-true method for decades, but its setup can be tedious, especially on a fresh Ubuntu server. This gem bridges the gap: it gives you a simple Capistrano-style workflow while preparing the server and the app environment, without requiring Passenger or complex container setups. Essentially, it automates the repetitive steps needed to make a Rails app server-ready, letting you focus on your app instead of manual server configuration.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Add the gem to your Rails application's `Gemfile` in the development group:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
group :development do
|
|
18
|
+
gem 'caput', '~> 0.1.0'
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or install it directly:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
gem install caput
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### 1. Initialise
|
|
33
|
+
|
|
34
|
+
Run the following in the root folder of your Rails application:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
caput init
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This creates a `caput.conf` file containing configuration for your server and deployment settings.
|
|
41
|
+
|
|
42
|
+
### 2. Configure
|
|
43
|
+
|
|
44
|
+
Edit `caput.conf` with the correct values for your setup:
|
|
45
|
+
|
|
46
|
+
- `setup_user` — user on the server with sudo access
|
|
47
|
+
- `deploy_user` — user that will own the app directories
|
|
48
|
+
- `server` — hostname or IP of the server
|
|
49
|
+
- `app_name` — short name for the Rails application
|
|
50
|
+
- `hostname` — the hostname that will be used to access the app (DNS must resolve)
|
|
51
|
+
|
|
52
|
+
> Make sure MySQL and Nginx are installed on the server. Caput will notify you if these dependencies are missing.
|
|
53
|
+
|
|
54
|
+
### 3. Prepare the server
|
|
55
|
+
|
|
56
|
+
Run:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
caput server
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Caput will:
|
|
63
|
+
|
|
64
|
+
- Validate prerequisites (MySQL client/server, Nginx, setup user, DNS)
|
|
65
|
+
- Create the Capistrano-compatible folder structure
|
|
66
|
+
- Set up a **systemd service** for Puma to serve requests in the background
|
|
67
|
+
- Configure permissions for the `deploy` user
|
|
68
|
+
|
|
69
|
+
> Caput does not deploy the Rails app. After this, you can deploy using standard Capistrano commands.
|
|
70
|
+
|
|
71
|
+
### 4. Configure the local repository
|
|
72
|
+
|
|
73
|
+
Run:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
caput local
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This will:
|
|
80
|
+
|
|
81
|
+
- Add Capistrano configuration and necessary files to your local Rails repository
|
|
82
|
+
- Make the repository ready for deployments
|
|
83
|
+
|
|
84
|
+
> Currently, `caput server` and `caput local` must be run separately, in that order. In the future, these may be combined into a single `caput setup` command.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
89
|
+
|
|
90
|
+
- Only a few prerequisites must be handled manually: MySQL, Nginx, setup user with sudo access, and DNS for the application hostname.
|
|
91
|
+
- Caput is designed to allow hosting multiple Rails applications on the same server. Each application gets its own folder structure, systemd service, and Nginx configuration.
|
|
92
|
+
|
data/exe/caput
ADDED
data/lib/caput/cli.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'caput/config'
|
|
5
|
+
require 'caput/remote'
|
|
6
|
+
require 'caput/server'
|
|
7
|
+
require 'caput/local'
|
|
8
|
+
require 'caput/teardown'
|
|
9
|
+
|
|
10
|
+
module Caput
|
|
11
|
+
class CLI < Thor
|
|
12
|
+
desc 'init', 'Create a local caput.conf with sample settings'
|
|
13
|
+
def init
|
|
14
|
+
Caput::Config.init_config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc 'server', 'Prepare the remote server for deployment'
|
|
18
|
+
def server
|
|
19
|
+
Caput::Config.load_config!
|
|
20
|
+
Caput::Server.check_dependencies
|
|
21
|
+
Caput::Server.prepare
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc 'local', 'Prepare the local Rails application for Capistrano'
|
|
25
|
+
def local
|
|
26
|
+
Caput::Config.load_config!
|
|
27
|
+
Caput::Local.prepare
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
desc 'teardown', 'Remove application-specific server configuration'
|
|
31
|
+
def teardown
|
|
32
|
+
Caput::Config.load_config!
|
|
33
|
+
Caput::Teardown.run
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/caput/config.rb
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Caput
|
|
4
|
+
module Config
|
|
5
|
+
CONFIG_FILE = 'caput.conf'
|
|
6
|
+
|
|
7
|
+
def self.init_config
|
|
8
|
+
return puts 'Configuration file already exists.' if File.exist?(CONFIG_FILE)
|
|
9
|
+
File.write(CONFIG_FILE, <<~CONF)
|
|
10
|
+
# Sample configuration for caput deployment
|
|
11
|
+
|
|
12
|
+
# Name of the application. This name will be used for the nginx site as well
|
|
13
|
+
# as for the Puma service definition.
|
|
14
|
+
APP_NAME="myapp"
|
|
15
|
+
|
|
16
|
+
# Setup user on the server. Note that this user should have passwordless sudo
|
|
17
|
+
# access on the server. This user is only used while setting up the application.
|
|
18
|
+
# This user must exist on the server, if it doesn't the script will notify the user
|
|
19
|
+
# and exit.
|
|
20
|
+
SETUP_USER="setup"
|
|
21
|
+
|
|
22
|
+
# Deploy user on the server. This is the user that will own the application and also
|
|
23
|
+
# run the Puma process that will run the application. If this user does not exist it
|
|
24
|
+
# will be created.
|
|
25
|
+
DEPLOY_USER="deploy"
|
|
26
|
+
|
|
27
|
+
# Target server hostname or IP. This is the address to which the application will be
|
|
28
|
+
# deployed. It does not, however, have to be the hostname of the application itself, and
|
|
29
|
+
# in most cases it will not be.
|
|
30
|
+
SERVER="example.com"
|
|
31
|
+
|
|
32
|
+
# Domain for nginx site. This is the hostname that users will enter in their browsers
|
|
33
|
+
# to reach the application. DNS configuration is assumed to have been done prior to
|
|
34
|
+
# installing the application using this script.
|
|
35
|
+
DOMAIN="www.example.com"
|
|
36
|
+
|
|
37
|
+
# Ruby version to use with rbenv. This version will be installed via rbenv on the server.
|
|
38
|
+
# It needs to match the RUBY version used by the Ruby on Rails application being deployed.
|
|
39
|
+
RUBY_VERSION="3.2.2"
|
|
40
|
+
|
|
41
|
+
# Path on the server where app will be deployed. This will be the root directory on the
|
|
42
|
+
# server where the Ruby on Rails application will be deployed by Capistrano.
|
|
43
|
+
DEPLOY_PATH="/var/www/myapp"
|
|
44
|
+
|
|
45
|
+
# Git repository URL. This will be used to configure the Capistrano deployment files.
|
|
46
|
+
REPO_URL="git@example.com:username/myapp.git"
|
|
47
|
+
CONF
|
|
48
|
+
puts "Created #{CONFIG_FILE}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.load_config!
|
|
52
|
+
unless File.exist?(CONFIG_FILE)
|
|
53
|
+
abort "Configuration file #{CONFIG_FILE} not found! Run `caput init`."
|
|
54
|
+
end
|
|
55
|
+
@config = {}
|
|
56
|
+
File.readlines(CONFIG_FILE).each do |line|
|
|
57
|
+
next if line.strip.empty? || line.strip.start_with?('#')
|
|
58
|
+
if line =~ /^(\w+)=(?:"(.*)"|'(.*)'|(.*))$/
|
|
59
|
+
key = $1
|
|
60
|
+
val = $2 || $3 || $4
|
|
61
|
+
@config[key] = val.to_s
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
# populate ENV for backwards compatibility
|
|
65
|
+
@config.each { |k, v| ENV[k] = v }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.[](key)
|
|
69
|
+
@config[key]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/caput/local.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require_relative 'config'
|
|
5
|
+
|
|
6
|
+
module Caput
|
|
7
|
+
module Local
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def prepare
|
|
11
|
+
app_name = Caput::Config['APP_NAME']
|
|
12
|
+
repo_url = Caput::Config['REPO_URL']
|
|
13
|
+
deploy_path = Caput::Config['DEPLOY_PATH']
|
|
14
|
+
ruby_version = Caput::Config['RUBY_VERSION']
|
|
15
|
+
server = Caput::Config['SERVER']
|
|
16
|
+
deploy_user = Caput::Config['DEPLOY_USER']
|
|
17
|
+
|
|
18
|
+
puts "Preparing local Rails application..."
|
|
19
|
+
|
|
20
|
+
unless bundle_has?('capistrano')
|
|
21
|
+
puts "Adding Capistrano gems to Gemfile..."
|
|
22
|
+
system('bundle add capistrano capistrano-rails capistrano-rbenv')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
puts "Running bundle install..."
|
|
26
|
+
system('bundle install')
|
|
27
|
+
|
|
28
|
+
unless File.exist?('Capfile')
|
|
29
|
+
puts "Running 'cap install' to create Capfile and config directories..."
|
|
30
|
+
system('bundle exec cap install')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
ensure_file_contains('Capfile', "require 'capistrano/rails'")
|
|
34
|
+
ensure_file_contains('Capfile', "require 'capistrano/rbenv'")
|
|
35
|
+
|
|
36
|
+
FileUtils.mkdir_p('config/deploy')
|
|
37
|
+
deploy_rb = 'config/deploy.rb'
|
|
38
|
+
backup_file(deploy_rb)
|
|
39
|
+
File.open(deploy_rb, 'a') do |f|
|
|
40
|
+
f.puts <<~DEPLOY
|
|
41
|
+
|
|
42
|
+
set :application, "#{app_name}"
|
|
43
|
+
set :repo_url, "#{repo_url}"
|
|
44
|
+
set :deploy_to, "#{deploy_path}"
|
|
45
|
+
set :rbenv_type, :user
|
|
46
|
+
set :rbenv_ruby, "#{ruby_version}"
|
|
47
|
+
set :puma_bind, "unix://#{deploy_path}/shared/tmp/sockets/puma.sock"
|
|
48
|
+
set :puma_pid, "#{deploy_path}/shared/tmp/pids/puma.pid"
|
|
49
|
+
ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp
|
|
50
|
+
set :linked_files, fetch(:linked_files, []).push('config/master.key')
|
|
51
|
+
DEPLOY
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
File.open('config/deploy/production.rb', 'a') do |f|
|
|
55
|
+
f.puts "server \"#{server}\", user: \"#{deploy_user}\", roles: %w{app db web}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
FileUtils.mkdir_p(['tmp/pids', 'tmp/sockets', 'log'])
|
|
59
|
+
|
|
60
|
+
puts "\nLocal application prepared. You can now run:\n\n bundle exec cap production deploy"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def bundle_has?(gem_name)
|
|
64
|
+
out = `bundle list --name-only 2>/dev/null || true`
|
|
65
|
+
out.split("\n").any? { |l| l.include?(gem_name) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ensure_file_contains(path, line)
|
|
69
|
+
return unless File.exist?(path)
|
|
70
|
+
content = File.read(path)
|
|
71
|
+
unless content.include?(line)
|
|
72
|
+
File.open(path, 'a') { |f| f.puts line }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def backup_file(path)
|
|
77
|
+
return unless File.exist?(path)
|
|
78
|
+
bak = "#{path}.bak"
|
|
79
|
+
FileUtils.cp(path, bak)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/caput/remote.rb
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/ssh'
|
|
4
|
+
require 'net/scp'
|
|
5
|
+
require 'tempfile'
|
|
6
|
+
|
|
7
|
+
module Caput
|
|
8
|
+
class Remote
|
|
9
|
+
def initialize(user:, host:, ssh_options: {})
|
|
10
|
+
@user = user
|
|
11
|
+
@host = host
|
|
12
|
+
@ssh_options = ssh_options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def exec!(cmd)
|
|
16
|
+
exit_status = nil
|
|
17
|
+
ssh_options = @options || {}
|
|
18
|
+
|
|
19
|
+
Net::SSH.start(@host, @user, ssh_options) do |ssh|
|
|
20
|
+
ssh.open_channel do |ch|
|
|
21
|
+
ch.exec("bash -l") do |_, success|
|
|
22
|
+
raise "could not start bash" unless success
|
|
23
|
+
|
|
24
|
+
ch.on_data { |_, data| $stdout.print data }
|
|
25
|
+
ch.on_extended_data { |_, _, data| $stderr.print data }
|
|
26
|
+
ch.on_request("exit-status") { |_, data| exit_status = data.read_long }
|
|
27
|
+
|
|
28
|
+
ch.send_data(cmd)
|
|
29
|
+
ch.send_data("\nexit\n")
|
|
30
|
+
ch.eof!
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
ssh.loop
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if exit_status != 0
|
|
38
|
+
raise "Remote command failed (exit #{exit_status}): #{cmd}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def upload_content!(content, remote_path, mode: 0644)
|
|
43
|
+
Tempfile.create do |f|
|
|
44
|
+
f.binmode
|
|
45
|
+
f.write(content)
|
|
46
|
+
f.flush
|
|
47
|
+
upload_file!(f.path, remote_path, mode: mode)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def upload_file!(local_path, remote_path, mode: 0644)
|
|
52
|
+
Net::SCP.start(@host, @user, **@ssh_options) do |scp|
|
|
53
|
+
scp.upload!(local_path, remote_path)
|
|
54
|
+
end
|
|
55
|
+
exec!("sudo chmod #{sprintf('%o', mode)} #{remote_path}")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def escape_for_bash(s)
|
|
61
|
+
s.gsub('"', '"').gsub('$', '\$')
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/caput/server.rb
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'remote'
|
|
4
|
+
require_relative 'config'
|
|
5
|
+
|
|
6
|
+
module Caput
|
|
7
|
+
module Server
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def check_dependencies
|
|
11
|
+
setup_user = Caput::Config['SETUP_USER']
|
|
12
|
+
server = Caput::Config['SERVER']
|
|
13
|
+
|
|
14
|
+
remote = Caput::Remote.new(user: setup_user, host: server)
|
|
15
|
+
|
|
16
|
+
puts "\nValidating server dependencies..."
|
|
17
|
+
|
|
18
|
+
begin
|
|
19
|
+
# Check for MySQL server
|
|
20
|
+
remote.exec!(<<~BASH)
|
|
21
|
+
if ! command -v mysql >/dev/null 2>&1; then
|
|
22
|
+
echo "ERROR: MySQL client/server not installed" >&2
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
if ! systemctl is-active --quiet mysql && ! systemctl is-active --quiet mariadb; then
|
|
26
|
+
echo "ERROR: MySQL/MariaDB service is not running" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
BASH
|
|
30
|
+
|
|
31
|
+
# Check for nginx
|
|
32
|
+
remote.exec!(<<~BASH)
|
|
33
|
+
if ! command -v nginx >/dev/null 2>&1; then
|
|
34
|
+
echo "ERROR: nginx not installed" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
if ! systemctl is-active --quiet nginx; then
|
|
38
|
+
echo "ERROR: nginx service is not running" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
BASH
|
|
42
|
+
|
|
43
|
+
rescue RuntimeError => e
|
|
44
|
+
exit 1 # stop Caput immediately
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
puts "All dependencies satisfied."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def prepare
|
|
51
|
+
setup_user = Caput::Config['SETUP_USER']
|
|
52
|
+
deploy_user = Caput::Config['DEPLOY_USER']
|
|
53
|
+
server = Caput::Config['SERVER']
|
|
54
|
+
app_name = Caput::Config['APP_NAME']
|
|
55
|
+
domain = Caput::Config['DOMAIN']
|
|
56
|
+
deploy_path = Caput::Config['DEPLOY_PATH']
|
|
57
|
+
ruby_version = Caput::Config['RUBY_VERSION']
|
|
58
|
+
|
|
59
|
+
raise 'Missing configuration: run `caput init` and edit caput.conf' unless setup_user && server && deploy_user && app_name
|
|
60
|
+
|
|
61
|
+
check_sudo!(setup_user, server)
|
|
62
|
+
|
|
63
|
+
remote = Caput::Remote.new(user: setup_user, host: server)
|
|
64
|
+
|
|
65
|
+
puts "\nInstalling system dependencies on remote..."
|
|
66
|
+
remote.exec!(<<~BASH)
|
|
67
|
+
sudo apt-get update || true
|
|
68
|
+
sudo apt-get install -y git curl build-essential libssl-dev libreadline-dev zlib1g-dev libffi-dev libyaml-dev libgdbm-dev libncurses-dev libdb-dev libsqlite3-dev libgmp-dev libbz2-dev autoconf bison pkg-config liblzma-dev libxml2-dev libxslt1-dev libcurl4-openssl-dev nginx || true
|
|
69
|
+
BASH
|
|
70
|
+
|
|
71
|
+
puts "\nVerify deploy user account..."
|
|
72
|
+
remote.exec!(<<~BASH)
|
|
73
|
+
if getent passwd #{deploy_user} >/dev/null 2>&1; then
|
|
74
|
+
echo "Deploy user already exists"
|
|
75
|
+
else
|
|
76
|
+
echo "Creating deploy user #{deploy_user}"
|
|
77
|
+
sudo adduser --disabled-password --gecos "" #{deploy_user}
|
|
78
|
+
fi
|
|
79
|
+
BASH
|
|
80
|
+
|
|
81
|
+
puts "\nEnsure deploy authorized_keys and home..."
|
|
82
|
+
remote.exec!(<<~BASH)
|
|
83
|
+
DEPLOY_HOME="$(getent passwd #{deploy_user} | cut -d: -f6)"
|
|
84
|
+
DEPLOY_SSH_DIR="$DEPLOY_HOME/.ssh"
|
|
85
|
+
SETUP_HOME="$(getent passwd #{setup_user} | cut -d: -f6)"
|
|
86
|
+
|
|
87
|
+
sudo mkdir -p "$DEPLOY_SSH_DIR"
|
|
88
|
+
sudo chown #{deploy_user}:#{deploy_user} "$DEPLOY_SSH_DIR"
|
|
89
|
+
sudo chmod 700 "$DEPLOY_SSH_DIR"
|
|
90
|
+
|
|
91
|
+
AUTH_KEYS="$DEPLOY_SSH_DIR/authorized_keys"
|
|
92
|
+
if [ ! -s "$AUTH_KEYS" ]; then
|
|
93
|
+
echo "Copying authorized_keys from setup user"
|
|
94
|
+
sudo cp "$SETUP_HOME/.ssh/authorized_keys" "$AUTH_KEYS" || true
|
|
95
|
+
sudo chown #{deploy_user}:#{deploy_user} "$AUTH_KEYS" || true
|
|
96
|
+
sudo chmod 600 "$AUTH_KEYS" || true
|
|
97
|
+
else
|
|
98
|
+
echo "Authorized keys already present"
|
|
99
|
+
fi
|
|
100
|
+
BASH
|
|
101
|
+
|
|
102
|
+
puts "\nCreate deploy directories..."
|
|
103
|
+
remote.exec!(<<~BASH)
|
|
104
|
+
if [ -d "#{deploy_path}" ]; then
|
|
105
|
+
echo "Deploy directories already exist"
|
|
106
|
+
else
|
|
107
|
+
sudo mkdir -p "#{deploy_path}/shared/tmp/pids" "#{deploy_path}/shared/tmp/sockets" "#{deploy_path}/shared/log"
|
|
108
|
+
sudo chown -R #{deploy_user}:#{deploy_user} "#{deploy_path}"
|
|
109
|
+
fi
|
|
110
|
+
BASH
|
|
111
|
+
|
|
112
|
+
puts "\nInstall Nginx site config..."
|
|
113
|
+
nginx_conf = <<~NGINX
|
|
114
|
+
server {
|
|
115
|
+
listen 80;
|
|
116
|
+
server_name #{domain};
|
|
117
|
+
|
|
118
|
+
root #{deploy_path}/current/public;
|
|
119
|
+
|
|
120
|
+
location / {
|
|
121
|
+
try_files $uri @puma;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
location @puma {
|
|
125
|
+
proxy_pass http://unix:#{deploy_path}/shared/tmp/sockets/puma.sock;
|
|
126
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
127
|
+
proxy_set_header Host $http_host;
|
|
128
|
+
proxy_redirect off;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
error_page 500 502 503 504 /500.html;
|
|
132
|
+
}
|
|
133
|
+
NGINX
|
|
134
|
+
|
|
135
|
+
remote.upload_content!(nginx_conf, "/tmp/#{app_name}.nginx")
|
|
136
|
+
remote.exec!(<<~BASH)
|
|
137
|
+
sudo mv /tmp/#{app_name}.nginx /etc/nginx/sites-available/#{app_name}
|
|
138
|
+
sudo ln -sf /etc/nginx/sites-available/#{app_name} /etc/nginx/sites-enabled/#{app_name}
|
|
139
|
+
sudo nginx -t
|
|
140
|
+
sudo systemctl reload nginx
|
|
141
|
+
BASH
|
|
142
|
+
|
|
143
|
+
puts "\nSetup Puma systemd service..."
|
|
144
|
+
service_file = <<~SERVICE
|
|
145
|
+
[Unit]
|
|
146
|
+
Description=Puma HTTP Server for #{app_name}
|
|
147
|
+
After=network.target
|
|
148
|
+
|
|
149
|
+
[Service]
|
|
150
|
+
Type=simple
|
|
151
|
+
User=#{deploy_user}
|
|
152
|
+
WorkingDirectory=#{deploy_path}/current
|
|
153
|
+
ExecStart=#{deploy_path}/shared/bin/start_puma.sh
|
|
154
|
+
Restart=always
|
|
155
|
+
|
|
156
|
+
[Install]
|
|
157
|
+
WantedBy=multi-user.target
|
|
158
|
+
SERVICE
|
|
159
|
+
|
|
160
|
+
remote.upload_content!(service_file, "/tmp/#{app_name}-puma.service")
|
|
161
|
+
remote.exec!(<<~BASH)
|
|
162
|
+
sudo mv /tmp/#{app_name}-puma.service /etc/systemd/system/#{app_name}-puma.service
|
|
163
|
+
sudo systemctl daemon-reload
|
|
164
|
+
sudo systemctl enable #{app_name}-puma
|
|
165
|
+
BASH
|
|
166
|
+
|
|
167
|
+
puts "Create a simple puma.rb and start script in shared (if missing)"
|
|
168
|
+
puma_rb = <<~PUMA
|
|
169
|
+
threads 0,16
|
|
170
|
+
workers 1
|
|
171
|
+
app_dir = "#{deploy_path}/current"
|
|
172
|
+
shared_dir = "#{deploy_path}/shared"
|
|
173
|
+
bind "unix://\#{shared_dir}/tmp/sockets/puma.sock"
|
|
174
|
+
pidfile "\#{shared_dir}/tmp/pids/puma.pid"
|
|
175
|
+
stdout_redirect "\#{shared_dir}/log/puma.stdout.log", "\#{shared_dir}/log/puma.stderr.log", true
|
|
176
|
+
PUMA
|
|
177
|
+
|
|
178
|
+
start_sh = <<~SH
|
|
179
|
+
#!/bin/bash
|
|
180
|
+
export RBENV_ROOT="$HOME/.rbenv"
|
|
181
|
+
export PATH="$RBENV_ROOT/bin:$PATH"
|
|
182
|
+
eval "$(rbenv init -)"
|
|
183
|
+
cd #{deploy_path}/current || exit 1
|
|
184
|
+
exec $RBENV_ROOT/shims/bundle exec puma -C #{deploy_path}/shared/puma.rb
|
|
185
|
+
SH
|
|
186
|
+
|
|
187
|
+
remote.upload_content!(puma_rb, "/tmp/puma.rb")
|
|
188
|
+
remote.exec!("sudo mv -f /tmp/puma.rb #{deploy_path}/shared/puma.rb || true")
|
|
189
|
+
remote.upload_content!(start_sh, "/tmp/start_puma.sh")
|
|
190
|
+
remote.exec!(<<~BASH)
|
|
191
|
+
sudo mkdir -p #{deploy_path}/shared/bin
|
|
192
|
+
sudo mv -f /tmp/start_puma.sh #{deploy_path}/shared/bin/start_puma.sh
|
|
193
|
+
sudo chown -R #{deploy_user}:#{deploy_user} #{deploy_path}/shared
|
|
194
|
+
sudo chmod +x #{deploy_path}/shared/bin/start_puma.sh
|
|
195
|
+
BASH
|
|
196
|
+
|
|
197
|
+
puts "\nInstall rbenv and Ruby for deploy user..."
|
|
198
|
+
deploy_remote = Caput::Remote.new(user: deploy_user, host: server)
|
|
199
|
+
deploy_remote.exec!(<<~BASH)
|
|
200
|
+
RBENV_DIR="$HOME/.rbenv"
|
|
201
|
+
if [ ! -d "$RBENV_DIR" ]; then
|
|
202
|
+
git clone https://github.com/rbenv/rbenv.git "$RBENV_DIR"
|
|
203
|
+
mkdir -p "$RBENV_DIR/plugins"
|
|
204
|
+
git clone https://github.com/rbenv/ruby-build.git "$RBENV_DIR/plugins/ruby-build"
|
|
205
|
+
else
|
|
206
|
+
echo "rbenv already installed"
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
export RBENV_ROOT="$RBENV_DIR"
|
|
210
|
+
export PATH="$RBENV_ROOT/bin:$PATH"
|
|
211
|
+
eval "$(rbenv init -)"
|
|
212
|
+
|
|
213
|
+
if ! rbenv versions | grep -q "#{ruby_version}"; then
|
|
214
|
+
rbenv install "#{ruby_version}" || true
|
|
215
|
+
else
|
|
216
|
+
echo "Ruby #{ruby_version} already installed"
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
rbenv global "#{ruby_version}"
|
|
220
|
+
rbenv rehash
|
|
221
|
+
|
|
222
|
+
if ! grep -q 'rbenv init' ~/.bashrc; then
|
|
223
|
+
echo 'export RBENV_ROOT=\"$HOME/.rbenv\"' >> ~/.bashrc
|
|
224
|
+
echo 'export PATH=\"$RBENV_ROOT/bin:$PATH\"' >> ~/.bashrc
|
|
225
|
+
echo 'eval \"$(rbenv init -)\"' >> ~/.bashrc
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
gem install bundler --no-document || true
|
|
229
|
+
rbenv rehash
|
|
230
|
+
BASH
|
|
231
|
+
|
|
232
|
+
puts "\nServer preparation complete."
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def check_sudo!(user, server)
|
|
236
|
+
cmd = %{ssh #{user}@#{server} "sudo -n true"}
|
|
237
|
+
puts "Checking passwordless sudo for #{user}@#{server}..."
|
|
238
|
+
success = system(cmd)
|
|
239
|
+
unless success && $?.exitstatus == 0
|
|
240
|
+
abort <<~MSG
|
|
241
|
+
Warning: Setup user #{user} does not have passwordless sudo on #{server}.
|
|
242
|
+
Example sudoers entry:
|
|
243
|
+
#{user} ALL=(ALL) NOPASSWD:ALL
|
|
244
|
+
MSG
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'remote'
|
|
4
|
+
require_relative 'config'
|
|
5
|
+
|
|
6
|
+
module Caput
|
|
7
|
+
module Teardown
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def run
|
|
11
|
+
setup_user = Caput::Config['SETUP_USER']
|
|
12
|
+
server = Caput::Config['SERVER']
|
|
13
|
+
app_name = Caput::Config['APP_NAME']
|
|
14
|
+
|
|
15
|
+
raise 'Missing configuration' unless setup_user && server && app_name
|
|
16
|
+
|
|
17
|
+
remote = Caput::Remote.new(user: setup_user, host: server)
|
|
18
|
+
|
|
19
|
+
puts "Tearing down application-specific server configuration on #{server} ..."
|
|
20
|
+
remote.exec!(<<~BASH)
|
|
21
|
+
sudo systemctl stop #{app_name}-puma || true
|
|
22
|
+
sudo systemctl disable #{app_name}-puma || true
|
|
23
|
+
sudo rm -f /etc/systemd/system/#{app_name}-puma.service || true
|
|
24
|
+
sudo systemctl daemon-reload || true
|
|
25
|
+
sudo rm -f /etc/nginx/sites-available/#{app_name} || true
|
|
26
|
+
sudo rm -f /etc/nginx/sites-enabled/#{app_name} || true
|
|
27
|
+
sudo nginx -t || true
|
|
28
|
+
sudo systemctl reload nginx || true
|
|
29
|
+
BASH
|
|
30
|
+
|
|
31
|
+
puts "Teardown complete."
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/caput.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
#require_relative 'caput/version'
|
|
5
|
+
#require_relative 'caput/cli'
|
|
6
|
+
|
|
7
|
+
require "caput/cli"
|
|
8
|
+
require "caput/config"
|
|
9
|
+
require "caput/server"
|
|
10
|
+
require "caput/local"
|
|
11
|
+
require "caput/teardown"
|
|
12
|
+
|
|
13
|
+
module Caput
|
|
14
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: caput
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.9.5
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Martin Bergek
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-09-09 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: thor
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.4'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.4'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: net-ssh
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '7.3'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '7.3'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: net-scp
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '4.1'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '4.1'
|
|
55
|
+
description: Caput automates the repetitive steps required to make a Rails application
|
|
56
|
+
ready to deploy on a fresh Ubuntu server. It ensures dependencies are installed,
|
|
57
|
+
configures users, directories, and permissions, and sets up the environment for
|
|
58
|
+
Capistrano deployments — all without requiring Passenger or container registries.
|
|
59
|
+
With Caput, developers can enjoy the simplicity of Capistrano while minimising manual
|
|
60
|
+
server setup.
|
|
61
|
+
email:
|
|
62
|
+
- contact@spotwise.com
|
|
63
|
+
executables:
|
|
64
|
+
- caput
|
|
65
|
+
extensions: []
|
|
66
|
+
extra_rdoc_files: []
|
|
67
|
+
files:
|
|
68
|
+
- LICENSE
|
|
69
|
+
- README.md
|
|
70
|
+
- exe/caput
|
|
71
|
+
- lib/caput.rb
|
|
72
|
+
- lib/caput/cli.rb
|
|
73
|
+
- lib/caput/config.rb
|
|
74
|
+
- lib/caput/local.rb
|
|
75
|
+
- lib/caput/remote.rb
|
|
76
|
+
- lib/caput/server.rb
|
|
77
|
+
- lib/caput/teardown.rb
|
|
78
|
+
- lib/caput/version.rb
|
|
79
|
+
homepage: https://github.com/mbergek/caput
|
|
80
|
+
licenses:
|
|
81
|
+
- MIT
|
|
82
|
+
metadata: {}
|
|
83
|
+
post_install_message:
|
|
84
|
+
rdoc_options: []
|
|
85
|
+
require_paths:
|
|
86
|
+
- lib
|
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '3.0'
|
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
requirements: []
|
|
98
|
+
rubygems_version: 3.5.22
|
|
99
|
+
signing_key:
|
|
100
|
+
specification_version: 4
|
|
101
|
+
summary: Simplifies preparing Ubuntu servers for Rails apps with a Capistrano-friendly
|
|
102
|
+
workflow
|
|
103
|
+
test_files: []
|