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.
ls -la ~/languages/
- drwxr-xr-x
- ▶ PHP
- ▶ Ruby
- ▶ Scala
- ▶ C#
- ▶ JavaScript
- ▶ Objective-C
- ▶ Shell Scripting
ls -la ~/toolchain/
- drwxr-xr-x
- ▶ Typo3
- ▶ Akka
- ▶ Capistrano
- ▶ Git
- ▶ MAMP
- ▶ Adobe Illustrator
- ▶ NSTrackingArea (Cocoa)
uname -a
- 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
| Key | Value |
|---|---|
| editor | Terminal-first workflow |
| os | Mac OS X / Unix |
| vcs | Git, distributed version control |
| deploy | Capistrano, cron automation |
| graphics | SVG, Adobe Illustrator troubleshooting |
| networking | IP validation, SSH, VPN |
git log --oneline --reverse
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.
Solving NDK build issues on OS X
Troubleshooting native development kit compilation problems on Mac OS X.
Programmatically adding PHP generated TypoScript to the backend configuration
Integrating dynamically generated TypoScript into Typo3 backend setups using PHP.
ArgumentError: Could not parse PKey: no start line
Debugging an SSH key parsing error encountered during deployment.
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.
Cocoa: Using NSTrackingArea
A short tutorial on using Cocoa's NSTrackingArea to capture mouseEntered and mouseExited events.
cat ~/contact.txt