whoami

Lucas Jenß

cat /etc/motd

The Coding Journal ツ — Notes taken on an epic coding journey. Technical solutions, debugging notes, and practical guides from the trenches of software development.

Close-up of white dominoes with black dots standing on green felt surface, shallow depth of field, focused mood

ls -la ~/languages/

total 8
drwxr-xr-x
▶ PHP
▶ Ruby
▶ Scala
▶ C#
▶ JavaScript
▶ Objective-C
▶ Shell Scripting

ls -la ~/toolchain/

total 7
drwxr-xr-x
▶ Typo3
▶ Akka
▶ Capistrano
▶ Git
▶ MAMP
▶ Adobe Illustrator
▶ NSTrackingArea (Cocoa)

uname -a

platforms
drwxr-xr-x
▶ Mac OS X
▶ Unix

Migrating a Ruby deployment from Capistrano 2 to Capistrano 3

A few months ago I sat at my desk in Melbourne, watching a deploy to a Sydney-based staging server crawl along because the old Capistrano 2 setup kept timing out over SSH. The stack itself was fine; the toolchain was the problem. After a decade of quiet stability, Capistrano 3 rewrote the rules, and the upgrade touched almost every file in the project's config directory. If you maintain a Ruby on Rails application that still uses the older deployment recipe, you will eventually face the same decision: keep patching the legacy pipeline or bite the bullet and migrate.

Capistrano 2 was built on Net::SSH and exposed a simple set of rake-style tasks. Developers loved it because configuration lived in a single deploy.rb and the syntax was forgiving. Capistrano 3 swapped that foundation for SSHKit, introduced a strict configuration block, and split responsibilities between the core gem, the Rails plugin, and a handful of community extensions. The change was disruptive, and several Australian engineering teams I spoke with put off the upgrade for two or three years because their pipelines still worked in production, even if slowly.

The migration is more than a syntax update. Permission semantics changed, environment variables behave differently between stages, and any custom task you wrote will probably need to be ported line by line. You also need to plan around deploy windows that suit teams spread across AEST and AEDT, particularly when the production environment sits behind an AWS Sydney region. I walked through this process on a project described in my deployment notes, and the steps below are the condensed version of that journey.

This article is not a comprehensive reference manual. It focuses on the parts that bite teams who have been sitting on Capistrano 2 for years: the structural rewrite, the new task DSL, SSHKit quirks, and a practical checklist for testing the new pipeline against the old one.

Aspect Capistrano 2 Capistrano 3
Configuration root set :variable, value at top of deploy.rb Wrapped in configure block
SSH layer Net::SSH SSHKit
Task syntax run "cd #{release_path} && bundle install" execute :bundle, :install, "--path #{fetch(:bundle_path)}"
Hooks before "deploy:symlink", "deploy:custom" before :symlink, :custom or string form
Multistage config Loose config/deploy/stage.rb files Strict config/deploy/stage.rb requiring explicit server entries
Bundler integration Manual bundle install in custom task Bundler plugin loaded via Capfile
Rollback cap deploy:rollback cap production deploy:rollback

What Changed and Why the Rewrite Mattered

The maintainers rebuilt Capistrano because the original code base had grown difficult to extend. Plugins had no clean contract, the rake task list was a long flat file, and testing a custom deploy recipe locally was painful. SSHKit solved the SSH dependency problem by giving every command a clear interface that returns output streams you can inspect. The team also formalised the role system, so a db server could be targeted separately from web and app without contorting the configuration.

The practical consequence for Australian teams running on local infrastructure is that the new version is genuinely faster on flaky NBN connections, because persistent SSH sessions are managed more carefully and reconnects are less common. That is not a marketing claim you can find in the official changelog, but it shows up in deployment metrics when you compare a Sydney region deploy against the older Net::SSH path.

Restructuring the Project for Capistrano 3

Begin by creating a fresh Capfile. In Capistrano 2 you could get away with a minimal file that simply loaded the deployment recipe, but Capistrano 3 expects explicit require statements for the core gem and any plugin you use. A typical setup includes capistrano/rails, capistrano/bundler, capistrano/rails/migrations, and capistrano/rails/assets. Each plugin needs to be installed as a separate gem, which is a change that surprises teams who were used to a single dependency.

Then split your deploy.rb into a shared base file and a per-stage file under config/deploy/. The shared file holds defaults that apply to every environment, and each stage file (production, staging, canary) defines the server list, branch, and any overrides. Place production secrets in a file ignored by version control and load them through a helper. Australian companies subject to the Privacy Act and the Notifiable Data Breaches scheme should keep deploy credentials and database URIs out of git history, even on internal repositories.

Rewriting Tasks in the New DSL

The biggest day-to-day change is the task syntax. Where Capistrano 2 had run "rake db:migrate", Capistrano 3 expects execute :rake, "db:migrate". The new form takes individual arguments rather than a single shell string, which makes it harder to accidentally inject spaces or misquote variables. You also need to declare task :name do; on roles(:app) do; ... end; end, which gives you a clear place to scope commands to particular server roles.

