Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@nickhammond
Copy link
Contributor

@nickhammond nickhammond commented Aug 28, 2024

This PR introduces Cloud Native Buildpacks to the list of builder options for Kamal.

This opens up the option to utilize buildpacks instead of writing a Dockerfile from scratch for each app that you want to deploy with Kamal. The end result is still an OCI-compliant docker image that you can run the same as the currently built docker images with Kamal.

You can also use any buildpacks or builders that you'd like so if you prefer some of the Paketo buildpacks instead you can use those too. The example below is utilizing Heroku's builder with the ruby and procfile buildpack which gives you the familiar Heroku build process when you deploy your application. Auto-detection of bundler, cache management of gem and asset installation, and various other features that come along with those.

With this PR you'd need to have pack installed as well as Docker and then within your deploy.yml change your builder to:

builder:
  arch: amd64
  pack:
    builder: heroku/builder:24
    buildpacks:
    - heroku/ruby
    - heroku/procfile

The default process that the buildpack tries to boot is the web process, you can add a Procfile for this:

web: ./bin/docker-entrypoint ./bin/rails server

And lastly, buildpacks don't bind to a default port so you'll either need to set proxy.app_port(Kamal 2.0 / kamal-proxy) to your application port or set your app to use port 80 which is the default kamal-proxy port.

Option 1 (Assuming your app uses port 3000):

proxy:
  app_port: 3000

Option 2 (Rails app running Puma that supports setting $PORT):

env:
  clear:
    PORT: 80
servers:
  web:
    hosts:
      - 123.456.78.9

Buildpacks work in a detect and then build flow. The detect step looks for common files or checks that indicate it is indeed a ruby application by looking for a Gemfile.lock for instance. If the detect step passes then it triggers the build phase which essentially triggers a bundle install in this example.

With heroku/builder:24 so far I've found that the image size is about the same, it's only 2mb off for a 235mb image. Build time is typically faster with pack but depends on how well you've optimized your Dockerfile. The win though is not having to think about how to cache your gem installs, node installs or any other package manager installs that have a buildpack. It's also following the common conventions for building containers and various stumbling blocks that Heroku and others have been blazing through over the years.

Kamal discussion: #795
Heroku discussion: heroku/buildpacks#6
Heroku official buildpacks: https://devcenter.heroku.com/articles/buildpacks
Heroku 3rd party buildpacks: https://elements.heroku.com/buildpacks
Full setup overview: https://www.fromthekeyboard.com/deploying-a-rails-app-with-kamal-heroku-style/

Todos:

  • Companion PR for kamal-site Started in Add a builder example section about buildpacks kamal-site#117
  • Excluded files mention via project.toml
  • Catch up with main
  • Discuss potential for a remote pack option Out of scope
  • Test pack with the git archive context
  • What should kamal build create do when using buildpacks? Just point to the install docs? https://buildpacks.io/docs/for-platform-operators/how-to/integrate-ci/pack/ - Since you don't typically call kamal build create and it's instead called within a build I'm going to close this one out.
  • Update kamal build details to run pack version && pack builder inspect
  • Does kamal build remove need to do anything? Since we're not creating a build context like you normally would with Docker there's nothing to actually remove. Is there anything in the Kamal lifecycle though that we at least need a no-op method for this?

Demo applications:

buildpacks,
"-t", config.absolute_image,
"-t", config.latest_image,
"--env", "BP_IMAGE_LABELS=service=#{config.service}",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kamal expects there to be a service label, this automatically adds the label via the paketo-buildpacks/image-labels buildpack.

end

def buildpacks
(pack_buildpacks << "paketo-buildpacks/image-labels").map { |buildpack| ["--buildpack", buildpack] }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this buildpack automatically so that we can label the image for Kamal


# Buildpack configuration
#
# The build configuration for using pack to build a Cloud Native Buildpack image.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add mention of project.toml to set your excluded options. https://buildpacks.io/docs/for-app-developers/how-to/build-inputs/use-project-toml/

Copy link
Contributor Author

@nickhammond nickhammond Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I was thinking about this and removing context: "." it doesn't matter as much since it's using the git clone. The exclusion list is really only relevant when you're using "." as your build context.

@nickhammond nickhammond changed the title Add a pack option to the builder options for cloud native buildpacks Add pack option to the builder options for cloud native buildpacks Aug 28, 2024
@nickhammond nickhammond marked this pull request as ready for review August 28, 2024 06:02
@alexohre
Copy link

