Hatchbox Manages Env Vars with asdf

Hatchbox handles a bunch of the details for setting up, running, and deploying a Rails app on an Ubuntu server on a number of infrastructure providers. Managing environment variables is one of the details they handle for you. Environment variables like DATABASE_URL, RAILS_MASTER_KEY, etc. can be set, updated, and deleted via the Hatchbox UI.

But how does a value set in the Hatchbox UI make its way into my app's environment when I deploy? Let's take a look.

I have a Hetzner server managed via Hatchbox. When I SSH into that server (I added my public SSH key via the Hatchbox UI already) and inspect the environment, I don't see any of the values I set in the Env Var UI.

$ echo $DATABASE_URL

$ env | sort
ASDF_DIR=/home/deploy/.asdf
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
HOME=/home/deploy
LANG=en_US.UTF-8
PATH=/home/deploy/.asdf/shims:/home/deploy/.asdf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
PWD=/home/deploy
SHELL=/bin/bash
USER=deploy
_=/usr/bin/env

When I connect to the Rails console though, I do see those env vars:

> ENV.each { |key, value| puts "#{key}: #{value}" }
=>
{"ASDF_CONFIG_FILE"=>"/home/deploy/.asdfrc",
 "ASDF_DATA_DIR"=>"/home/deploy/.asdf",
 "ASDF_DEFAULT_TOOL_VERSIONS_FILENAME"=>".tool-versions",
 "ASDF_DIR"=>"/home/deploy/.asdf",
 ...
 "DATABASE_URL"=>"postgresql://user_123:[email protected]/my_app_db",
 ...
}

So, something is making those env vars available to the Rails environment even though I'm not seeing them directly in the env as the deploy user.

There are a number of ways that env vars might get set. They could get set via dotenv with .env* files. They could get set via direnv and .envrc files. They could be managed via a systemd unit file. None of that is what is going on with this server.

Some careful googling turned up a help article answered by Chris Oliver which gives us just the hint we need. Chris points out that you can load all your app's environment variables with asdf like so:

$ cd ~/my-app/current
$ eval "$(/home/deploy/.asdf/bin/asdf vars)"

I've used asdf to manage Ruby and Node versions. It turns out you can manage env vars as well. This functionality comes from Chris's asdf-vars plugin for asdf. That plugin looks for any .asdf-vars files along the current chain of directories and loads those.

As it turns out, we have a number of those files that Hatchbox has added throughout our server.

$ find . -name ".asdf-vars" -type f
./.asdf-vars
./my-app/.asdf-vars
./my-app/releases/20250120195106/.asdf-vars
./my-app/releases/20250121041054/.asdf-vars
./my-app/releases/20250120180854/.asdf-vars
./my-app/releases/20250120181559/.asdf-vars
./my-app/releases/20250120193623/.asdf-vars

Looking at the second one, in my app, but above releases, I find all those env vars that I set in the Hatchbox UI:

$ cat my-app/.asdf-vars
BUNDLE_WITHOUT=development:test
DATABASE_URL=postgresql://user_123:[email protected]/my_app_db
PORT=9000
RACK_ENV=production
RAILS_ENV=production
RAILS_LOG_TO_STDOUT=true
RAILS_MASTER_KEY=abc123
SECRET_KEY_BASE=abc123efg456

Looking in the server logs for a Hatchbox deploy, I see the following bit of setup which includes a couple lines about Uploading .asdf-vars:

-----> Connecting to my-app (1.2.3.4 port 22) as deploy
-----> Retrieving latest commit for `main` branch
-----> Creating release 20250121041054
-----> git remote update --prune
From https://github.com/jbranchaud/my-app
 - [deleted]         (none)            -> dependabot/bundler/tailwindcss-rails-3.3.0
 - [deleted]         (none)            -> refs/pull/22/merge
   d3ec8a9..31f38cc  main              -> main
 * [new ref]         refs/pull/27/head -> refs/pull/27/head
 * [new ref]         refs/pull/28/head -> refs/pull/28/head
-----> git archive 31f38ccc3705b63f37425a6c9d2c56987676f923 | tar -x -f - -C /home/deploy/my-app/releases/20250121041054
-----> ln -s ../../shared/log log
-----> ln -s ../../shared/tmp tmp
-----> ln -s ../../shared/node_modules node_modules
-----> ln -s ../../shared/storage storage
-----> ln -s ../../../shared/public/system /home/deploy/my-app/releases/20250121041054/public/system
-----> ln -s ../../../shared/public/uploads /home/deploy/my-app/releases/20250121041054/public/uploads
-----> Uploading REVISION
-----> Uploading .asdf-vars
-----> Uploading .asdf-vars
-----> /home/deploy/.asdf/bin/asdf update --head

We haven't quite closed the loop though. How do the env vars defined in these .asdf-vars files make their way into the Ruby processes for serving our app's web server, running migrations, connecting to the console, etc.?

The Install instructions in the README for asdf-vars gives us another hint. It says to update the ~/.asdf/lib/commands/command-exec.bash file with the following lines:

eval "$($ASDF_DIR/bin/asdf vars)"
with_shim_executable "$shim_name" exec_shim || exit $?

That file exists on my Hatchbox server. I opened it up and searched for vars and found within the exec_shim() function this line:

eval "$(asdf vars)" # asdf_allow: eval

Anytime an asdf shim is executed, these env vars get loaded into the environment of that process.

$ cd my-app/current
$ which ruby
/home/deploy/.asdf/shims/ruby
$ ruby -e "puts ENV['DATABASE_URL']"
postgresql://user_123:[email protected]/my_app_db

That's an example of how the shimmed Ruby version is clearly getting the env vars from .asdf-vars.

These details aren't something we strictly need to know when working with Hatchbox. Nevertheless I often find it helpful to take a look under the hood, especially if I need to do some debugging at some point.

Tell us about your project

We build good software through good partnerships. Reach out and we can discuss your business, your goals, and how VisualMode can help.