Skip to content

feedbin/capistrano-bluey

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

capistrano-bluey

Blue/green deploys for Capistrano.

Each app host runs two instances of a systemd template unit — e.g. myapp-web@blue.service and myapp-web@green.service. One color is active, receiving traffic from nginx through a live unix-socket symlink; the other is idle, still holding the previous release, kept warm for instant rollback.

A deploy boots the idle color with the new release, health-checks it over its unix socket, then atomically cuts traffic over by flipping the symlink and reloading nginx. If the health check fails, the new color is stopped and the deploy aborts — the old color never stopped serving.

Installation

In your Gemfile:

gem "capistrano-bluey", github: "feedbin/capistrano-bluey"

In your Capfile:

require "capistrano/bluey"
install_plugin Capistrano::Bluey

Infrastructure expectations

The gem assumes this infrastructure already exists on each host with the :app role:

  • A systemd template unit (e.g. myapp-web@.service) whose instance name is the color. Each instance must bind its own socket named <bluey_app_server>-%i.sock (e.g. puma-blue.sock, puma-green.sock) in the socket directory.
  • Each color runs the release at <deploy_to>/current-<color> (e.g. the unit's WorkingDirectory= points at current-%i). The gem manages these symlinks; you don't create them.
  • An nginx upstream pointing at the live symlink, <bluey_app_server>.sock (e.g. puma.sock), in the socket directory. On the first deploy the gem creates this symlink automatically, pointing at blue.
  • The deploy user can run sudo systemctl start/stop/is-active on the units and sudo systemctl reload nginx without a password prompt.
  • The app exposes an HTTP health-check endpoint that returns 2xx when ready (Rails 7.1+ ships one at /up).
  • curl and timeout are available on the hosts.

Settings

Setting Default Description
bluey_app_server (required) Socket name prefix, e.g. "puma" or "pitchfork". Sockets are named <bluey_app_server>-<color>.sock, with the live symlink at <bluey_app_server>.sock.
bluey_service_template (required) printf-style systemd unit name with %{color}, e.g. "myapp-web@%{color}.service".
bluey_health_check_host (required) Value for the Host: header sent with the health-check request.
bluey_health_check_path "/up" Path requested over the unix socket during the health check.
bluey_socket_dir #{shared_path}/tmp/sockets Directory containing the per-color sockets and the live symlink.
bluey_health_timeout 90 Seconds to wait for the new color to become healthy before aborting.
bluey_health_interval 1 Seconds between health-check polls.

Example deploy.rb:

set :bluey_app_server, "puma"
set :bluey_service_template, "myapp-web@%{color}.service"
set :bluey_health_check_host, "myapp.example.com"

Wiring it into your deploy

The plugin defines tasks but adds no hooks — you decide when the blue/green cutover runs. The usual approach is to override deploy:restart in deploy.rb:

namespace :deploy do
  task :restart do
    invoke "bluey:deploy"
  end
end

after "deploy:published", "deploy:restart"

Tasks

cap <stage> bluey:deploy

Runs in two passes. First pass, per host: restarts the idle unit on the new release (stop, repoint current-<color>, start) and polls the health-check endpoint over the unix socket. Second pass, only once every host is healthy: flips the live symlink and reloads nginx on each host. If any host fails its health check, that color is stopped and the deploy aborts — traffic never moved.

cap <stage> bluey:flip_back

Instant rollback: flips the live symlink back to the previous color, reloads nginx, and repoints current at that color's release. Precondition: the previous color's unit must still be active and its current-<color> symlink must exist — i.e. no deploy has happened since. Otherwise the task aborts; use cap <stage> deploy:rollback instead.

cap <stage> bluey:status

Shows, for every app host: where the live symlink points, and for each color which release current-<color> points at plus its systemd state:

bluey: app1.example.com
  puma.sock -> puma-green.sock
  current-blue -> /srv/apps/myapp/releases/20260609111111 [inactive]
  current-green -> /srv/apps/myapp/releases/20260610090909 [active]

How a deploy works

The live symlink (<bluey_app_server>.sock) is the sole source of truth for which color is active on a host. Each host resolves its own active/idle colors independently by reading that symlink — there is no shared state.

Symlink flips are atomic: the new link is created at a temporary name and renamed over the destination (ln -sfn + mv -fT). A bare ln -sfn would unlink and recreate the path, leaving a window where a request hitting nginx gets ENOENT from the upstream socket.

This means the fleet can end up on mixed colors, e.g. after a deploy that failed partway through cutover. That's not an error: as long as every host is serving the same release, traffic is consistent. Use bluey:status to confirm releases match across hosts.

The old color stays running until the next deploy stops it (to reuse its slot for the new release). So the instant-rollback window is "until the next deploy starts."

Rollback semantics

  • bluey:flip_back rolls back code only, never migrations. For this to be safe, migrations must follow expand/contract discipline: a deploy may add columns and tables, but must not drop or rename anything the previous release's code depends on. If a deploy includes a destructive migration, accept that flipping back is not safe for that deploy.
  • If flip_back is impossible (the previous color was already stopped by a subsequent deploy), use cap <stage> deploy:rollback, which rebuilds the prior release through the normal blue/green flow.

License

MIT

About

Blue/green deploys for Capistrano

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages