Capistrano: run Grunt locally and upload files

So is there a way to run grunt build on the remote server in some way?

I notice Scott has this gem: GitHub - roots/capistrano-grunt: Capistrano extension for Grunt tasks

Or do we run grunt build locally and then scp these compiled assets to the remote server using a Capistrano task?

I’m a bit confused as to what’s the best way.

Any advice would be appreciated.

1 Like

Not to confuse you further, but it’s really up to you. There’s benefits to each method and it mostly comes down to whether or not you want to installed Node and Grunt on your remote servers. I’d lean towards compiling locally since you already have Grunt setup but unfortunately Capistrano isn’t set up by default to do things locally then copy.

Thanks, @swalkinshaw.

I’ve gone with the build locally and copy approach.

This is how I’m achieving it – any improvements most welcome:

    set :rel_path_to_theme, "web/app/themes/mytheme"
    set :local_path_to_app, "/Applications/MAMP/htdocs/mysite/"
    set :local_path_to_theme, "#{fetch(:local_path_to_app) + fetch(:rel_path_to_theme)}"

    namespace :deploy do
      task :copy_compiled_assets do
        system("grunt --base #{fetch(:rel_path_to_theme)} --gruntfile #{fetch(:rel_path_to_theme)}/GruntFile.js build")
        on roles(:web) do
          %w{/assets/css/main.min.css /assets/js/scripts.min.js /assets/js/vendor/modernizr.min.js}.each do |asset|
            upload! "#{fetch(:local_path_to_theme) + asset}", "#{release_path.to_s + '/' + fetch(:rel_path_to_theme) + asset}"

I’ve modified this to be a bit cleaner and more Ruby/Cap idiomatic. The one major difference is ideally you should modify your Grunt production task to output the files you want to some separate dir like dist/ then you can just upload that whole file instead of individually listing files. It’s just easier not to duplicate some logic from Grunt in here.

Note: I haven’t actually tested this.

set :theme_path,'web/app/themes/mytheme')
set :local_app_path,'/Applications/MAMP/htdocs/mysite')
set :local_theme_path, fetch(:local_app_path).join(fetch(:theme_path))

namespace :deploy do
  task :compile_assets do
    run_locally do
      within fetch(:local_theme_path) do
        execute :grunt, :build

  task :copy_assets do
    invoke 'deploy:compile_assets'

    on roles(:web) do
      upload! fetch(:local_theme_path).join('dist'), release_path.join(fetch(:theme_path)), recursive: true
1 Like

Hey @swalkinshaw

This looks great. Thank you.

Having a slight issue though – whenever I deploy I keep getting an SSHKit::runner::ExecuteError.

It tells me that my local dist directory is a directory. Well, duh. It’s meant to be a directory.

Anyway, error as follows:

The deploy has failed with an error: #<SSHKit::Runner::ExecuteError: Exception while executing on host Is a directory - /Applications/MAMP/htdocs/myapp/web/app/themes/mytheme/dist

I’ve tried adding the folder to the repo (with .gitkeep) and subsequently removing it to see if that helped. But no dice.

Also, this:

suggests adding recursive: true, but that doesn’t work for me either.

Any ideas? I’m sure it must be something simple.


Not really sure unfortunately :(… never used this feature myself. I did update my code to include recursive: true since directories would need that.

edit: most likely this:

Meaning the destination release_path.join(fetch(:theme_path)) doesn’t exist on the remote host. I’d verify that’s all working.

Okay, so the fix I’ve found is to ensure the first pathname is a string.

The issue here – I think – is the local path fetch(:local_theme_path).join('dist') is still a Pathname object, whereas the upload! command expects the path as a string.

So this works:

upload! fetch(:local_theme_path).join('dist').to_s, release_path.join(fetch(:theme_path)), recursive: true

Hope that helps somebody else.

1 Like

And after adding a script to get those Bootstrap fonts up to the server, I’m currently using this:

Good catch. Odd thing is that release_path is a Pathname too and it’s working without to_s apparently.

I created a Gist inspired by christhesoul’s that doesn’t require setting up a dist directory. It just keeps a list of the generated assets in deploy.rb and uploads them.

It’s slightly less clean than just copying a single directory, but it doesn’t require mucking about updating paths in the theme, so it’s easier to get up and running.


Hi there,

thanks a lot for this

really helpful

I’m on the latest Bedrock/Sage version

2 questions if you have time

  • I had to comment the invoke deploy:compile_assets line - can someone explain what it’s supposed to do?

  • it took a bit of time to understand the ‘assets to remote’ process - I know it’s up to the user to choose his/her method but would not a basic default method help new users? like having the upload method task in the deploy.rb file by default (like there are other optional tasks)

thanks again


@thomasmery See my reply on Using Bedrock + Sage to deploy with Capistrano (theme Gulp dist files?)

The gist is that there it’s left over from another build and you should disregard. Though, you should check out the thread for more info.

assuming that your Cap root is in the same directory you could adjust the local_app_path
set :local_app_path, Dir.pwd its a bit more independent.