Problem: I can manage my entire Docker Compose stack with Git, except secrets.
Solution: Encrypt your secrets using Ojster and safely store them in Git — even inside a public repository. Values can only be decrypted by the server holding the private key.
Docker Compose workflows commonly rely on environment variables. Even when encrypted with tools like Ansible Vault, Dotenvx, or env-vault, decrypted values often end up embedded in container specs, visible in management UIs (Docker Desktop, Portainer), or leaked via docker inspect, logs, or image metadata.
Ojster closes this gap. It provides one-way, quantum-safe encryption (MLKEM + AES) and a hardened decryption server so you can commit encrypted values to Git and decrypt them just in time at container startup — in memory and under strict least-privilege constraints. This zero-trust, ephemeral secrets solution is ideal for self-hosting prebuilt images in a GitOps workflow with Docker Compose.
seal, unseal, and keypair implementations (MLKEM + AES) and can also use Dotenvx as an alternative backend if desired.Get ready to BYOB and safely store encrypted secrets in Git.
Ojster is in an early-stage, pre-release phase. Expect breaking changes as the design evolves, security hardening improves, and real-world feedback shapes the API and integration model. The core concepts are stable, but details may change between versions. Review release notes before upgrading.
On the server that hosts your Docker containers:
git clone https://github.com/ojster/ojster
cd ojster
# Build Your Own Binary (image)
docker bake
# Run containers as the current uid/gid to grant access to the files in this repo
# Not required for Docker Desktop on macOS
# For a production setup it's recommended to use a dedicated server uid/gid
PUID="$(id -u)"; PGID="$(id -g)"
export PUID PGID
# Common docker run flags
COMMON1=(
--user="${PUID:-64646}:${PGID:-64646}"
--pull=never
--read-only
--cap-drop=ALL
--network=none
--security-opt=no-new-privileges=true
)
# Generate a keypair using Ojster's built-in keypair command
docker run "${COMMON1[@]}" --rm -v "$(pwd)":/o ojster/ojster keypair
# Do NOT commit the ojster_priv.key to Git!
# Bring up Ojster server
docker compose up -d
# Encrypt a variable using Ojster's built-in seal command (no private key needed)
# Enter an example secret and press Ctrl-D (twice) when done.
docker run "${COMMON1[@]}" -it --rm -v "$(pwd)":/o ojster/ojster seal EXAMPLE
CLIENT_DIR=examples/01_client
COMMON2=(
--project-name=ojster-client-example
--file=./"$CLIENT_DIR"/compose.base.yaml
--project-directory=.
)
# Bring up example stack WITHOUT Ojster enabled
docker compose "${COMMON2[@]}" up
# Note in output that env var is still encrypted (prefix OJSTER-1:)
# Bring up example stack WITH Ojster enabled
docker compose "${COMMON2[@]}" -f ./"$CLIENT_DIR"/compose.ojster.yaml up
# Note in output that env var is now decrypted
# Cleanup
docker compose "${COMMON2[@]}" down
docker compose down -v
Ideally the Ojster server compose.yaml file becomes part of the stack you manage via GitOps, as well a the PUBLIC key, so you can easily add new encrypted environment variables.
Notes
keypair and seal commands. If you prefer, Ojster can also interoperate with Dotenvx — it is pluggable and works with Dotenvx out of the box.Add the snippet in compose.ojster.yaml to any service you want to integrate. Ojster acts as a lightweight docker-init replacement and injects decrypted values at process start — no need to modify entrypoints, commands, or rebuild images.
Ojster currently does not support podman. The dockerfile relies on a BuildKit feature which podman/buildah doesn’t offer. Additionally podman doesn’t support the bake .hcl files, volume.type=image and has a different --init implementation: /run/podman-init. But most importantly podman will throw this error when trying to provide our own init binary in combination with --init: “Error response from daemon: container create: conflict with mount added by –init to “/run/podman-init”: duplicate mount destination”.
If you know Kubernetes, Ojster is conceptually similar to Bitnami Sealed Secrets — but for Docker Compose.
Shared principles:
Sealed Secrets uses a Kubernetes controller. Ojster applies the same pattern to Docker Compose using a lightweight client and a hardened decryption server.
Ojster implements its own MLKEM + AES sealing/unsealing and keypair generation, but it remains pluggable and compatible with Dotenvx. The table below compares Ojster to plain Dotenvx (the free, Open Source version) and Docker secrets.
| Feature | Ojster | dotenvx runoutside container |
dotenvx runinside container |
Docker secrets |
|---|---|---|---|---|
| Secure secrets in Git | ✅ | ✅ | ✅ | ❌ |
| Encrypted env vars in container spec |
✅ | ❌ | ⚠️ | ❌ |
| Quantum-safe encryption | ✅ | ❌ | ❌ | ❌ |
| Unmodified container image |
✅ | ✅ | ❌ | ✅ |
| Native Docker Compose | ✅ | ❌ | ✅ | ✅ |
| Air-gapped private key access |
✅ | ❌ | ❌ | N/A |
| 0 third-party runtime dependencies (client side) |
✅ | ✅ | ❌ | ✅ |
| Image size increase | N/A | N/A | ~50 MB | N/A |
Interpretation
dotenvx run outside container calling docker compose up still places decrypted values into container specs (visible to orchestration tooling), requires wrapping around Compose commands, and is not air-gapped (e.g. to prevent a malicious dependency from data exfiltration).dotenvx run inside container requires shipping decryption tooling in each image (823 third-party dependencies) and exposes keys to all containers, increasing attack surface and image size. The official walkthrough sets the private key as plaintext env var in the container spec.Ojster does not provide the feature set of a full secrets platform. If that’s what you need, consider:
OJSTER_REGEX (configurable).key → encrypted value map to the Ojster server over a Unix domain socket.execs the real entrypoint..env and .env.keys symlinked to private keystatfsSecurely provision the private key on the Ojster server host. Only the Ojster server should ever have access to the private key. Access to the ojster volume (which contains the IPC socket file) is equivalent to the ability to request decryptions: any process that can open the socket can talk HTTP to the server and obtain decrypted values. Treat the socket like a sensitive IPC endpoint.
chmod 600 and ownership matching UID/GID running the Ojster server.no-new-privileges, no DNS, no outbound network access, immutable rootfs, tmpfs for tmp files.Contributing
./tools/test passes locally.Testing
./tools/test
License
Apache License 2.0.
Ojster (pronounced “oyster”) is a metaphor for a protective shell that keeps something valuable sealed away until the moment it’s needed. The J gives the name a distinctive, memorable twist while subtly nodding to its creator. Its nautical undertone fits naturally within the Docker ecosystem.