Skip to content

Commit

Permalink
Use local docker registry to push and pull app images
Browse files Browse the repository at this point in the history
Allow applications to be deployed without needing to set up a repository
in a remote Docker registry.

If the registry server starts with `localhost`, Kamal will start a local
docker registry on that port and push the app image to it.

Then when pulling the image onto the servers, we use net-ssh to forward
the that port from the app server to the deployment server.
  • Loading branch information
djmb authored and npezza93 committed Jan 14, 2025
1 parent 1547089 commit f24e74c
Show file tree
Hide file tree
Showing 19 changed files with 323 additions and 91 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Setup Ruby and install gems
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
ruby-version: 3.4.1
bundler-cache: true
- name: Run Rubocop
run: bundle exec rubocop --parallel
Expand All @@ -28,7 +28,7 @@ jobs:
- "3.1"
- "3.2"
- "3.3"
- "3.4.0-preview2"
- "3.4"
gemfile:
- Gemfile
- gemfiles/rails_edge.gemfile
Expand Down
71 changes: 35 additions & 36 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,23 @@ PATH
GEM
remote: https://rubygems.org/
specs:
actionpack (8.0.0.1)
actionview (= 8.0.0.1)
activesupport (= 8.0.0.1)
actionpack (8.0.1)
actionview (= 8.0.1)
activesupport (= 8.0.1)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actionview (8.0.0.1)
activesupport (= 8.0.0.1)
actionview (8.0.1)
activesupport (= 8.0.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activesupport (8.0.0.1)
activesupport (8.0.1)
base64
benchmark (>= 0.3)
bigdecimal
Expand All @@ -48,32 +48,30 @@ GEM
ast (2.4.2)
base64 (0.2.0)
bcrypt_pbkdf (1.1.1)
bcrypt_pbkdf (1.1.1-arm64-darwin)
bcrypt_pbkdf (1.1.1-x86_64-darwin)
benchmark (0.4.0)
bigdecimal (3.1.8)
bigdecimal (3.1.9)
builder (3.3.0)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
connection_pool (2.5.0)
crass (1.0.6)
date (3.4.1)
debug (1.9.2)
debug (1.10.0)
irb (~> 1.10)
reline (>= 0.3.8)
dotenv (3.1.5)
dotenv (3.1.7)
drb (2.2.1)
ed25519 (1.3.0)
erubi (1.13.0)
erubi (1.13.1)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
io-console (0.8.0)
irb (1.14.2)
irb (1.14.3)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
json (2.9.0)
json (2.9.1)
language_server-protocol (3.17.0.3)
logger (1.6.3)
loofah (2.23.1)
logger (1.6.5)
loofah (2.24.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
minitest (5.25.4)
Expand All @@ -84,25 +82,26 @@ GEM
net-sftp (4.0.0)
net-ssh (>= 5.0.0, < 8.0.0)
net-ssh (7.3.0)
nokogiri (1.17.2-arm64-darwin)
nokogiri (1.18.1-arm64-darwin)
racc (~> 1.4)
nokogiri (1.17.2-x86_64-darwin)
nokogiri (1.18.1-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.17.2-x86_64-linux)
nokogiri (1.18.1-x86_64-linux-gnu)
racc (~> 1.4)
ostruct (0.6.1)
parallel (1.26.3)
parser (3.3.6.0)
ast (~> 2.4.1)
racc
psych (5.2.1)
psych (5.2.2)
date
stringio
racc (1.8.1)
rack (3.1.8)
rack-session (2.0.0)
rack-session (2.1.0)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.1.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.2.1)
rack (>= 3)
Expand All @@ -113,22 +112,22 @@ GEM
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
railties (8.0.0.1)
actionpack (= 8.0.0.1)
activesupport (= 8.0.0.1)
railties (8.0.1)
actionpack (= 8.0.1)
activesupport (= 8.0.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rdoc (6.8.1)
rdoc (6.10.0)
psych (>= 4.0.0)
regexp_parser (2.9.3)
reline (0.5.12)
regexp_parser (2.10.0)
reline (0.6.0)
io-console (~> 0.5)
rubocop (1.69.2)
rubocop (1.70.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
Expand All @@ -138,15 +137,15 @@ GEM
rubocop-ast (>= 1.36.2, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.36.2)
rubocop-ast (1.37.0)
parser (>= 3.3.1.0)
rubocop-minitest (0.36.0)
rubocop (>= 1.61, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-performance (1.23.0)
rubocop-performance (1.23.1)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.27.0)
rubocop-rails (2.28.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0)
Expand All @@ -158,7 +157,7 @@ GEM
rubocop-rails
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
securerandom (0.4.0)
securerandom (0.4.1)
sshkit (1.23.2)
base64
net-scp (>= 1.1.2)
Expand All @@ -169,7 +168,7 @@ GEM
thor (1.3.2)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.2)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.2)
Expand All @@ -189,4 +188,4 @@ DEPENDENCIES
rubocop-rails-omakase

BUNDLED WITH
2.4.3
2.6.2
18 changes: 10 additions & 8 deletions lib/kamal/cli/build.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,16 @@ def push

desc "pull", "Pull app image from registry onto servers"
def pull
if (first_hosts = mirror_hosts).any?
#  Pull on a single host per mirror first to seed them
say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
pull_on_hosts(first_hosts)
say "Pulling image on remaining hosts...", :magenta
pull_on_hosts(KAMAL.hosts - first_hosts)
else
pull_on_hosts(KAMAL.hosts)
Kamal::Cli::PortForwarding.new(KAMAL.hosts, KAMAL.config.registry.local_port).forward do
if (first_hosts = mirror_hosts).any?
#  Pull on a single host per mirror first to seed them
say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
pull_on_hosts(first_hosts)
say "Pulling image on remaining hosts...", :magenta
pull_on_hosts(KAMAL.hosts - first_hosts)
else
pull_on_hosts(KAMAL.hosts)
end
end
end

Expand Down
4 changes: 2 additions & 2 deletions lib/kamal/cli/main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def deploy
invoke_options = deploy_options

say "Log into image registry...", :magenta
invoke "kamal:cli:registry:login", [], invoke_options.merge(skip_local: options[:skip_push])
invoke "kamal:cli:registry:setup", [], invoke_options.merge(skip_local: options[:skip_push])

if options[:skip_push]
say "Pull app image...", :magenta
Expand Down Expand Up @@ -184,7 +184,7 @@ def remove
invoke "kamal:cli:app:remove", [], options.without(:confirmed)
invoke "kamal:cli:proxy:remove", [], options.without(:confirmed)
invoke "kamal:cli:accessory:remove", [ "all" ], options
invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true)
invoke "kamal:cli:registry:remove", [], options.without(:confirmed).merge(skip_local: true)
end
end
end
Expand Down
44 changes: 44 additions & 0 deletions lib/kamal/cli/port_forwarding.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
class Kamal::Cli::PortForwarding
attr_reader :hosts, :port

