diff --git a/deploy/README.md b/deploy/README.md index 91aa199e2..b193b8503 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,7 +1,8 @@ -# One-click deploy templates +# Deploy templates -Stand up agentmemory on managed infrastructure without rolling your own -Docker host. Each template ships a self-contained Dockerfile that pulls +Stand up agentmemory on managed infrastructure (fly.io, Railway, Render, +Coolify) or on a Docker host you already run yourself (NAS, homelab, plain +`docker compose`). Each template ships a self-contained Dockerfile that pulls `@agentmemory/agentmemory` from npm at build time and copies the iii engine binary in from the official `iiidev/iii` image — no pre-built agentmemory image required. Storage mounts at `/data`; an HMAC secret @@ -17,6 +18,7 @@ exec'ing the agentmemory CLI. | [Railway](./railway/README.md) | Push from GitHub, volume in the dashboard. Easiest managed dashboard flow. | $5/month (Hobby plan flat fee) | | [Render](./render/README.md) | Blueprint-driven; persistent disk attaches automatically. Most "set it and forget it." | $7.25/month (Starter web + 1 GB disk) | | [Coolify](./coolify/README.md) | Self-hosted on your own VPS. Same Docker Compose stack, you own the host and the data. | VPS cost only (Hetzner CX22 ~€3.79/month) | +| [Plain Docker](./docker/README.md) | No PaaS control plane at all — a NAS, a homelab box, a Windows machine with Docker Desktop, anything that runs `docker compose`. You own the host, the proxy (if any), and the data. | Whatever hardware you already have | ## What every template guarantees @@ -30,13 +32,16 @@ exec'ing the agentmemory CLI. - **Only port 3111 is exposed publicly.** The viewer on port 3113 stays bound to the container's localhost. Reach it via SSH tunnel (see each platform's README). -- **TLS upstream of the container.** Every managed platform terminates - TLS at its edge proxy; the templates publish a single internal port - (`3111`) to that proxy, never to the host. Integration plugins - configured with `AGENTMEMORY_REQUIRE_HTTPS=1` will refuse to send the - bearer over plaintext HTTP to a non-loopback host, so a - misconfigured TLS layer fails loud instead of silently leaking the - secret. +- **TLS upstream of the container (managed platforms).** fly.io, + Railway, Render, and Coolify all terminate TLS at their edge proxy; + those templates publish a single internal port (`3111`) to that + proxy, never to the host. Integration plugins configured with + `AGENTMEMORY_REQUIRE_HTTPS=1` will refuse to send the bearer over + plaintext HTTP to a non-loopback host, so a misconfigured TLS layer + fails loud instead of silently leaking the secret. **Plain Docker is + the exception** — it publishes `3111` directly to the host network + by design (see its README), so TLS termination there is on you if + you need it. ## Pick a platform @@ -49,8 +54,12 @@ exec'ing the agentmemory CLI. - Pick **Coolify** if you already run a VPS and want a self-hosted control plane — same Docker Compose stack, no third-party host has your memories. +- Pick **Plain Docker** if you already have a Docker host running + somewhere (NAS, homelab, a Windows box with Docker Desktop) and don't + want a control plane at all — you manage the proxy and TLS, if any, + yourself. -All four give you the same agentmemory API at the same port (3111) +All five give you the same agentmemory API at the same port (3111) with the same auth model. Migrating between them later is a `tar` of `/data` and a re-import — see each platform's README for the exact commands. diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile new file mode 100644 index 000000000..eaa50e8ca --- /dev/null +++ b/deploy/docker/Dockerfile @@ -0,0 +1,38 @@ +ARG III_VERSION=0.11.2 + +FROM iiidev/iii:${III_VERSION} AS iii-image + +FROM node:22-slim + +ARG AGENTMEMORY_VERSION=0.9.12 +ARG III_VERSION=0.11.2 +ARG III_SDK_VERSION=0.11.2 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssl ca-certificates tini gosu curl \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=iii-image /app/iii /usr/local/bin/iii + +# Install agentmemory into a dedicated prefix so the local package.json's +# `overrides` field pins iii-sdk down to match the engine (agentmemory's +# caret range `^0.11.2` otherwise resolves to 0.11.6, the version that +# requires the new sandbox-everything worker model the agentmemory CLI +# is not refactored for yet). `npm install -g` ignores overrides, hence +# the local prefix. +WORKDIR /opt/agentmemory +RUN printf '{"name":"agentmemory-deploy","version":"1.0.0","private":true,"overrides":{"iii-sdk":"%s"}}\n' "${III_SDK_VERSION}" > package.json \ + && npm install "@agentmemory/agentmemory@${AGENTMEMORY_VERSION}" --omit=optional --no-fund --no-audit \ + && ln -s /opt/agentmemory/node_modules/.bin/agentmemory /usr/local/bin/agentmemory + +ENV AGENTMEMORY_III_VERSION=${III_VERSION} \ + TINI_SUBREAPER=1 + +COPY --chmod=0755 entrypoint.sh /usr/local/bin/agentmemory-entrypoint.sh + +EXPOSE 3111 3113 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD curl -fsS http://127.0.0.1:3111/agentmemory/livez || exit 1 + +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/agentmemory-entrypoint.sh"] diff --git a/deploy/docker/README.md b/deploy/docker/README.md new file mode 100644 index 000000000..12a047139 --- /dev/null +++ b/deploy/docker/README.md @@ -0,0 +1,145 @@ +# Deploy agentmemory on plain self-hosted Docker + +For running agentmemory on infrastructure you already control and aren't +asking a PaaS to manage — a NAS, a homelab server, a Windows machine running +Docker Desktop, or any other Docker host with no built-in reverse proxy or +TLS termination. Same Dockerfile/entrypoint pattern as the +[Coolify template](../coolify/README.md), with the platform-specific bits +(`SERVICE_FQDN_*`, Coolify's managed Traefik/Caddy proxy) removed and replaced +with plain `ports:` publishing you control directly. + +## What you get + +- A Docker Compose stack exposing the agentmemory REST/MCP API on port + `3111`, published directly to whatever network your Docker host is on (no + proxy required, but nothing stops you from putting one in front). +- A persistent named volume (`agentmemory-data`) backing `/data` — memories, + BM25 index, and stream backlog survive container recreation. +- An HMAC secret generated on first boot and persisted to the volume (see + below) — never baked into the image or a committed config file. +- The viewer (port `3113`) stays loopback-only inside the container by + default, matching the npm package's own safe default. Reaching it from + another machine is opt-in (see "Viewer access" below). + +## One-time setup + +```bash +git clone https://github.com/rohitg00/agentmemory +cd agentmemory/deploy/docker +docker compose up -d --build +``` + +Watch the first-boot logs for the generated secret: + +```bash +docker compose logs -f agentmemory +# look for a line: AGENTMEMORY_SECRET=<64 hex chars> +``` + +Copy it into your MCP client's environment (see the main repo README's +Claude Code / Cursor / etc. integration sections). It is not printed again on +subsequent boots — to rotate it, see "Rotate the HMAC secret" below. + +## Verify the deployment + +```bash +curl http://:3111/agentmemory/livez +# {"status":"ok"} +``` + +For an authenticated call, send `Authorization: Bearer `. + +## Viewer access (port 3113 stays internal by default) + +Two options, in order of how much you trust the network this host is on: + +**Option A — SSH/local tunnel (recommended for anything beyond a fully +trusted LAN).** + +The viewer port must first be exposed on the Docker host before a tunnel can +reach it: uncomment the `3113:3113` line in `docker-compose.yml`'s `ports:` +block and run `docker compose up -d` to apply it (you can leave +`AGENTMEMORY_VIEWER_HOST`/`VIEWER_ALLOWED_HOSTS` unset for this option, since +the tunnel terminates on `127.0.0.1` itself). Then: + +```bash +ssh -L 3113:127.0.0.1:3113 @ +# then open http://localhost:3113 on your own machine +``` + +**Option B — expose it directly on your LAN.** Reasonable if this Docker +host already lives on a trusted home/office network (e.g. a NAS). Three +things have to change together — doing only one or two of them will not +work, and the failure modes are confusing enough that they're worth listing +explicitly: + +1. Uncomment the `3113:3113` line in `docker-compose.yml`'s `ports:` block. +2. Uncomment and set `AGENTMEMORY_VIEWER_HOST` and `VIEWER_ALLOWED_HOSTS` in + the same file's `environment:` block. `VIEWER_ALLOWED_HOSTS` must be the + *exact* `host:port` your browser will send as the `Host` header (e.g. + `192.168.1.50:3113`) — the viewer rejects anything not on this allowlist + with a `403 forbidden host`, by design (it's a DNS-rebinding guard, not a + bug). +3. **If you put your own reverse proxy in front of this** (nginx, Caddy, + Traefik, etc. — common on a NAS that already runs one for other + services): make sure it forwards the Host header with the port intact. + nginx's `$host` variable *strips the port* (it's meant for server-name + matching, not header passthrough) — use `proxy_set_header Host $http_host;` + instead of `$host`, or step 2's allowlist will never match and every + request 403s even though the configuration looks correct. This is the + single most confusing failure mode in this whole setup if you hit it + blind; it's called out here so you don't have to rediscover it. + +Then `docker compose up -d` to apply, and confirm with: + +```bash +curl -H "Host: :3113" http://:3113/ +``` + +## Rotate the HMAC secret + +```bash +docker compose exec agentmemory rm /data/.hmac +docker compose restart agentmemory +docker compose logs agentmemory | grep AGENTMEMORY_SECRET +``` + +## Back up `/data` + +```bash +docker run --rm -v agentmemory-data:/data -v "$(pwd)":/backup alpine \ + tar czf /backup/agentmemory-backup.tar.gz -C /data . +``` + +This assumes a Linux/WSL2 shell (`$(pwd)` and `tar` both need one). On native +Windows PowerShell without WSL2, run it from a WSL2 shell instead, or +substitute `${PWD}` for `$(pwd)` — see "Windows / Docker Desktop notes" below. + +Restore by extracting that tarball back into the same named volume. + +## Windows / Docker Desktop notes + +This template builds and runs on Docker Desktop's WSL2 backend the same as +any Linux Docker host — there's nothing Windows-specific in the +Dockerfile/compose file itself. Two things that are easy to trip on: + +- Run `docker compose` from a WSL2 shell or a terminal where Docker Desktop's + context is active, not a plain PowerShell session without Docker Desktop's + CLI integration enabled. +- If you bind-mount `/data` to a Windows path instead of using the named + volume this template defaults to, file ownership (`chown` in + `entrypoint.sh`) behaves differently across the Windows/WSL2 filesystem + boundary — stick with the named volume (`agentmemory-data:`) unless you + have a specific reason not to. + +## Known caveats + +- The image builds locally on `docker compose up --build` — first build + pulls `node:22-slim` and `iiidev/iii`, subsequent builds are cache-fast + unless you bump `AGENTMEMORY_VERSION`/`III_VERSION` in the compose file's + `build.args`. +- No TLS termination is included — this template assumes either a fully + trusted local network or that you're fronting it with your own reverse + proxy (see the Host-header caution above if you do). +- arm64 hosts work — the `iiidev/iii` base image and the iii binary + selection both resolve per-architecture automatically. diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 000000000..828aff63c --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -0,0 +1,50 @@ +services: + agentmemory: + build: + context: . + dockerfile: Dockerfile + args: + AGENTMEMORY_VERSION: "0.9.12" + III_VERSION: "0.11.2" + III_SDK_VERSION: "0.11.2" + restart: unless-stopped + # 3111 (REST/MCP) is published to the LAN by default — this template is + # for self-hosting on infrastructure you already control (a NAS, a + # homelab box, a Windows machine running Docker Desktop), not the open + # internet. Put your own reverse proxy / firewall in front if this host + # is internet-reachable. 3113 (viewer) is commented out: it stays + # loopback-only inside the container by default (safe), so it isn't + # reachable from another machine until you both uncomment the port below + # AND set AGENTMEMORY_VIEWER_HOST + VIEWER_ALLOWED_HOSTS (see README). + # If you terminate TLS with your own reverse proxy on this same host and + # don't want 3111 reachable from the LAN directly, bind it to loopback + # instead: "127.0.0.1:3111:3111". + ports: + - "3111:3111" + # - "3113:3113" + environment: + AGENTMEMORY_TEAM_ID: ${AGENTMEMORY_TEAM_ID:-} + AGENTMEMORY_USER_ID: ${AGENTMEMORY_USER_ID:-} + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + # Uncomment to expose the viewer beyond the container's own loopback — + # see the Dockerfile's EXPOSE list and the commented port above, and + # the README's reverse-proxy caution before doing this on a LAN with + # other machines on it. + # AGENTMEMORY_VIEWER_HOST: "0.0.0.0" + # VIEWER_ALLOWED_HOSTS: ":3113" + volumes: + - agentmemory-data:/data + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:3111/agentmemory/livez || exit 1"] + interval: 30s + timeout: 5s + start_period: 30s + retries: 3 + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +volumes: + agentmemory-data: diff --git a/deploy/docker/entrypoint.sh b/deploy/docker/entrypoint.sh new file mode 100644 index 000000000..166f539ce --- /dev/null +++ b/deploy/docker/entrypoint.sh @@ -0,0 +1,119 @@ +#!/bin/sh +# agentmemory first-boot entrypoint. +# +# Runs as root so it can: +# 1. Overwrite the npm-bundled iii-config.yaml (which binds 127.0.0.1 +# and uses relative ./data paths) with a deploy-tuned version that +# binds 0.0.0.0 and uses absolute /data paths. +# 2. chown the host-mounted /data volume to the runtime user (a fresh +# bind mount or named volume is root-owned by default). +# 3. Generate the HMAC secret on first boot and persist it to +# /data/.hmac (chmod 600) so the secret survives restarts. +# +# Then it execs the agentmemory CLI under gosu as the unprivileged +# `node` user. + +set -eu + +DATA_DIR="${AGENTMEMORY_DATA_DIR:-/data}" +HMAC_FILE="${AGENTMEMORY_HMAC_FILE:-/data/.hmac}" +RUN_AS="node:node" +III_CONFIG="/opt/agentmemory/node_modules/@agentmemory/agentmemory/dist/iii-config.yaml" + +mkdir -p "$DATA_DIR" +# Skip the recursive walk once /data is already node-owned (a freshly created +# bind mount or named volume is root-owned; subsequent boots aren't) — avoids +# adding restart latency proportional to volume size on every boot. +if [ "$(stat -c '%U' "$DATA_DIR")" != "node" ]; then + chown -R "$RUN_AS" "$DATA_DIR" +fi + +cat > "$III_CONFIG" <<'EOF' +workers: + - name: iii-http + config: + port: 3111 + host: 0.0.0.0 + default_timeout: 180000 + cors: + allowed_origins: + - "http://localhost:3111" + - "http://localhost:3113" + - "http://127.0.0.1:3111" + - "http://127.0.0.1:3113" + allowed_methods: [GET, POST, PUT, DELETE, OPTIONS] + - name: iii-state + config: + adapter: + name: kv + config: + store_method: file_based + file_path: /data/state_store.db + - name: iii-queue + config: + adapter: + name: builtin + - name: iii-pubsub + config: + adapter: + name: local + - name: iii-cron + config: + adapter: + name: kv + - name: iii-stream + config: + port: 3112 + host: 0.0.0.0 + adapter: + name: kv + config: + store_method: file_based + file_path: /data/stream_store + - name: iii-observability + config: + enabled: true + service_name: agentmemory + exporter: memory + sampling_ratio: 1.0 + metrics_enabled: true + logs_enabled: true + logs_console_output: true +EOF +chown "$RUN_AS" "$III_CONFIG" + +if [ ! -s "$HMAC_FILE" ]; then + SECRET="$(openssl rand -hex 32)" + umask 077 + printf '%s\n' "$SECRET" > "$HMAC_FILE" + chmod 600 "$HMAC_FILE" + chown "$RUN_AS" "$HMAC_FILE" + echo "================================================================" + echo "agentmemory: generated HMAC secret on first boot" + echo "AGENTMEMORY_SECRET=$SECRET" + echo "Copy this value now. It will not be printed again." + echo "Stored at: $HMAC_FILE (chmod 600)" + echo "To rotate: delete $HMAC_FILE on the persistent volume and restart." + echo "================================================================" +fi + +AGENTMEMORY_SECRET="$(cat "$HMAC_FILE")" +export AGENTMEMORY_SECRET + +# Unlike the managed-platform templates (which detect their own platform env +# vars to decide this automatically), a generic self-hosted box has no such +# signal — so this is opt-in only, never auto-detected. The viewer stays +# safe-by-default (127.0.0.1-only, per the npm package's own default) unless +# the operator explicitly sets AGENTMEMORY_VIEWER_HOST themselves (e.g. in +# docker-compose.yml's `environment:` block) to reach it from another host on +# the LAN or through a reverse proxy. If you do this, VIEWER_ALLOWED_HOSTS is +# mandatory too (the viewer refuses non-loopback binds without an explicit +# Host-header allowlist) — see this template's README for the exact gotcha +# you'll hit if a reverse proxy sits in front (nginx's $host variable strips +# the port, which breaks the allowlist match; use $http_host instead). +if [ -n "${AGENTMEMORY_VIEWER_HOST:-}" ]; then + export AGENTMEMORY_VIEWER_HOST + export VIEWER_ALLOWED_HOSTS="${VIEWER_ALLOWED_HOSTS:-}" +fi + +exec gosu "$RUN_AS" agentmemory "$@"