The best part of Kamal is that it makes deployment feel smaller. Less ceremony, less platform overhead and less "let’s book infra time next week.”
That is why secret handling deserves extra attention.
When deploys get easier, it becomes tempting to keep a handful of values floating around in shell sessions, CI settings, or copied snippets because everything still works. And for a while, it does.
If you are already deploying with Kamal, chances are your secrets travel through environment variables. Something like DB_PASSWORD=$DB_PASSWORD in .kamal/secrets is miles better than hardcoding credentials into a committed file. But it still leaves a quieter question behind: where does that value live before Kamal gets it, who can fetch it, and how much of your deploy path has to touch it?
This post will show you how to replace the loose flow with a safer, 1Password-backed setup, without turning deployment back into a DevOps side quest.

Before You Start
If you are new to Kamal, start with my video, 30 Minutes Deploy with Kamal – No DevOps Required. It covers the most relevant parts of Kamal, from the initial setup to having both production and staging environments.
Also note that Kamal is not tied to a single secret manager. Its documented secret adapters include 1Password, Bitwarden, Bitwarden Secrets Manager, LastPass, AWS Secrets Manager, GCP Secret Manager, Doppler, and Passbolt.
I am going to use 1Password in the examples because companies often choose it as their password manager. The shape of the solution is similar to the other tools, though: Kamal stays the deploy tool, the secret manager becomes the source of truth, and your deploy environment retrieves only what it needs.
One important exception before we go further: if you are storing real plaintext secrets directly in .kamal/secrets, stop doing that immediately. Rotate those secrets, clean up the file, and move back to environment-variable based handling before you follow anything else in this guide.
What Kamal Already Does Well
According to the official docs, environment variables can come either directly from your Kamal config or from .kamal/secrets. That file uses dotenv, supports variable substitution, supports command substitution, and can call secret helpers. Kamal also distinguishes between clear and secret values under env, and the subtle part here matters: secret values are not passed directly to docker run. Kamal stores them in an env file on the host.
That means the common baseline already looks something like this:
# .kamal/secrets
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
DB_PASSWORD=$DB_PASSWORD# config/deploy.yml
env:
clear:
APP_ENV: production
secret:
- KAMAL_REGISTRY_PASSWORD
- DB_PASSWORDIn fact, that is the right starting point for a lot of teams. The problem is that this pattern says nothing about where DB_PASSWORD came from or how long it sits in a shell. It does not tell you whether it came from a personal account, whether your CI runner can read far more than it should, or whether the same fetch path exists for staging, production, and local debugging. That is the real hardening gap.
Kamal also supports destination-specific secret files. If you deploy to both staging and production, you can split secrets across .kamal/secrets.staging and .kamal/secrets.production, with .kamal/secrets-common for values shared by both. That structure pairs well with the vault-per-environment approach described later.
Environment Variables Stay, the Source Changes
Containers still need environment variables, and that is fine. What changes is how often a secret gets materialized, who can trigger that, and how easy it is to audit and revoke access. When a secret needs to change, you rotate it in 1Password and redeploy. No CI config updates, no chasing down which shell profile has the old value, no Slack messages asking "does anyone have the new DB password?"
What You Need To Set Up In 1Password First
Before touching .kamal/secrets, do the 1Password side properly.
- Create a dedicated vault for the application and environment you are deploying, because separate vaults make it much easier to manage who has access to each environment.
- Create a Secure Note item in that vault and add a new field for each environment variable Kamal needs, such as
REGISTRY_PASSWORDandDB_PASSWORD. Other item types can work too, but Secure Note is usually the most flexible option here.

- Create a 1Password service account with access only to the vaults that the deployment actually needs.
- Install the 1Password CLI on the machine that will run
kamal deploy. - Authenticate that CLI with the service account instead of a personal account.
- Verify that the machine can read the expected item before you add the Kamal integration on top.
op item get "Env" --vault "Production"ID: 123456789 Title: Env Vault: Production Created: 55 minutes ago Updated: 6 seconds ago by Alexandre Favorite: false Version: 3 Category: SECURE_NOTE Fields: notesPlain: API Production Environment Variables DB_PASSWORD: [use 'op item get 123456789 --reveal' to reveal] REGISTRY_PASSWORD: [use 'op item get 123456789 --reveal' to reveal]
The service-account part deserves a closer look. 1Password’s service account docs are clear here: service accounts exist for shared environments and automation, and you can scope them to specific vaults and Environments. They also give you a much cleaner automation boundary than tying deploy access to a human account.
This matters because Kamal deploys often happen in the places where access sprawl gets normalized:
- CI runners
- long-lived terminals
- scripts everyone copied from the same wiki page three years ago
If the command that runs kamal deploy authenticates to 1Password with a service account, you can reduce the blast radius dramatically. The deploy path only needs access to the vaults required for that application and that environment. Not your whole 1Password account. Not a random engineer’s personal vault access. Just the deploy secrets.