alexohre commented Sep 3, 2024

hey @nickhammond and @dhh, does this change resolve the custom build issue like using the builder of choice. e.g. docker build cloud?

@nickhammond
Copy link
Contributor Author

Hey @alexohre - No, that'll just be a remote builder with the engine pointing to Docker cloud(#914) which would be a different PR. The builders were just reorganized a bit as well so it might be simpler to add an additional option for a remote cloud builder in a different PR.

@alexohre
Copy link

alexohre commented Sep 4, 2024

Hey @alexohre - No, that'll just be a remote builder with the engine pointing to Docker cloud(#914) which would be a different PR. The builders were just reorganized a bit as well so it might be simpler to add an additional option for a remote cloud builder in a different PR.

Oh, thanks for the awareness. I would be glad if you could help me make a PR for that since I don't know how to build gems or modify it for now

@dhh
Copy link
Member

dhh commented Sep 22, 2024

This is fascinating work, @nickhammond. I'm surprised by how unobtrusive it is! But I'd like to understand the whole flow better. I'm not sure this is going to be all that relevant for Rails apps that now already come with well-optimized Dockerfiles out of the box, but I could see how that may well be different if you're doing a Sinatra app or some app from another framework that doesn't provide that.

Could you show how the entire flow would go with, say, a Sinatra app, using buildpacks, and deploying on something like Digital Ocean? Want to make sure that this isn't tied to any one company or platform.

@nickhammond
Copy link
Contributor Author

Hey @dhh, thanks for taking a look!

I think adding support for buildpacks will be great for the adoption of Kamal but you can always still reach for the sharper tool(a full Dockerfile) when needed.

I built out a few hello world examples, the main thing is just making sure your app boots on port 80 for kamal-proxy or just ensuring that you set your app_port if it doesn't. This isn't a buildpack-specific thing but more of a change that came with kamal-proxy, packs don't export a port by default.

Here are the hello world apps that I built and tested out on Digital Ocean and wrote a more detailed overview for the whole process as well.

@hone
Copy link

hone commented Sep 25, 2024

@nickhammond thanks for all your investigations and opening this PR. ❤️

Want to make sure that this isn't tied to any one company or platform.

@dhh 👋 It's been a while. As Cloud Native Buildpacks (CNB) maintainer I'm biased and would love to see this supported in kamal. :)

Nick touches on this in his blog, but if it's any assurance CNB as an upstream project is a CNCF Incubation project which pushes for not being a single vendor OSS project. In fact, the project was started from the get go by two companies, Heroku & Pivotal. It's really about bringing that Heroku magic to container image building, transforming your app source code into an OCI image (No Dockerfile needed). You can push image to a registry, docker run it locally, or even use it as a base image in the FROM directive of a Dockerfile. If you don't want to use the Heroku builder and buildpacks, there`s the Paketo ones, and you can also write your own.

@nickhammond
Copy link
Contributor Author

Started on the docs in this kamal-site PR basecamp/kamal-site#117.

Copy link
Collaborator

@djmb djmb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nickhammond - I noticed in your sample apps, that you've set the context for the builder to ., which avoids using the git clone for building.

Is that just a preference or is there any reason it would be required?


def inspect_builder
docker :buildx, :inspect, builder_name unless docker_driver?
docker :buildx, :inspect, builder_name unless docker_driver? || pack?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could extract a buildx? method here?

def buildx?
  !docker_driver? && !pack?
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@djmb We could also run pack builder inspect which returns a bunch of information about the default builder. It's a lot of information but might be useful to help triage if you're not sure what builder you're using. The Pack CLI lets you set your default builder so I have mine set to heroku/builder:24 via pack config default-builder heroku/builder:24

Copy link

@alexohre alexohre Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nickhammond Does this mean I can now pass my builder name to kamal?

Buildx cloud_builder

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexohre No, I don't think there's a PR open for that, just the discussion here #914 (comment)

"-t", config.absolute_image,
"-t", config.latest_image,
"--env", "BP_IMAGE_LABELS=service=#{config.service}",
*argumentize("--env", secrets, sensitive: true),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is using environment variables the standard way to get secrets into a buildpack?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@djmb Yes, they only have the --env flag.

I just tested building with a few secrets because I was concerned they'd end up in the final image but they don't.

