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.