Custom roles are simpler to define. In the old DSL you wrote role :web, "..." and Capistrano inferred role assignment from task placement. In Capistrano 3 you declare server "host", user: "deploy", roles: %w[web app] inside the stage file, then reference roles(:web) in your task. This explicit declaration makes it much easier to reason about which commands land on which box, particularly for teams running dedicated migration hosts in AWS Sydney.

If you wrote hooks in Capistrano 2 with before "deploy:symlink", "deploy:custom", the equivalent in Capistrano 3 is similar but slightly stricter: before :starting, :custom for new-style flow hooks, or the older string form for backwards compatibility. I kept the string form for one cycle so my team in Brisbane could read the recipes without learning the new symbol syntax.

Working with SSHKit and Connection Options

SSHKit replaces Net::SSH as the underlying transport and exposes a more predictable API. The most useful change is the ability to set forward agents and proxy settings in ssh_options. If your Australian hosting provider only allows key-based authentication from a bastion host, configure ssh_options[:forward_agent] = true and set the proxy in the stage file. A few teams I have worked with at local SaaS companies in Sydney also need ssh_options[:keys] = ["~/.ssh/deploy.pem"] because their managed SSH policies reject default key locations.

Persistent connections are managed automatically by SSHKit, but you can force a reset with within release_path do; with rails_env: fetch(:rails_env) do; execute :rake, "cache:clear"; end; end. The nested with blocks let you scope environment variables and working directory in a readable way. If you previously relied on default_environment in Capistrano 2, the new equivalent is set :default_env, { path: "/usr/local/bin:$PATH" } in the shared deploy.rb.

One quirk worth noting: SSHKit raises its own exception types, so any rescue blocks in your custom tasks must catch SSHKit::Command::Failed rather than the older Capistrano::Command::Failed. I learned this the hard way during a midnight deploy that swallowed its own error and then failed the symlink step in silence.

Multistage Deployments and the New Environment Model

Capistrano 3 keeps the multistage concept but tightens it. Every stage file must declare at least one server, and missing declarations will fail at parse time. For a typical Australian ecommerce setup you may have a canary environment in AWS Melbourne for performance testing before promoting the release to a Sydney production cluster. The stage files make that promotion explicit, which removes the ambiguity that used to lead to accidental deploys.

When migrating, port the variable assignments carefully. set :branch, "main" lives in the stage file now, not in a global override. The :keep_releases default dropped from 5 to 3, so check your disk usage on long-running servers before relying on it. If your team bills in Australian dollars for hosting, note that the new pipeline tends to use slightly less disk and bandwidth because the persistent SSH sessions reduce handshake overhead.

Testing the Migration and Rolling It Out Safely

Before cutting over, run both pipelines against a non-production stage and compare timing and output. The simplest harness is a fresh staging box provisioned through your usual IaC tooling, pointed at by the new stage file, while the old pipeline still runs against the original target. I usually leave both running for a week, then disable the legacy scripts.

Smoke-test the rollback path manually. In Capistrano 3 the rollback is cap production deploy:rollback, and it works correctly only if your migration hooks are idempotent and your symlinks point to stable shared paths. I once watched a team in Perth lose two deploy windows because an old migration tried to run on a rolled-back release, which is why I now keep migrations in a separate task that runs only on the primary deploy path.

Finally, remove the Capistrano 2 gems from the Gemfile and commit a clear migration note for your team. A good deployment pipeline should fade into the background, and once you have completed this migration the new DSL is easier to teach to the next junior engineer joining your Brisbane or Adelaide office. If you want a working sample of the resulting Capfile and stage files I used, the original write-up with annotated snippets is on my site.

Pull up a flat white, carve out a quiet afternoon, and start by drafting the new Capfile against a disposable staging environment.


cat ~/interests.json

KeyValue
editorTerminal-first workflow
osMac OS X / Unix
vcsGit, distributed version control
deployCapistrano, cron automation
graphicsSVG, Adobe Illustrator troubleshooting
networkingIP validation, SSH, VPN

git log --oneline --reverse

2013-10-30

Solving SVG import issues in Adobe Illustrator CS6 and CC

When importing an SVG into Illustrator, the operation fails with an unknown error [CANT]. A workaround for this Adobe-side bug.

2013

Solving NDK build issues on OS X

Troubleshooting native development kit compilation problems on Mac OS X.

2013

Programmatically adding PHP generated TypoScript to the backend configuration

Integrating dynamically generated TypoScript into Typo3 backend setups using PHP.

2013

ArgumentError: Could not parse PKey: no start line

Debugging an SSH key parsing error encountered during deployment.

2011-08-04

Validating IP-Addresses in PHP

Using PHP filter functions with flags like FILTER_FLAG_IPV4 and FILTER_FLAG_IPV6, and understanding how filter_var handles reserved IP addresses.

2011-07-09

Cocoa: Using NSTrackingArea

A short tutorial on using Cocoa's NSTrackingArea to capture mouseEntered and mouseExited events.


cat ~/contact.txt

github: github.com/x3ro
stackoverflow: x3ro
coderwall: coderwall.com/x3ro
twitter: @x3rames