I just found this in the docs site though. TLDR; It's just a build-time env var, they're not available at image runtime. So they're naturally "secret", neat.

https://buildpacks.io/docs/for-platform-operators/how-to/integrate-ci/pack/cli/pack_build/#options

  -e, --env stringArray              Build-time environment variable, in the form 'VAR=VALUE' or 'VAR'.
                                     When using latter value-less form, value will be taken from current
                                       environment at the time this command is executed.
                                     This flag may be specified multiple times and will override
                                       individual values defined by --env-file.
                                     Repeat for each env in order (comma-separated lists not accepted)
                                     NOTE: These are NOT available at image runtime.

@nickhammond
Copy link
Contributor Author

@edmorley Adding the --timestamps option helped. It's not with every pack but when it does happen it looks like it's when attempting to create the registry cache, here's an example with ~14 seconds. Most of the time it's 5 seconds it looks like though, I think either way it's fine but just trying to keep the pack time low. It looks like I don't have the containerd setting enabled locally too just FYI.

2024/11/26 07:55:05.265098 Creating registry cache for github.com//buildpacks/registry-index
2024/11/26 07:55:19.678694 Pulling image docker.io/paketobuildpacks/image-labels@sha256:1fd7d8f00f15ec404c616f3671aec8019f9a5103eac8e3a8aaf0f2e8d4bb883d with platform linux/amd64
pack CLI run
pack build app --platform linux/amd64 --builder heroku/builder:24 --buildpack heroku/deb-packages --buildpack heroku/ruby --buildpack heroku/procfile --buildpack paketo-buildpacks/image-labels -t app/hey:2bb2b2f7d8b40aa287b65698de0efca208f716d9 -t app/hey:latest-production --env BP_IMAGE_LABELS=service=app --path . --timestamps --verbose
2024/11/26 07:55:02.134940 Using project descriptor located at project.toml
2024/11/26 07:55:02.134959 Builder heroku/builder:24 is trusted
2024/11/26 07:55:02.135017 Pulling image index.docker.io/heroku/builder:24 with platform linux/amd64
2024/11/26 07:55:03.092787 24: 2024/11/26 07:55:03.092834 Pulling from heroku/builder
2024/11/26 07:55:03.092869 2024/11/26 07:55:03.101072 Digest: sha256:2324afe304202e81d452bb203eb4edcc7fed682840d0ec3c82f11fdba96cc199
2024/11/26 07:55:03.101126 Status: Image is up to date for heroku/builder:24
2024/11/26 07:55:03.849599 CheckReadAccess succeeded for the run image docker.io/heroku/heroku:24
2024/11/26 07:55:04.319850 CheckReadAccess succeeded for the run image public.ecr.aws/heroku/heroku:24
2024/11/26 07:55:04.319954 Selected run image docker.io/heroku/heroku:24
2024/11/26 07:55:04.319983 Pulling image docker.io/heroku/heroku:24 with platform linux/amd64
2024/11/26 07:55:05.183422 24: 2024/11/26 07:55:05.183475 Pulling from heroku/heroku
2024/11/26 07:55:05.183492 2024/11/26 07:55:05.189156 Digest: sha256:613aa12fc84c16054be6a9501578e06948d76363e2921e4326447fd0e5a770cf
2024/11/26 07:55:05.189195 Status: Image is up to date for heroku/heroku:24
2024/11/26 07:55:05.202342 Downloading buildpack from registry: paketo-buildpacks/image-labels
2024/11/26 07:55:05.202488 Refreshing registry cache for github.com//buildpacks/registry-index
2024/11/26 07:55:05.202501 Validating registry cache for github.com//buildpacks/registry-index
2024/11/26 07:55:05.265098 Creating registry cache for github.com//buildpacks/registry-index
2024/11/26 07:55:19.678694 Pulling image docker.io/paketobuildpacks/image-labels@sha256:1fd7d8f00f15ec404c616f3671aec8019f9a5103eac8e3a8aaf0f2e8d4bb883d with platform linux/amd64

This branch has been updated with the latest from main as well. I'm currently using this branch to deploy 2 apps that I'm actively working on.

@edmorley
Copy link

@nickhammond
Copy link
Contributor Author

I've updated this with the latest from main. I've been using this branch to deploy a few projects for a few months now and haven't had any issues. Let me know if I can tweak or update anything.

