From e46691a3ef744409b2d3dcd311f50046c2f7c2eb Mon Sep 17 00:00:00 2001 From: LIOsDev Date: Thu, 25 Jun 2026 23:50:06 -0400 Subject: [PATCH 1/2] feat: add plain self-hosted Docker deploy template Adapts the existing Coolify template's Dockerfile/entrypoint pattern (official iiidev/iii binary via multi-stage build, HMAC secret generated and persisted on first boot, 0.0.0.0 bind override for the engine's REST/ streams workers) for operators who already run their own Docker host with no PaaS control plane at all - a NAS, a homelab box, or a Windows machine running Docker Desktop - and don't want Coolify-specific env vars or a managed proxy in the way. The README documents the one failure mode most likely to confuse a self-hoster who fronts this with their own reverse proxy: nginx's $host variable strips the port, which silently breaks the viewer's Host-header allowlist even when VIEWER_ALLOWED_HOSTS is configured correctly. Use $http_host instead. Signed-off-by: LIOsDev --- deploy/README.md | 12 ++- deploy/docker/Dockerfile | 38 +++++++++ deploy/docker/README.md | 135 +++++++++++++++++++++++++++++++ deploy/docker/docker-compose.yml | 47 +++++++++++ deploy/docker/entrypoint.sh | 114 ++++++++++++++++++++++++++ 5 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 deploy/docker/Dockerfile create mode 100644 deploy/docker/README.md create mode 100644 deploy/docker/docker-compose.yml create mode 100644 deploy/docker/entrypoint.sh diff --git a/deploy/README.md b/deploy/README.md index 91aa199e2..e2cca4972 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 @@ -49,6 +51,10 @@ 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) with the same auth model. Migrating between them later is a `tar` of 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..ba8fde769 --- /dev/null +++ b/deploy/docker/README.md @@ -0,0 +1,135 @@ +# 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).** + +```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 . +``` + +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..5606b8619 --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -0,0 +1,47 @@ +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). + 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..3eb331909 --- /dev/null +++ b/deploy/docker/entrypoint.sh @@ -0,0 +1,114 @@ +#!/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" +chown -R "$RUN_AS" "$DATA_DIR" + +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 "$@" From 94d672938ad804b57df606e2629d3a4f206b1ed3 Mon Sep 17 00:00:00 2001 From: LIOsDev Date: Fri, 26 Jun 2026 08:12:39 -0400 Subject: [PATCH 2/2] docs+fix: address CodeRabbit findings on Plain Docker deploy template - Scope the TLS-upstream/never-to-host claim to managed platforms; note Plain Docker's exception - Fix stale 'All four' platform count to 'All five' - Document the viewer-port prerequisite for the SSH tunnel option - Note Windows PowerShell incompatibility in the backup command - Skip the recursive chown on /data once it is already node-owned - Document the loopback-bind variant for same-host reverse proxies --- deploy/README.md | 19 +++++++++++-------- deploy/docker/README.md | 10 ++++++++++ deploy/docker/docker-compose.yml | 3 +++ deploy/docker/entrypoint.sh | 7 ++++++- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index e2cca4972..b193b8503 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -32,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 @@ -56,7 +59,7 @@ exec'ing the agentmemory CLI. 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/README.md b/deploy/docker/README.md index ba8fde769..12a047139 100644 --- a/deploy/docker/README.md +++ b/deploy/docker/README.md @@ -56,6 +56,12 @@ 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 @@ -105,6 +111,10 @@ 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 diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index 5606b8619..828aff63c 100644 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -16,6 +16,9 @@ services: # 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" diff --git a/deploy/docker/entrypoint.sh b/deploy/docker/entrypoint.sh index 3eb331909..166f539ce 100644 --- a/deploy/docker/entrypoint.sh +++ b/deploy/docker/entrypoint.sh @@ -21,7 +21,12 @@ RUN_AS="node:node" III_CONFIG="/opt/agentmemory/node_modules/@agentmemory/agentmemory/dist/iii-config.yaml" mkdir -p "$DATA_DIR" -chown -R "$RUN_AS" "$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: