Docs

Docker

Run your app in a container with no .env and one credential.

Your image carries no secrets and no .env. It gets one credential from wherever you deploy it, fetches everything else at startup, and runs your app.

FROM oven/bun:1-alpine

RUN bun add -g @shellhaki/envi-cli
ENV PATH="/root/.bun/bin:${PATH}"

WORKDIR /app
COPY . .
RUN bun install --frozen-lockfile

ENTRYPOINT ["envi", "run", "--"]
EXPOSE 3000
CMD ["bun", "index.ts"]
docker build -t myapp .
docker run -p 3000:3000 -e ENVI_TOKEN=st_live_xxxxx myapp

envi run fetches the environment's secrets, puts them in the process environment, and execs your command. Your app reads process.env.DATABASE_URL and knows nothing about Envi.

bun add -g installs binaries to /root/.bun/bin, which is not on PATH in that image. Without the ENV PATH line you get envi: not found. With npm, npm install -g @shellhaki/envi-cli needs no PATH change.

Settings

Everything is an environment variable, so it works the same in docker run, Compose, Coolify, Railway, Fly, or Kubernetes.

| Variable | Purpose | |---|---| | ENVI_TOKEN | The credential. A service token, scoped to one environment | | ENVI_PROJECT | Which project, when the credential isn't a service token | | ENVI_ENVIRONMENT | Which environment, same | | ENVI_API_URL | Only if you self-host |

A service token already belongs to one environment, so ENVI_TOKEN on its own is enough — no envi.toml, no envi init, and no environment id baked into the image. The same image runs in staging or production depending on the token you give it.

envi token create --name myapp-production --permission read

Use a read token for deployments. It can fetch secrets and nothing else.

Node instead of Bun

FROM node:22-alpine
RUN npm install -g @shellhaki/envi-cli

WORKDIR /app
COPY . .
RUN npm ci --omit=dev

ENTRYPOINT ["envi", "run", "--"]
CMD ["node", "server.js"]

Deploy platforms that only take a Dockerfile

On Coolify, Railway, Render and Fly there is no docker run to pass flags to: the same Dockerfile applies, and the variables go in the platform's own environment-variable panel. ENTRYPOINT and CMD compose, so a platform that overrides the start command still gets wrapped.

If a platform replaces the entrypoint rather than the command, the wrapper disappears silently and your app starts with no secrets. A script is the defensive version, and it also lets the image run without Envi at all, which is useful locally:

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["bun", "index.ts"]
#!/bin/sh
set -eu
# No token: run anyway, so local builds and CI smoke tests need no credential.
[ -n "${ENVI_TOKEN:-}" ] || exec "$@"
exec envi run -- "$@"

Things that will bite

Never fetch secrets during the build. RUN envi pull writes a .env into an image layer, where it stays forever and is readable by anyone who pulls the image. Build-time is also the wrong moment — your platform builds without runtime variables. ENTRYPOINT runs at container start, which is the right one.

Add a .dockerignore. COPY . . will copy your local .env into the image otherwise, which is the exact thing this setup exists to prevent:

node_modules
.env
.git

Signals work. envi run becomes PID 1 and forwards SIGTERM, SIGINT and SIGHUP to your process, then exits with its status. docker stop shuts the app down cleanly instead of force-killing it after the ten-second grace period.

Health checks should hit your app, not Envi:

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget -qO- http://127.0.0.1:3000/health >/dev/null || exit 1

Reading key-value entries instead

If your application reads the key-value store rather than environment variables, it needs the same one credential and no wrapper — it calls GET /kv with the token itself:

ENTRYPOINT ["bun", "index.ts"]
docker run -e ENVI_TOKEN=st_live_xxxxx myapp

The two do different jobs: envi run injects an environment's secrets before your process starts, and the key-value store holds project values your code reads and writes while it runs. You can use either, or both.

Platforms that run no container of yours

Vercel, Netlify and Cloudflare Workers build and run your code themselves, so there is no entrypoint to wrap and no process to start with envi run. For those, copy the environment out of the dashboard and paste it into the platform once: Copy all on the secrets panel puts every secret on your clipboard as .env text, which each of their environment-variable screens parses in bulk. See Copy all.

That is a copy, not a link — rotating a secret in Envi afterwards means pasting again. Where you do control the process, the wrapper above is the better trade.