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 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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'caput'
5
+ Caput::CLI.start(ARGV)
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Caput
2
+ VERSION = '0.9.5'
3
+ 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: []