Documentation

Everything you need to install, deploy and use dotMage — the E2E-encrypted .env manager.

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-x86_64 -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.

Deploy server

One command installs everything via Docker:

$ curl -fsSL https://raw.githubusercontent.com/dotMage/server/main/install.sh | 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

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.

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 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 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 existing environment
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 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.
$ dmage token
  Web admin login token (one-time, 5 min):

  dmage_etok_a1b2c3d4e5f6...

  Paste this token in the web admin login page.

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 all dotMage data from this machine (config, tokens, cache). Nuclear option.
$ dmage clean

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_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

The SQLite database at /data/dotmage.db contains everything — encrypted blobs, device tokens, audit log. Back it up regularly.

# While the server is running
$ docker compose exec server sqlite3 /data/dotmage.db ".backup /data/backup.db"

# Copy backup out of container
$ docker compose cp server:/data/backup.db ./backup-$(date +%F).db
! 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.

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-amd64 \
            -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-amd64
        -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

Global flags

Available on every command:

FlagDescription
--env <ENV>Override the active environment for this command
-q, --quietSuppress non-error output
--jsonJSON output for scripting

Exit codes

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