Documentation

Install, deploy and use dotMage — the end-to-end-encrypted .env manager.

Overview

How it works

Security model

Install CLI

dotMage CLI is a single Rust binary with no dependencies. Choose your platform:

$ brew install dotMage/dotmage/dotmage
$ curl -fsSL https://github.com/dotMage/dotmage/releases/latest/download/dmage-linux-$(uname -m) -o /usr/local/bin/dmage
$ chmod +x /usr/local/bin/dmage
> irm https://raw.githubusercontent.com/dotMage/dotmage/main/install.ps1 | iex

Downloads the binary to %LOCALAPPDATA%\dotmage\ and adds it to PATH. Restart your terminal — dmage works from anywhere.

Or download from GitHub releases.

Upgrading

Direct-binary installs upgrade themselves — the CLI downloads the release, verifies it against the release's SHA256SUMS, and swaps atomically. Homebrew installs use brew:

$ dmage upgrade
$ brew upgrade dotmage   # if installed via Homebrew
$ dmage upgrade --channel dev   # opt into prerelease builds

The default channel is stable and ignores prereleases. --channel dev installs the newest build including vX.Y.Z-dev.N prereleases; the next stable release is a regular upgrade off the channel.

Deploy server

One command installs everything via Docker:

$ curl -fsSL https://raw.githubusercontent.com/dotMage/server/main/install.sh | bash

For a team server, add one flag — the same installer starts the server with team endpoints enabled:

$ curl -fsSL https://raw.githubusercontent.com/dotMage/server/main/install.sh | DOTMAGE_MODE=team bash

This creates a dotmage/ directory with docker-compose.yml, pulls the server and web-admin images, and starts the containers on port 9470. The bootstrap secret is printed to the console — save it.

! The bootstrap secret is printed at first start. Save it — you need it to create the account and to add every new device.

Quick start

After the server is running (example: VPS at 167.71.55.12, port 9470):

# 1. Authenticate with the server
$ dmage auth --server http://167.71.55.12:9470

# 2. Init an app from your .env
$ dmage init myapp
   Created app 'myapp'. Pushed revision 1 from .env (12 keys).

# 3. Run your app with injected secrets (no .env on disk)
$ dmage exec myapp -- npm run dev

Adding another device

On any new machine — just the bootstrap secret and your master password. No first device needed, no tokens to generate.

$ dmage auth --server http://167.71.55.12:9470

  New device — register with bootstrap secret

  Bootstrap secret: ****
  Master password: ****
   Device registered. Key cached (expires in 7d).

$ dmage pull myapp
   Decrypted 12 keys → .env

Your identity is a set of devices, so laptop, home PC and a machine in another city can all use the same account. On a device you're already signed in on, mint a one-time enrollment token — dmage token, or the admin panel's Devices → Add a device — then on the new machine:

$ dmage auth --server https://your-server --enroll dmage_etok_...
# enter your master password → your key is unwrapped locally

Each machine is its own device: independent token, its own TTL, revocable on its own from the Devices page if it's lost. Registering a device always needs proof — an enrollment token from a device you already have (or the server's bootstrap secret) — never just the password.

i Working remotely? The server must be reachable from where you are. If your team isn't all on one network, expose it over HTTPS (a TLS reverse proxy) or put it behind a VPN — dotMage is client↔server, it doesn't sync peer-to-peer.
! On a shared or untrusted computer, prefer dmage exec (secrets stay in memory, nothing written to disk) and dmage lock afterwards — or just use the web panel, which only ever shows metadata, never secret values. Revoke the device when you're done.

Organizing apps with folders

Use / in app names to group them into folders. No configuration needed — the folder is just a naming convention.

$ dmage init work/myapp
$ dmage init work/backend
$ dmage init personal/blog

$ dmage apps
  work/
    myapp                2   envs   2026-06-11
    backend              1   envs   2026-06-10

  personal/
    blog                 3   envs   2026-06-11

All commands work the same — just use the full name with the folder prefix:

$ dmage push work/myapp
$ dmage pull work/backend --env prod
$ dmage exec personal/blog -- npm run dev

The web admin groups apps by folder automatically. Apps without a / appear at the root level as before.

Multiple servers (work / personal)

Optional — with a single server nothing changes. Map project directories to servers in the global config (like git's includeIf) and every command picks the right server from the directory you run it in. No files are created inside your projects.

$ dmage server add work https://secrets.corp.com --path ~/code/work
$ dmage auth --server work
$ dmage server add personal https://home.example.com --path ~/code/personal
$ dmage auth --server personal

# inside a mapped directory the app name defaults to the dir name
$ cd ~/code/work/billing-api
$ dmage push
   Pushed revision 5 (12 keys).  → work (secrets.corp.com)

Server resolution order: --server <name> flag → DOTMAGE_SERVER env var → longest matching mapped path → default (dmage server use <name>). Ambiguity is an error listing candidates, never a silent guess. dmage status shows which server was picked and why; dmage server list shows all servers with auth state and paths.

Beyond .env — any file

Store any config file — DataGrip datasource XML, kubeconfig, service-account JSON. The file name and format are encrypted together with the contents (the server never sees them), and pull on another machine recreates the file under its original name:

$ dmage init dbconf --file dataSources.xml
   Created app 'dbconf'. Pushed revision 1 from dataSources.xml (4.2 KB, text).

# on another machine
$ dmage pull dbconf          # writes dataSources.xml
$ dmage push dbconf          # picks up dataSources.xml automatically

Formats — env (key diff, exec injection), text (line/byte diff), binary (sha256 compare) — are detected from the extension; override with --format. exec works only with env apps. Non-env apps require dmage ≥ 1.3 on all devices; existing .env apps are unaffected.

Key rotation

If a device with a cached key leaves your control, rotate the Account Key — every revision is re-encrypted client-side with a fresh key (the server cannot read blobs, so it cannot do this for you). The operation is resumable: interrupt it at any point and re-run.

$ dmage rotate-key
  Master password: ********
  re-encrypted 51/51 revisions
   Key rotated to generation 2 (51 revision(s) re-encrypted).

After a rotation: old cached keys stop working for new data (devices re-run dmage auth), CI tokens must be regenerated, and — important — backups taken before the rotation remain decryptable by the old key: destroy or re-encrypt them if you rotated because of a compromise.

Team mode v2

One product, two modes: solo is a team of one, and nothing changes until you invite a second person. Each member gets their own master password wrapping the shared vault key — offboarding stops being "change the shared password and hope".

# fresh server — one flag on the installer
$ curl -fsSL https://raw.githubusercontent.com/dotMage/server/main/install.sh | DOTMAGE_MODE=team bash

# existing server — add to docker-compose.yml and restart
#   environment:
#     DOTMAGE_MODE: "team"
$ docker compose up -d

# owner invites a member
$ dmage user invite kolya --role editor
  dmage_uinv_eyJp...          # one-time token — send over a PRIVATE channel

# on their machine — they pick their OWN password
$ dmage auth --invite dmage_uinv_eyJp...
  ✓ Joined as 'kolya' (editor). Key cached.

$ dmage user list
  USER           ROLE     STATUS    SINCE
  owner          owner    active    2026-07-03
  kolya          editor   active    2026-07-03

Roles — owner (everything), editor (push/pull/rollback), viewer (read-only) — are enforced by the server. Note the limit: roles are authorization, not cryptography — every member holds the vault key and could technically decrypt any blob they can download. The audit log now names people, not just devices.

Offboarding (someone leaves)

$ dmage user rm kolya
  ✓ removed 'kolya' (1 device(s) revoked)
  Rotate the Account Key now (recommended)? [Y/n]

Removal deletes their key wraps and revokes their devices; the chained rotate-key makes their cached key useless for anything pushed afterwards. Then: rotate the secret values they saw (what they read, they read) and destroy pre-rotation backups — rotation cannot reach into old backups.

Migrating from a shared password

If two of you have been sharing one master password (the pre-v2 workaround):

# 1. upgrade the server, set DOTMAGE_MODE=team
$ dmage user invite kolya --role editor      # you (owner)
$ dmage auth --invite dmage_uinv_...          # colleague: sets their OWN password
# 2. colleague re-auths their other devices as themselves; you revoke their old devices
$ dmage rotate-key                            # kills every old cached key
# 3. change your master password; destroy pre-rotation backups

Python SDK

Use dotMage from Python code — CI jobs, backend services, scripts. The dotmage package is a second, independent client: a typed API client plus the full client-side crypto, byte-compatible with the dmage CLI. Secrets are encrypted and decrypted locally, exactly as with the CLI — the server still sees only blobs.

$ pip install dotmage
# first device on a fresh server — creates the vault
from dotmage import DotMage

client, recovery_code = DotMage.init_vault(
    "https://secrets.example.com", BOOTSTRAP_SECRET, MASTER_PASSWORD,
    device_name="laptop",
)

with client:
    client.create_app("work/api")
    client.create_env("work/api", "prod")
    client.push("work/api", "prod", {"DATABASE_URL": "postgres://...", "DEBUG": "0"})
    secrets = client.pull("work/api", "prod")   # decrypted locally

Other entry points mirror the CLI flows: DotMage.enroll(...) (add a machine via enrollment token), DotMage.from_ci(...) (scoped CI token), DotMage.join(...) (accept a team invitation). An async client (AsyncDotMage) is included.

# CI: pull one app/env with a scoped token
client = DotMage.from_ci(SERVER_URL, ci_token=CI_TOKEN, master_password=MASTER_PASSWORD)
with client:
    secrets = client.pull("work/api", "prod")

Full docs — getting started, API reference, security model and the crypto contract — live in the repository.

dmage auth

authrequired first
Authenticate with the dotMage server. First device — creates the account. Subsequent devices — registers with bootstrap secret. Or use --enroll for programmatic enrollment.
# First device (creates account)
$ dmage auth --server http://167.71.55.12:9470

# Additional devices (bootstrap secret + master password)
$ dmage auth --server http://167.71.55.12:9470

# Programmatic enrollment (CI, scripts)
$ dmage auth --server http://167.71.55.12:9470 --enroll <token>
--server <URL>
Server URL (required on first auth)
--enroll <TOKEN>
Enrollment token for 2nd+ devices
--ttl <DURATION>
Cache TTL, e.g. 7d, 30d

dmage init

init
Create a new app from the current .env file. Encrypts and pushes as revision 1.
$ dmage init myapp
$ dmage init myapp --file .env.local
<NAME>
Application name (required)
--file <PATH>
Path to .env file (default: .env)

dmage push

push
Push local .env as a new encrypted revision.
$ dmage push myapp
$ dmage push myapp --file .env.production --env prod
<NAME>
Application name (required)
--file <PATH>
Path to .env file (default: .env)

dmage pull

pull
Pull secrets from the server, decrypt locally, and write to .env.
$ dmage pull myapp
$ dmage pull myapp --rev 3 --env prod
$ dmage pull myapp --stdout
<NAME>
Application name (required)
--rev <N>
Specific revision number (default: latest)
--output <PATH>
Output file path
--stdout
Print to stdout instead of writing file
--force
Overwrite without confirmation

dmage sync

sync
One command that does the right thing: pulls when the server is ahead, pushes local changes, or — when both moved — shows a key-level diff and asks you to choose (never auto-merges). One verb for the 90% case, so you don't have to remember push vs pull. Bare dmage in a project also hints when you have unpushed changes.
$ dmage sync
  ✓ in sync (rev 7).
$ dmage sync         # after editing .env
  local changes → pushing
  ✓ Pushed revision 8.

dmage exec

execrecommended
Run a command with decrypted secrets injected as environment variables. Secrets stay in memory — never written to disk.
$ dmage exec myapp -- npm run dev
$ dmage exec myapp --env prod -- python manage.py migrate
<NAME>
Application name (required)
<COMMAND> [ARGS...]
Command to run (after --)

dmage diff

diff
Show difference between local .env and the latest remote revision.
$ dmage diff myapp
$ dmage diff myapp --show-values
<NAME>
Application name (required)
--show-values
Show actual secret values in the diff

dmage history

history
Show revision history for an app — timestamps, devices, revision numbers.
$ dmage history myapp
<NAME>
Application name (required)

dmage rollback

rollback
Rollback an environment to a previous revision. Creates a new revision that copies the target blob.
$ dmage rollback myapp --rev 2
<NAME>
Application name (required)
--rev <N>
Target revision number (required)

dmage apps

apps
List all applications registered on the server.
$ dmage apps

dmage app rm

app rm
Delete an application and all its environments and revisions. This action is irreversible.
$ dmage app rm myapp
$ dmage app rm work/backend --yes
<NAME>
Application name (required)
--yes
Skip confirmation prompt

dmage env

env
Manage environments (dev, staging, prod) within an app. Each environment has its own independent revision history.
# Show active environment
$ dmage env

# List environments for an app
$ dmage env list myapp

# Create a new environment
$ dmage env new myapp staging
$ dmage env new myapp prod --copy-from dev

# Delete an environment
$ dmage env rm myapp staging --yes
env
Show active environment
env list <APP>
List environments for an app
env new <APP> <NAME>
Create new environment
--copy-from <ENV>
Copy secrets from an existing environment. The copy happens on your machine (decrypt → re-encrypt for the new env) — the server can't copy blobs it can't read. Requires CLI ≥ 2.1
env rm <APP> <NAME>
Delete an environment
--yes
Skip confirmation prompt

dmage status

status
Show sync status — what's up-to-date and what's outdated on this device.
$ dmage status

dmage gen-token

gen-token
Generate an enrollment token for adding a new device programmatically (e.g. via --enroll). For CI/CD, use gen-ci-token instead.
$ dmage gen-token --name "work-pc" --ttl 24h
--name <NAME>
Token label (optional)
--ttl <DURATION>
Time to live, e.g. 24h (default: 24h)

dmage open

open
Open the web admin panel in your browser, already logged in — the easiest way in. Resolves the server like push/pull, finds the panel from the server's /health, and signs you in with a one-time link. On a headless/SSH box, add --print to get the link to open elsewhere.
$ dmage open
  ✓ Opening admin panel — http://your-server:9471
  Logging you in automatically (one-time link, valid 5 min).

dmage token

token
Generate a one-time login code for the web admin. Creates a 5-minute enrollment token and prints it. Paste it in the web admin login page — or just use dmage open.
$ dmage token
  Web admin login token (one-time, 5 min):

  dmage_etok_a1b2c3d4e5f6...

  Paste this token in the web admin login page.

dmage completions

completions
Print a shell completion script (bash, zsh, fish, elvish, powershell) to stdout, so Tab completes commands and flags. Homebrew installs these automatically; otherwise redirect the script where your shell looks for completions.
# zsh — one of the dirs in your $fpath
$ dmage completions zsh > ~/.zfunc/_dmage
# bash
$ dmage completions bash > ~/.local/share/bash-completion/completions/dmage
# fish
$ dmage completions fish > ~/.config/fish/completions/dmage.fish
# powershell — add to $PROFILE
$ dmage completions powershell | Out-String | Invoke-Expression

dmage gen-ci-token

gen-ci-tokenCI/CD
Generate a scoped CI token for a specific app and environment. The token is self-contained — it includes both API access and decryption key, but is restricted to the specified app+env on the server.
$ dmage gen-ci-token --app myapp --env prod --ttl 30d
  CI token for myapp/prod:

  dmage_ci_eyJ0Ijoi...

  Store it as a CI secret (e.g. DOTMAGE_CI_TOKEN).
--app <NAME>
Application name (required)
--env <ENV>
Environment name (required)
--ttl <DURATION>
Token lifetime, e.g. 30d (default: 30d)

dmage lock

lock
Remove the cached key from this device's keychain. Device token is kept — you'll need to re-enter your password on next operation.
$ dmage lock

dmage logout

logout
Full logout — removes cached key, device token, and all local data.
$ dmage logout

dmage clean

clean
Wipe dotMage data from this machine. Without a flag it wipes everything (all servers, config, cache) — nuclear. --server <name> wipes just one server.
$ dmage clean
$ dmage clean --server work   # just one

dmage server

servermulti-server
Manage named servers and the directories mapped to them. Renaming changes only the local label — the URL, tokens and cached key are keyed by URL and stay put.
$ dmage server list
$ dmage server add work https://secrets.corp.com --path ~/code/work
$ dmage server rename <old> <new>    # local label only, URL unchanged
$ dmage server map work ~/code/work
$ dmage server unmap work ~/code/work
$ dmage server use work              # default for unmapped dirs
$ dmage server rm work               # remove + wipe its tokens/key
list
All servers with auth state and mapped paths
add <name> <url> [--path DIR]
Add or update a server; --path may repeat
rename <old> <new>
Rename locally (URL unchanged)
map / unmap <name> <dir>
Bind/unbind a directory so commands there auto-route
use <name>
Set the fallback default server
rm <name> [--yes]
Remove a server and wipe its local tokens + key

dmage whoami

whoami
Show who you are on the resolved server — your user, role and device.
$ dmage whoami

dmage user

userteam
Manage team members on a team-mode server. Invitations are one-time sealed tokens — send them privately. Removing a member deletes their key wraps, revokes their devices and offers a key rotation.
$ dmage user list
$ dmage user invite kolya --role editor   # prints a one-time token
$ dmage user role kolya owner
$ dmage user rm kolya

# the invitee, on their machine — picks their OWN password:
$ dmage auth --invite dmage_uinv_...
invite <name> [--role R] [--ttl T]
Roles: owner / editor / viewer. TTL default 24h
role <name> <role>
Change a member's role (owner only)
rm <name>
Offboard: wraps removed, devices revoked, rotation offered

dmage rotate-key

rotate-key
Re-encrypt every revision with a fresh Account Key (client-side). Resumable after interruption. Run it when a device with a cached key leaves your control — note that pre-rotation backups still decrypt with the old key.
$ dmage rotate-key

Server configuration

All settings are configured via environment variables, prefixed with DOTMAGE_.

VariableDefaultDescription
DOTMAGE_DB_URLsqlite:////data/dotmage.dbDatabase URL. SQLite or PostgreSQL DSN.
DOTMAGE_BOOTSTRAP_SECRETauto-generatedOne-time secret for account init. If empty, printed to stderr at startup.
DOTMAGE_MODEsolosolo | team — team endpoints (users, invitations) exist only in team mode; see Team mode.
DOTMAGE_TOKEN_TTL24hDevice access token lifetime. Formats: 123s, 30m, 24h, 7d.
DOTMAGE_REFRESH_TTL30dRefresh token lifetime for token rotation.
DOTMAGE_RATE_LIMIT10/minRate limit on auth endpoints.
DOTMAGE_LOG_LEVELinfoLog level: debug, info, warning, error.
DOTMAGE_STATIC_DIR/app/staticPath to web admin static files.

Docker

Quickest way to run the server:

$ curl -fsSL https://raw.githubusercontent.com/dotMage/server/main/install.sh | bash
$ cd dotmage && docker compose up -d

Or with a custom docker-compose.yml:

version: "3.8"
services:
  server:
    image: ghcr.io/dotmage/server:latest
    restart: unless-stopped
    environment:
      DOTMAGE_DB_URL: "sqlite:////data/dotmage.db"
      DOTMAGE_TOKEN_TTL: "24h"
    volumes:
      - dotmage-data:/data
    ports:
      - "9470:8000"
volumes:
  dotmage-data:

The server will be available at http://<your-ip>:9470. Point the CLI at this address when authenticating.

TLS / Production

For production, put a reverse proxy in front of the server. Example with Caddy (automatic HTTPS with Let's Encrypt):

services:
  server:
    image: ghcr.io/dotmage/server:latest
    restart: unless-stopped
    environment:
      DOTMAGE_DB_URL: "sqlite:////data/dotmage.db"
    volumes:
      - dotmage-data:/data
    expose:
      - "8000"

  proxy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
volumes:
  dotmage-data:
  caddy-data:

Caddyfile:

secrets.example.com {
    reverse_proxy server:8000
}

Without a domain, you can also use nginx or Caddy with a self-signed cert, or just run plain HTTP on a private network:

$ dmage auth --server http://192.168.1.50:9470

Backup & restore

The SQLite database at /data/dotmage.db contains everything — encrypted blobs, the password-wrapped Account Key, device tokens, audit log.

This is a consequence of the end-to-end design, not an oversight: the server only ever stores ciphertext it cannot open, so there is no "forgot password" flow and no server-side recovery. The server cannot restore what it never sees. That makes the database file the whole vault — and backups your responsibility.

! Losing the database means losing access to all secrets — even with the master password. The database contains the encrypted Account Key. Always have a backup.

One-off backup

SQLite's online backup is safe while the server is running — no downtime, no locked writes:

# Online backup via Python (safe while server is running)
$ docker compose exec server python -c "import sqlite3; s=sqlite3.connect('/data/dotmage.db'); d=sqlite3.connect('/data/backup.db'); s.backup(d); d.close()"

# Copy backup out of container
$ docker compose cp server:/data/backup.db ./backup-$(date +%F).db

Scheduled backup (cron)

The same two commands as a nightly cron job on the Docker host (note the \% — cron treats a bare % as a newline):

# crontab -e  — nightly at 03:00, keep two weeks
0 3 * * * cd /opt/dotmage && docker compose exec -T server python -c "import sqlite3; s=sqlite3.connect('/data/dotmage.db'); d=sqlite3.connect('/data/backup.db'); s.backup(d); d.close()" && docker compose cp server:/data/backup.db /var/backups/dotmage-$(date +\%F).db
30 3 * * * find /var/backups -name 'dotmage-*.db' -mtime +14 -delete

Ship the files off the machine with whatever you already use — rsync, restic, rclone. The blobs inside are E2E-encrypted, but treat backups as sensitive anyway: they contain the password-wrapped Account Key, so a weak master password could be brute-forced offline.

Verify a backup

A backup you never checked is a hope, not a backup:

$ sqlite3 dotmage-2026-07-16.db "PRAGMA integrity_check;"
ok

# no sqlite3 on the host? borrow the server image:
$ docker run --rm -v "$PWD:/b" ghcr.io/dotmage/server:latest python -c "import sqlite3; print(sqlite3.connect('/b/dotmage-2026-07-16.db').execute('PRAGMA integrity_check').fetchone()[0])"
ok

Restore

Restoring replaces the database in the data volume with a backup file. Accounts, devices and secrets return to their state at backup time; anything pushed after that moment is gone.

# 1. stop the stack
$ cd dotmage && docker compose down

# 2. copy the backup into the data volume (name: <project>_dotmage-data)
$ docker volume ls | grep dotmage-data
$ docker run --rm -v dotmage_dotmage-data:/data -v "$PWD:/host" alpine cp /host/dotmage-2026-07-16.db /data/dotmage.db

# 3. start and verify
$ docker compose up -d
$ curl -s http://localhost:9470/health   # → "account_exists": true
$ dmage status
i Clients keep working after a restore — device tokens live in the same database. A client that pushed after the backup was taken just creates a new revision on top of the restored state with its next dmage push.

Upgrading the server

dotMage server is distributed as a Docker image. To upgrade an existing installation:

i The admin panel shows a banner when a newer release is available, with the exact command to copy. The canonical upgrade is to re-run the installer — it is idempotent and pins the major, so minor and patch updates arrive on the next pull:
$ curl -fsSL https://raw.githubusercontent.com/dotMage/server/main/install.sh | bash

Prefer to do it by hand? The steps below are what the installer runs.

1. Backup the database

$ cd dotmage
$ docker compose exec server python -c "import sqlite3; s=sqlite3.connect('/data/dotmage.db'); d=sqlite3.connect('/data/backup.db'); s.backup(d); d.close()"
$ docker compose cp server:/data/backup.db ./backup-$(date +%F).db

2. Pull the latest image and restart

$ docker compose pull
$ docker compose up -d

Docker will download the new image and recreate the container. The data volume is preserved — your database, accounts, and all encrypted secrets remain intact.

3. Verify

$ docker compose logs --tail 20 server

Check the logs to confirm the server started cleanly. The CLI does not require an update for server-only upgrades — but it's a good idea to keep both in sync.

i The CLI can be upgraded the same way it was installed: brew upgrade dotmage on macOS, or re-run the curl/PowerShell command on Linux/Windows.

CI/CD setup

dotMage can inject secrets into CI/CD pipelines using scoped CI tokens. Each token gives access to exactly one app+environment — nothing more.

1. Generate a scoped CI token

$ dmage gen-ci-token --app myapp --env prod --ttl 30d
# → prints dmage_ci_eyJ0Ijoi...

Store this as a CI secret named DOTMAGE_CI_TOKEN. That's it — one variable, no dmage auth in the pipeline.

Pipeline examples

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # ── dotMage: pull secrets ──
      - name: Pull secrets
        env:
          DOTMAGE_CI_TOKEN: ${{ secrets.DOTMAGE_CI_TOKEN }}
        run: |
          curl -fsSL https://github.com/dotMage/dotmage/releases/latest/download/dmage-linux-$(uname -m) \
            -o /usr/local/bin/dmage && chmod +x /usr/local/bin/dmage
          dmage pull myapp --env prod --output .env

      # ── your deploy steps ──
      - name: Deploy
        run: npm start
# Reusable template — paste once
.dmage:
  before_script:
    - curl -fsSL https://github.com/dotMage/dotmage/releases/latest/download/dmage-linux-$(uname -m)
        -o /usr/local/bin/dmage && chmod +x /usr/local/bin/dmage
    - dmage pull $DMAGE_APP --env $DMAGE_ENV --output .env

# Your job — just extend .dmage
deploy:
  extends: .dmage
  variables:
    DOTMAGE_CI_TOKEN: $DOTMAGE_CI_TOKEN
    DMAGE_APP: myapp
    DMAGE_ENV: prod
  script:
    - source .env && npm start

How does it compare to Infisical, Vaultwarden, sops & Doppler?

dotMage syncs .env-style secrets between machines with end-to-end encryption, using a small self-hosted server. The tools below overlap with it in some areas and differ in others.

Tool E2E by default Self-hosted Weight & dependencies
dotMageyes — the server only ever stores ciphertextyes; no vendor accountone container + SQLite, ~75 MiB RAM for the whole stack
Infisicalno — encryption is server-side, so the backend can power rotation and integrationsyesPostgres + Redis + API/UI — a small cluster
Vaultwardenyes (Bitwarden protocol)yesone container + SQLite
sops + gityes (age/GPG) — files are encrypted before commitno server at allzero infrastructure
dotenv-vaultpartially — .env.vault is encrypted, but keys flow through dotenv.orgno — cloud onlyzero infra + vendor cloud
Dopplerno — server-side, that's what makes its integrations possibleno — SaaS only, closed sourcezero infra (their cloud)

When to pick them instead

Limitations

? Found an error in this table? File an issue.

Global flags

Available on every command:

FlagDescription
--env <ENV>Override the active environment for this command
--server <NAME>Use a specific configured server for this command
-q, --quietSuppress non-error output
--jsonMachine-readable JSON output on status, diff, history, apps, whoami. One JSON document on stdout, errors stay on stderr; schemas are a semver contract. diff --json never includes secret values

Exit codes

CodeMeaning
0Success
1General error / not found
3Not authenticated — run dmage auth
4Conflict (e.g. concurrent push)