@nickhammond
Copy link
Contributor Author

The test matrix run for Ruby 3.4.0-preview2 with the current Gemfile is failing for some reason, it's working fine with the same version and the Rails edge gemfile. It's passing for me locally with that test, I'll try to revisit it this week at some point.

Test failure output
Error:
AppTest#test_stop,_start,_boot,_logs,_images,_containers,_exec,_remove:
RuntimeError: Container not healthy after 30 seconds
    test/integration/integration_test.rb:124:in 'IntegrationTest#wait_for_healthy'
    test/integration/integration_test.rb:8:in 'block in <class:IntegrationTest>'
    vendor/bundle/ruby/3.4.0+0/gems/activesupport-8.0.1/lib/active_support/callbacks.rb:406:in 'BasicObject#instance_exec'
    vendor/bundle/ruby/3.4.0+0/gems/activesupport-8.0.1/lib/active_support/callbacks.rb:406:in 'block in ActiveSupport::Callbacks::CallTemplate::InstanceExec0#make_lambda'
    vendor/bundle/ruby/3.4.0+0/gems/activesupport-8.0.1/lib/active_support/callbacks.rb:178:in 'block in ActiveSupport::Callbacks::Filters::Before#call'
    vendor/bundle/ruby/3.4.0+0/gems/activesupport-8.0.1/lib/active_support/callbacks.rb:667:in 'block (2 levels) in ActiveSupport::Callbacks::CallbackChain#default_terminator'
    vendor/bundle/ruby/3.4.0+0/gems/activesupport-8.0.1/lib/active_support/callbacks.rb:666:in 'Kernel#catch'
    vendor/bundle/ruby/3.4.0+0/gems/activesupport-8.0.1/lib/active_support/callbacks.rb:666:in 'block in ActiveSupport::Callbacks::CallbackChain#default_terminator'
    vendor/bundle/ruby/3.4.0+0/gems/activesupport-8.0.1/lib/active_support/callbacks.rb:179:in 'ActiveSupport::Callbacks::Filters::Before#call'
    vendor/bundle/ruby/3.4.0+0/gems/activesupport-8.0.1/lib/active_support/callbacks.rb:558:in 'block in ActiveSupport::Callbacks::CallbackSequence#invoke_before'
    <internal:array>:42:in 'Array#each'
    vendor/bundle/ruby/3.4.0+0/gems/activesupport-8.0.1/lib/active_support/callbacks.rb:558:in 'ActiveSupport::Callbacks::CallbackSequence#invoke_before'
    vendor/bundle/ruby/3.4.0+0/gems/activesupport-8.0.1/lib/active_support/callbacks.rb:108:in 'ActiveSupport::Callbacks#run_callbacks'
    vendor/bundle/ruby/3.4.0+0/gems/activesupport-8.0.1/lib/active_support/testing/setup_and_teardown.rb:41:in 'ActiveSupport::Testing::SetupAndTeardown#before_setup'

bin/test /home/runner/work/kamal/kamal/test/integration/app_test.rb:4

#<Thread:0x00007fa93d08efb0 /home/runner/work/kamal/kamal/vendor/bundle/ruby/3.4.0+0/gems/sshkit-1.23.2/lib/sshkit/backends/connection_pool.rb:52 run> terminated with exception (report_on_exception is true):
/home/runner/work/kamal/kamal/vendor/bundle/ruby/3.4.0+0/gems/sshkit-1.23.2/lib/sshkit/backends/connection_pool.rb:140:in 'block in SSHKit::Backend::ConnectionPool#run_eviction_loop': unexpected invocation: #<AnyInstance:Object>.sleep(5) (Minitest::Assertion)
	from <internal:kernel>:168:in 'Kernel#loop'
	from /home/runner/work/kamal/kamal/vendor/bundle/ruby/3.4.0+0/gems/sshkit-1.23.2/lib/sshkit/backends/connection_pool.rb:136:in 'SSHKit::Backend::ConnectionPool#run_eviction_loop'
	from /home/runner/work/kamal/kamal/vendor/bundle/ruby/3.4.0+0/gems/sshkit-1.23.2/lib/sshkit/backends/connection_pool.rb:52:in 'block in SSHKit::Backend::ConnectionPool#initialize'

@djmb
Copy link
Collaborator

djmb commented Feb 6, 2025