1Password also documents OP_SERVICE_ACCOUNT_TOKEN as the environment variable used to authenticate the CLI with a service account. That gives you a clear automation path for CI or dedicated deploy hosts.
Use Kamal’s Native 1Password Adapter
In practice, the cleanest option is to let .kamal/secrets fetch from 1Password directly.
Kamal ships a kamal secrets command with a native 1password adapter. The pattern is simple: fetch the secret set once, then extract the individual values you need:
# .kamal/secrets
SECRETS=$(kamal secrets fetch --adapter 1password --account myaccount --from Production/Env REGISTRY_PASSWORD DB_PASSWORD)
KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract REGISTRY_PASSWORD $SECRETS)
DB_PASSWORD=$(kamal secrets extract DB_PASSWORD $SECRETS)![Secrets flow from a centralized 1Password vault to Kamal via a controlled fetch, replacing scattered shell exports and CI config files]
Altogether, this is a meaningful upgrade over manually exporting long-lived values into your shell profile or CI settings.
Here is why:
- 1Password becomes the source of truth instead of a human copying secrets around.
- Kamal still gets the environment variables it expects.
- You reduce the chance of stale secrets living forever in a shell session or a notes app somewhere.
- The fetch and extraction logic stays close to the deployment tool that actually needs the values.
1Password Improves The Source Of Truth
Kamal still materializes environment variables for the deployment target. As 1Password’s own docs warn, you should assume that processes running as the same user can read each other’s environment. So, environment variables do not suddenly become safer just because 1Password is in the picture. What improves is the system that produces the values and the discipline of the path that fetches them.
If your deploy host is messy, 1Password will not save you from that. You still need to care about:
- who can log into the deploy machine
- who can inspect process environments
- what lands in shell history
- what gets echoed in debug output
- what your CI logs keep forever
This is also why service accounts and process scoping matter so much. The real win is not that secrets live in 1Password, but that fewer actors can cause them to leave it in the first place.
Watch Out If You Run Kamal Via Docker
Kamal’s Dockerized mode changes the secret story in one important way. The secret adapters do not work inside the container because the secret-manager CLIs are not installed there. Host environment variables are also unavailable unless you inject them explicitly.
If your setup depends on the 1Password adapter, use a normal Kamal install on the machine that runs the deploys. For cases where Dockerized Kamal is a hard requirement, you can fetch secrets outside the container first and pass them in via -e flags on the Docker alias. However, that shifts the trust boundary back to the host shell and loses most of the adapter benefits.
Digging Deeper: Making This Work In CI/CD
Beyond local deploys, the same adapter-based setup works well in CI/CD too. Here is a GitHub Actions example that shows the key moving parts:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Install 1Password CLI
uses: 1password/install-cli-action@v2
- name: Set up Ruby and install Kamal
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4.8"
- run: gem install kamal
- name: Set up SSH key
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Deploy
run: kamal deployThe only secrets stored in GitHub are OP_SERVICE_ACCOUNT_TOKEN and the SSH key for reaching the deploy target. Everything else comes from 1Password at deploy time through the same .kamal/secrets adapter setup you use locally. Kamal is installed via Ruby, not the Docker alias, so the 1Password adapter actually works.
The pattern is similar in GitLab CI, CircleCI, or Buildkite: install the 1Password CLI, expose the service account token, install Kamal natively, and deploy.
Closing Thought
Ultimately, using 1Password well in a Kamal workflow adds no enterprise theater. It keeps the simplicity while tightening the boundary around the one thing you do not want to leak.
The adapter gives you a single fetch-and-extract pattern that works the same way locally and in CI. As a result, secrets live in one place, access is scoped to what each environment actually needs, and rotating a credential means changing it in 1Password and redeploying.
Keep the environment-variable workflow. Just stop treating the machine that launches it as a bucket full of long-lived secrets.
We want to work with you. Check out our Services page!

