I recently started using Kamal to manage my Docker deployments and I’ve found it to be a really great tool. Here’s what my architecture looks like:

A few days ago Kamal released a 1.0 version which changed how it handles environment variables. Previously, Kamal would read a .env file and use that to populate the environment variables for the Docker container. I didn’t like this approach so I was using Ansible to generate ENV files on the server and have Kamal run the docker container with a custom --env-file argument.

With the new release, Kamal now takes the same approach, it generates an env file and pushes it to the server then runs the container with the --env-file argument. This is a much better approach and I’m glad to see it.

However, this still leaves the problem of where to store the secrets. To generate the env file Kamal supports using a Ruby ERB template file which is great because it allows you to run shell commands within the template.

Combing this with 1Password means you can extract secrets from 1Password and use them in your env file:

MY_SECRET_ENV_VAR=<%= `op read "op://Vault/Secret Env/password" -n` %>

This is a great solution and it documented in the Kamal docs but it does have a few drawbacks. Mainly, it’s a bit verbose and it can also be quite slow to retrieve all the secrets if you have a lot. Also, adding a new var requires updates in 3 places.

The solution I came up with was to store all the secrets in a single 1Password item and fetch them all at once with op get item instead of op read. This works because op get item returns YAML compatible output.

So now my template looks like this:

<% vars  = YAML.load(`op item get --vault "My Vault" "App Secrets" --fields type=string,type=concealed`) %>
<% for var in vars %>
  <% if  var.key?("Value") %>
<%= var['Label'] %>=<%= var['Value'] %>
  <% end %>
<% end  %>

Adding new fields to the 1Password item doesn’t require any changes to the template.