# Installation
## npm
```sh
npm install --save-dev dynoxide
```
Or run directly without installing:
```sh
npx dynoxide --port 8000
```
## Homebrew (macOS and Linux)
```sh
brew install nubo-db/tap/dynoxide
```
Homebrew 6.0.0 added [tap trust](https://docs.brew.sh/Tap-Trust). The fully-qualified command above still installs fine (Homebrew trusts a named formula at install time), but the tap stays untrusted after that, so `brew upgrade` will skip Dynoxide and `brew doctor` will flag the tap. Trust it once:
```sh
# Trust just Dynoxide - lets brew upgrade see it again
brew trust --formula nubo-db/tap/dynoxide
# Or trust the whole tap - also clears the brew doctor warning
brew trust nubo-db/tap
```
## Pre-built binaries
Download from [GitHub Releases](https://github.com/nubo-db/dynoxide/releases) for Linux (x86_64, aarch64), macOS (Intel, Apple Silicon), and Windows.
```sh
# Example: Linux x86_64
curl -fsSL https://github.com/nubo-db/dynoxide/releases/latest/download/dynoxide-x86_64-unknown-linux-musl.tar.gz | tar xz
sudo mv dynoxide /usr/local/bin/
```
## Cargo
```sh
cargo install dynoxide-rs
# With encryption support (SQLCipher + vendored OpenSSL)
cargo install dynoxide-rs --no-default-features --features encrypted-full
```
## As a library (Rust)
```toml
[dependencies]
# Minimal - just the embedded database, no server or CLI dependencies
dynoxide-rs = { version = "0.10", default-features = false, features = ["native-sqlite"] }
# Or with encryption:
# dynoxide-rs = { version = "0.10", default-features = false, features = ["encryption"] }
```
## Upgrading from 0.9.x
0.10.0 is a breaking release, but most of the breaks are library-only. The [CHANGELOG](../CHANGELOG.md) has the full list.
**Running the binary** (Homebrew, npm, the release archives, or the Docker image)? One change affects you:
- **MCP over HTTP now requires a bearer token.** Existing HTTP-transport clients break until they send an `Authorization: Bearer <token>` header. A loopback bind generates and persists a token on first run; a non-loopback bind will not start without one (`--mcp-token` or `DYNOXIDE_MCP_AUTH_TOKEN`). The stdio transport is unaffected, and plain `dynoxide serve` (DynamoDB only, no MCP) is unchanged. See [MCP Server](#mcp-server).
**Depending on the `dynoxide-rs` crate?** Also note:
- **`DynoxideError` is now `#[non_exhaustive]`.** Code that matches it exhaustively needs a `_ =>` arm.
- **`Database` is now generic, `Database<S>`.** The parameter defaults to the native backend, so code that names `Database` keeps compiling; a new `NativeDatabase` alias names that default explicitly.
- **Embedding the MCP HTTP server:** `dynoxide::mcp::serve_http` and `serve_http_with_shutdown` take an `HttpOptions` struct (bind host, auth mode, allowed hosts) in place of a bare port.
## GitHub Actions
```yaml
- uses: nubo-db/dynoxide/action@v0.10.0
with:
snapshot-url: https://example.com/test-data.db.zst # optional
port: 8000
```
See [action/action.yml](../action/action.yml) for all inputs and outputs.
## Docker
A 5 MB drop-in for `amazon/dynamodb-local` in containerised test suites. Same DynamoDB-compatible API, faster startup, smaller image. Note that this is a packaging convenience for test fixtures, not a containerised database product; production-database-on-Kubernetes patterns are out of scope.
```sh
docker run --rm -p 8000:8000 ghcr.io/nubo-db/dynoxide
```
With persistent storage:
```sh
docker run --rm -p 8000:8000 \
-v "$(pwd)/data:/data" \
ghcr.io/nubo-db/dynoxide \
serve --host 0.0.0.0 --port 8000 --db-path /data/dynoxide.sqlite
```
The image runs as root by default, matching `amazon/dynamodb-local`, so bind mounts on Linux Just Work without `--user`. The canonical image lives at `ghcr.io/nubo-db/dynoxide`. Mirrors are pushed to `docker.io/nubodb/dynoxide` and `public.ecr.aws/h4s0n6a2/dynoxide` on a best-effort basis. SLSA provenance and SBOM attestations are published to GHCR only; if you want to verify provenance, pull from the GHCR canonical.
If you override `CMD` to bind to a different port, set the healthcheck target with environment variables so the container's `HEALTHCHECK` follows:
```sh
docker run -e DYNOXIDE_HEALTHCHECK_PORT=9000 ghcr.io/nubo-db/dynoxide serve --port 9000
```
`DYNOXIDE_HEALTHCHECK_HOST` and `DYNOXIDE_HEALTHCHECK_PORT` are documented public surface and will not be renamed in a patch or minor release.
### Running as nonroot
For security-conscious operators, opt into a nonroot uid:
```sh
docker run --rm -p 8000:8000 --user 65532:65532 ghcr.io/nubo-db/dynoxide
```
Persistent mode under nonroot needs a host-owned bind mount, since the in-image `/data` is owned by root:
```sh
docker run --rm -p 8000:8000 \
--user "$(id -u):$(id -g)" \
-v "$(pwd)/data:/data" \
ghcr.io/nubo-db/dynoxide \
serve --host 0.0.0.0 --port 8000 --db-path /data/dynoxide.sqlite
```
The default in-memory mode needs no flags whether root or nonroot. The uid 65532 is the well-known nonroot uid used by Google's distroless images; pick any uid you prefer with `--user <uid>:<gid>`.
### MCP over HTTP in Docker
The default image serves DynamoDB only. To also expose the [MCP](#mcp-server) Streamable HTTP transport, override the command to start it on `0.0.0.0` and supply a bearer token. The token is **mandatory** for any non-loopback bind. Pass it via the `DYNOXIDE_MCP_AUTH_TOKEN` environment variable (which keeps it out of shell history and `ps`), not a `--mcp-token` flag:
```sh
TOKEN=$(openssl rand -base64 24)
docker run --rm -p 8000:8000 -p 19280:19280 \
-e DYNOXIDE_MCP_AUTH_TOKEN="$TOKEN" \
ghcr.io/nubo-db/dynoxide \
serve --host 0.0.0.0 --port 8000 \
--mcp --mcp-host 0.0.0.0 --mcp-port 19280
```
DynamoDB is then reachable on `http://localhost:8000` and MCP on `http://localhost:19280/mcp`. Point an HTTP-transport MCP client at the latter with an `Authorization: Bearer <token>` header. See [MCP Server](#mcp-server) for the client config shape.
A few things to know:
- **The token is not optional.** Omit it and the container exits immediately with `a non-loopback MCP bind requires an explicit token`. The default `docker run ghcr.io/nubo-db/dynoxide` stays DynamoDB-only precisely because a token-less `0.0.0.0` MCP bind cannot boot.
- **Reaching MCP from another container** by service name (rather than `localhost`) needs that name added to the Host allowlist: `--mcp-allowed-host <name>` (e.g. `--mcp-allowed-host dynoxide`). The `-p`-mapped `localhost` access above needs nothing extra.
- **`--network host`** (Linux only) is an alternative to `-p`, but it bypasses Docker network isolation and binds MCP directly on the host's network interface, reachable from the LAN, not just the host. Prefer `-p` unless you specifically need host networking.