Skip to main content

Capsitrano Environment variables


This is the story of an epic fight. Me (regular guy) vs the server (Rails 3, deployed via Capistrano to a Passenger ā€“ Nginx hosted Ubuntu server, using Mandrill transactional mail service). not a fair fight.

If you already know the deal (or just donā€™t want to hear me whine) jump there for the Capistrano task thatā€™ll magically expand your environment variables in the config.

First if you follow Mandrillā€™s guide (spoiler: you should not), this is what youā€™d set up in your config/production.rb :

config.action_mailer.smtp_settings = {
  :address   => "smtp.mandrillapp.com",
  :port      => 587,
  :enable_starttls_auto => true,
  :user_name => "MANDRILL_USERNAME",
  :password  => "MANDRILL_API_KEY",
  :authentication => 'login'
}

I know your extra picky security cerebrum already picked up the issue with this. hardcoding the username and password in your config file cannot be a good idea, especially if youā€™re sharing your code with many people. A better way is to use environment variables.

:user_name => ENV["MANDRILL_USERNAME"],
:password  => ENV["MANDRILL_API_KEY"],

you then have to set these variables on your prod server, in /etc/profile for example :

export MANDRILL_USERNAME=foo
export MANDRILL_API_KEY=bar

Now, it canā€™t be that simple, right ? right. Whereas my mails were not sent, the logger did not give me any errors, so first things first, I figured Iā€™d turn the failed mail deliveries warning on in config/production.rb :

config.action_mailer.raise_delivery_errors = true

if youā€™re doing this live on your production server (and you should) you have to restart Passenger :

touch tmp/restart.txt

And I could now see a beautiful Net::SMTPServerBusy, Relay access denied error. First I believed it came from a blocked 587 port, got postfix to work, and everything worked fin, hmmm. Heck, it even worked when I launched a thin server on the prod ! This Stack Overflow entry showed me the way to the problem : Passenger doesnā€™t set your environment variables when you fire it up.

One solution is to load your variables in the wrapper around Ruby interpreter that Passenger uses (more info on this here). I donā€™t like this idea much, it feels very hacky, whatā€™ll happen when Iā€™ll upgrade my Ruby for example ?

My solution is to expand the variables in your config file during the deploy (meaning replacing ENV[ā€œMANDRILL_USERNAMEā€] with the actuall username value, copied from the environment variable). This amazing SO answer helped a lot, I tweaked it a bit to fit a Capistrano deploy task. It will expand ANY of your environment variables, how great is that ?!

Warning: this is some mad-ass syntax, even the built-in highlighter canā€™t figure it out. Thereā€™s a LOT of escaping, between sed syntax and rubyā€™s ā€¦

desc "Replace environment variables with hardcoded values in config file"
task :replace_env_vars, roles: :app do
  run "mv #{release_path}/config/environments/production.rb #{release_path}/config/environments/production.before_sed.rb"
  run 'env | sed \'s/[\%]/\\&/g;s/\([^=]*\)=\(.*\)/s%ENV\\\[\\\"\1\\\"\\\]%\"\2\"%/\' > ' + "#{release_path}/script/expand_env_vars.sed.script"
  run "cat #{release_path}/config/environments/production.before_sed.rb | sed -f #{release_path}/script/expand_env_vars.sed.script > #{release_path}/config/environments/production.rb"
end

after "deploy:update_code", "deploy:replace_env_vars"

Note : this will only replace strings that use double quotes like ENV[ā€œJESSICA_ALBA_PHONE_NUMBERā€] (I gather you wouldnā€™t share that piece of intel with any other developer)

Hey, lucky you ! Thereā€™s a last step, and itā€™s so easy it feels good. By default, the SSH session opened by Capistrano doesnā€™t execute your init scripts (~/.profile, ~/.bashrc, not even /etc/profile/). These are DanM instructions (cheers !) to load them up. Create ~/.ssh/environment and reput the export instructions, or you can even load your whole /etc/profile file, even though I have no idea what the security implications are. just donā€™t if you are ignorant like me. Tell your SSH server to load this file : add this line in /etc/ssh/sshd_config :

PermitUserEnvironment yes

and restart it with

sudo /etc/init.d/ssh restart

That should make your day. If it does not, feel free to let out your sorrow in the comments.