def initialize(hosts, port)
@hosts = hosts
@port = port
end

def forward
if KAMAL.config.registry.local?
@done = false
forward_ports
end

yield
ensure
stop
end

private

def stop
@done = true
@threads.to_a.each(&:join)
end

def forward_ports
@threads = hosts.map do |host|
Thread.new do
Net::SSH.start(host, KAMAL.config.ssh.user) do |ssh|
ssh.forward.remote(port, "localhost", port, "localhost")
ssh.loop(0.1) do
if @done
ssh.forward.cancel_remote(port, "localhost")
break
else
true
end
end
end
end
end
end
end
24 changes: 16 additions & 8 deletions lib/kamal/cli/registry.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
class Kamal::Cli::Registry < Kamal::Cli::Base
desc "login", "Log in to registry locally and remotely"
desc "setup", "Setup local registry or log in to remote registry locally and remotely"
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
def login
run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
def setup
if KAMAL.registry.local?
run_locally { execute *KAMAL.registry.setup } unless options[:skip_local]
else
run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
end
end

desc "logout", "Log out of registry locally and remotely"
desc "remove", "Remove local registry or log out of remote registry locally and remotely"
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
def logout
run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
def remove
if KAMAL.registry.local?
run_locally { execute *KAMAL.registry.remove, raise_on_non_zero_exit: false } unless options[:skip_local]
else
run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
end
end
end
17 changes: 15 additions & 2 deletions lib/kamal/commands/builder/local.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
def create
docker :buildx, :create, "--name", builder_name, "--driver=#{driver}" unless docker_driver?
return if docker_driver?

options =
if KAMAL.registry.local?
"--driver=#{driver} --driver-opt network=host"
else
"--driver=#{driver}"
end

docker :buildx, :create, "--name", builder_name, options
end

def remove
Expand All @@ -9,6 +18,10 @@ def remove

private
def builder_name
"kamal-local-#{driver}"
if KAMAL.registry.local?
"kamal-local-registry-#{driver}"
else
"kamal-local-#{driver}"
end
end
end
17 changes: 17 additions & 0 deletions lib/kamal/commands/registry.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
class Kamal::Commands::Registry < Kamal::Commands::Base
delegate :registry, to: :config
delegate :local?, :local_port, to: :registry

def login
return if local?

docker :login,
registry.server,
"-u", sensitive(Kamal::Utils.escape_shell_value(registry.username)),
Expand All @@ -11,4 +14,18 @@ def login
def logout
docker :logout, registry.server
end

def setup
combine \
docker(:start, "kamal-docker-registry"),
docker(:run, "--detach", "-p", "127.0.0.1:#{local_port}:5000", "--name", "kamal-docker-registry", "registry:2"),
by: "||"
end

def remove
combine \
docker(:stop, "kamal-docker-registry"),
docker(:rm, "kamal-docker-registry"),
by: "&&"
end
end
8 changes: 8 additions & 0 deletions lib/kamal/configuration/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ def password
lookup("password")
end

def local?
server&.match?("^localhost[:$]")
end

def local_port
local? ? (server.split(":").last.to_i || 80) : nil
end

private
def lookup(key)
if registry_config[key].is_a?(Array)
Expand Down
Loading

0 comments on commit f24e74c

Please sign in to comment.