Hey @nickhammond - I'm keen to get this into Kamal - I'll be away next week, but when I'm back I'll do a final review. Thanks for your patience!

end

def export(export_action)
return unless export_action == "registry"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the output option changes from #1357, support needed to be added for an export_action but it's a little different than docker buildx output behavior. I went with supporting a build and push behavior as the default and then if you specify anything other than registry it just doesn't push the image to the registry.

I don't believe there's additional options for the output format since pack build is just outputting the OCI image but @edmorley might have some ideas here.

Docker buildx output options: https://docs.docker.com/reference/cli/docker/buildx/build/#output

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! I'm not sure I quite follow the question (I've only skimmed the comments via email notifications) - but in case it helps, this is the only Pack CLI option relating to how the generated asset is handled:

      --publish                      Publish the application image directly to the container registry specified in <image-name>, instead of the daemon. The run image must also reside in the registry.

@nickhammond
Copy link
Contributor Author

@djmb No worries, enjoy your time off next week!

Ideally buildpacks/pack#2268 that @edmorley opened gets merged in so we don't have to inject the image-labels pack but that looks like it's still a WIP.

@nickhammond
Copy link
Contributor Author

The Paketo buildpack CLI also recommends adding labels via the image labels buildpack so we might just want to leave the labelling as-is.

https://paketo.io/docs/howto/configuration/#applying-custom-labels

@nickhammond
Copy link
Contributor Author

@djmb Are there any tests or verification steps that I can add to make this easier for you to review? I'm using this branch to deploy 5 different apps now, a few weekly and the others at least once a month.

@djmb
Copy link
Collaborator

djmb commented May 26, 2025

Hey @nickhammond - no nothing needed from me. I'm happy to include this in Kamal 2.7 when that is ready to go out

@nickhammond
Copy link
Contributor Author

@djmb Sounds good, I'll take a look at the kamal-site docs PR again as well this week.

@nickhammond
Copy link
Contributor Author

@djmb I updated all of the demo apps and redeployed with the latest using this branch, all went well. The build context was removed "." and there's no longer a need for a project.toml so I removed that from the demo apps.

Let me know if I can provide insight on anything else, excited to get people using this!

@djmb djmb merged commit 054a85d into basecamp:main Jun 16, 2025
7 checks passed
@gregjotau
Copy link

@nickhammond tried this for a spring boot app:

builder:
  pack:
    # This is the official Paketo builder based on Ubuntu Jammy.
    # It includes buildpacks for Java, Maven, Gradle, and more.
    builder: paketobuildpacks/builder-jammy-base

Got this error:

BUILD SUCCESSFUL in 517ms
5 actionable tasks: 5 up-to-date
Build and push app image...
  INFO [89be1e2e] Running docker --version && docker buildx version as user@localhost
  INFO [89be1e2e] Finished in 0.190 seconds with exit status 0 (successful).
  INFO [e2e8e934] Running docker login -u [REDACTED] -p [REDACTED] as user@localhost
  INFO [e2e8e934] Finished in 2.561 seconds with exit status 0 (successful).
Building with uncommitted changes:
 M config/deploy.yml
  INFO [bce6fcd5] Running /usr/bin/env pack builder inspect paketobuildpacks/builder-jammy-base as user@localhost
  INFO [bce6fcd5] Finished in 2.398 seconds with exit status 0 (successful).
  Finished all in 5.2 seconds
  ERROR (NoMethodError): undefined method `<<' for nil

Using Kamal 2.7.0

If you had insight into how to get this working for a Spring boot app that would be great :)

@nickhammond
Copy link
Contributor Author

@gregjotau It looks like you've defined a builder but you also need a buildpack. I haven't built a spring app but this looks like the one mentioned in the Spring docs(https://docs.spring.io/spring-boot/reference/packaging/container-images/cloud-native-buildpacks.html)

https://github.com/paketo-buildpacks/spring-boot

builder:
  pack:
    # This is the official Paketo builder based on Ubuntu Jammy.
    # It includes buildpacks for Java, Maven, Gradle, and more.
    builder: paketobuildpacks/builder-jammy-base
    buildpacks:
      - paketo-buildpacks/spring-boot

@nickhammond nickhammond deleted the buildpacks branch June 18, 2025 14:45
@gregjotau
Copy link

@nickhammond never got it to work, so started a separate dicussion instead: #1637

Thanks for the feature! Would be great if we managed to use it 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants