Rails on Docker: Build Secrets for Yarn with 1Password

Whilst moving an existing Rails project to Docker, I've been making use of multi-stage builds to optimise the image creation process. For this project, some of the npm/yarn packages were hosted in a private git repository. The package.json looked something like:

{
  "license": "UNLICENSED",
  "private": true,
  "dependencies": {
    // ...
    "package-a": "github:cblunt/package-a#main",
    "package-b": "github:cblunt/package-b#main1",
	// ...
  }

When building the image, the yarn install step attempts to download these packages, but requires the use of my host machine's private SSH key to authenticate and connect to the Github repositories.

Luckily, Docker has introduced the concept of build-secrets to handle just this situation. Build-secrets allow us to mount environment variables temporarily into the build process, discarding them afterwards so that they are not present in any of the image layers.

Furthermore, there is a special SSH build-secret that forwards the host's SSH agent into the image. In the Dockerfile, this can be achieved using the --mount=type=ssh argument:

RUN --mount=type=ssh mkdir -p ~/.ssh && \
	ssh-keyscan github.com > ~/.ssh/known_hosts && \
	yarn install && \
	rm -rf ~/.ssh

The docker image can then be built by passing adding your key to the local SSH agent, and forwarding it into the build (using the identifier default which defaults to the value of the SSH_AUTH_SOCK environment variable):

$ eval $(ssh-agent)
$ ssh-add ~/.ssh/id_ed25519 # path to your private SSH keyfile
# Identity added: ...

$ docker buildx build --ssh default .

Docker will now build the image, temporarily using the forwarded SSH credentials to connect to Github and download packages from private repositories.

1Password Managed Credentials

However, I use 1Password to manage my SSH keys and act as the SSH agent. This means that the private keyfiles (id_ed25519, id_rsa, etc.) are not saved into my home folder.

As 1Password runs as the SSH agent on my machine, instead of mounting the SSH_AUTH_SOCK, I needed to instead pass the socket file that 1Password creates, the location of which can be found in their documentation.

With this, we can skip adding the key to the default SSH agent, and instead make use of 1Password's agent:

$ docker buildx build --ssh default==$HOME/Library/Group\ Containers/2BUA8C4S2C.com.1password/t/agent.sock .

Docker will now build the image, using 1Password's SSH agent to forward the private key into the build process. This keeps the key securely stored in 1Password.

The same argument can also be passed when using Docker Compose:

$ docker compose build --ssh default=$HOME/Library/Group\ Containers/2BUA8C4S2C.com.1password/t/agent.sock

Passing Github Tokens via 1Password

Similarly, I use 1Password's gh plugin to manage my Github access token.

In the same way as SSH secrets can be mounted, Docker can also forward other secrets into the build process that are stored in environment variables.

For example, to pass your Github token into the build downloading gems from Github:

RUN --mount=type=secret,id=github_token BUNDLE_GITHUB__COM="x-access-token:$(cat /run/secrets/github_token)" \ 
    bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git

Note that the secrets are accessed like a text file, so to get their value in the Dockerfile, we use cat to output the value of the secret: cat /run/secrets/github_token.

To pass this value into Docker:

$ docker buildx build --secret id=github_token,env=GITHUB_TOKEN .

Using the 1Password, op command, these values can retrieved and passed into the build:

$ GITHUB_TOKEN=$(op read "op://Vault/Github Token Item Name/token") \
    docker buildx build --secret id=github_token,env=GITHUB_TOKEN .

Similarly, the token can be configured in Docker Compose:

services:
  # ...
  
secrets:
  github_token:
    environment: GITHUB_TOKEN

It can then be read when running docker compose build by setting the environment variable:

$ GITHUB_TOKEN=$(op read "op://Vault/Github Token Item Name/token") \
    docker compose build

Using this approach, it is possible to inject both SSH and token secrets managed by 1Password into your Docker build process, while maintaining the benefit of Docker's build secrets.

👋 Thanks for reading - I hope you enjoyed this post. If you find it helpful and want to support further writing and tutorials like this one, please consider supporting my work with a coffee!

Support ☕️