# Runs the agent locally against whichever backends you want to exercise. # # The two axes the application chooses between at startup each have a compose profile per value: # # persistence jpa (default) | mongodb | redis app.persistence.type # vector store simple (default) | milvus spring.ai.vectorstore.type # # `jpa` and `simple` name no service on purpose — they are the two that need no server, which is # the reason they are the defaults — so their profiles start nothing and exist only so the commands # below read the same whichever pair you pick. # # There is a third, `rag`, which is not an axis but a feature: the knowledge base, off unless asked # for. It starts the same Milvus the vector store profile does, because the tool search index and # the knowledge base share a server and differ only by collection. It is separate from the # `milvus` profile so that a knowledge base does not force the tool index off the simple in-heap # store — the two are independent choices, and the commonest one is `simple` plus `rag`: # # COMPOSE_PROFILES=rag docker compose up -d # Milvus only, app run separately # RAG_ENABLED=true ./gradlew :spring-agent-app-feishu:bootRun # # RAG_ENABLED belongs to whichever process runs the agent, not to the profile: the profile starts # Milvus, the variable is what tells the application to use it. Set it on the compose command only # when the `app` profile is also in play. Turning it on without a reachable Milvus stops the # application from starting rather than quietly running without a knowledge base. # # app.ai.rag.milvus.host defaults to localhost, which is what the published port above is, so an # agent run from an IDE or bootRun against a compose Milvus needs nothing else said. Inside the # compose network that default is wrong, which is why the app service overrides it to `milvus`. # # Only one COMPOSE_PROFILES assignment takes effect — writing it twice on one command line silently # keeps the last, so combine profiles with a comma: # # COMPOSE_PROFILES=milvus,rag docker compose up -d # # The agent itself is a third profile, `app`, so the backend profiles are usable without it. To # bring up only the servers and run the agent from an IDE or `./gradlew bootRun` against them: # # PERSISTENCE_TYPE=redis VECTORSTORE_TYPE=milvus \ # COMPOSE_PROFILES=$PERSISTENCE_TYPE,$VECTORSTORE_TYPE docker compose up # # Note that the agent then runs on the host, so point it at the published ports — # PERSISTENCE_TYPE and VECTORSTORE_TYPE still apply, but REDIS_HOST and MILVUS_HOST default to # localhost, which is where those ports are. To run everything in containers instead, add `app`: # # PERSISTENCE_TYPE=redis VECTORSTORE_TYPE=milvus \ # COMPOSE_PROFILES=app,$PERSISTENCE_TYPE,$VECTORSTORE_TYPE docker compose up # # Naming the backend profiles after the property values is what keeps the containers and the # application's own choice from drifting apart: one pair of variables drives both, so there is no # way to start MongoDB and then have the agent write to SQLite. # # The image is not built here — it comes from the Paketo buildpack, so there is no Dockerfile to # point at. Rebuild it after changing code, or the `app` profile runs whatever you built last: # # ./gradlew bootBuildImage # # Model credentials and the chat surface's settings (Feishu or Slack) are read from .env, which is gitignored and yours to fill # in; see application.yaml for the full set. It is optional here, so `docker compose config` works # on a fresh clone, but the application will not start without at least OPENAI_BASE_URL, # OPENAI_API_KEY, OPENAI_MODEL and the EMBEDDING_* trio — or, on DashScope, DASHSCOPE_API_KEY plus # DASHSCOPE_CHAT_MODEL and DASHSCOPE_EMBEDDING_MODEL, one credential covering every endpoint but no # endpoint having a default model — or, on Gemini, GEMINI_API_KEY plus GEMINI_CHAT_MODEL and # GEMINI_EMBEDDING_MODEL with CHAT_MODEL_PROVIDER and EMBEDDING_MODEL_PROVIDER set to google-genai. # or, on Anthropic, CHAT_MODEL_PROVIDER=anthropic plus ANTHROPIC_API_KEY and ANTHROPIC_CHAT_MODEL — # or, for Claude served by a Google Cloud project, ANTHROPIC_BACKEND=vertex with # ANTHROPIC_VERTEX_PROJECT and ANTHROPIC_VERTEX_LOCATION and no key at all, the credential coming # from the workload identity the container already runs under. Anthropic serves no embeddings, so # that set is always paired with one of the others. # Those switches are per kind, so the sets mix: a Gemini chat model over DashScope embeddings needs # one line from each. Image generation is off in every case until IMAGE_MODEL_PROVIDER names a # provider; see spring-agent-provider-dashscope/README.md and # spring-agent-provider-google-genai/README.md. # # Every published port is overridable through a *_HOST_PORT variable, because a machine that # already runs a Redis or a MongoDB is the normal case rather than the exception: # # REDIS_HOST_PORT=6380 COMPOSE_PROFILES=redis docker compose up # # Those names are deliberately distinct from the REDIS_HOST/REDIS_PORT the application reads: the # app reaches its backends over the compose network on their standard ports, and what the host # publishes has nothing to do with it. Sharing one name would let a stray .env entry move the # published port and the application's target in opposite directions. # # Requires Docker Compose v2.20 or newer, for `depends_on: required: false` — that is what lets one # app service wait on whichever backend a profile actually started and ignore the rest. # Docker injects any proxy configured in ~/.docker/config.json into every container it starts, and # this is the list that keeps it away from traffic between them. Costs nothing on a machine with no # proxy set; without it, on a machine with one, nothing here works and every symptom points at the # wrong service. # # Both halves are needed, because clients reach each other both ways: # # the service names — a Go client consults NO_PROXY by *hostname*, before resolving it, so a # proxy whose own no-proxy list covers private IP ranges still swallows `etcd:2379`. Milvus then # dials etcd through the host proxy, gets no gRPC preface back, and aborts, reporting a problem # with etcd — which is healthy and innocent. # # the private ranges — Milvus standalone runs its coordinators in one process and connects to # them over its own container IP on high ports. Those are addresses, not names, so no list of # service names can cover them, and proxying them breaks Milvus internally rather than visibly: # the container stays up and simply never becomes healthy. x-no-proxy: &no-proxy "localhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,etcd,minio,milvus,redis,mongodb,app" x-app-env: &app-env NO_PROXY: *no-proxy no_proxy: *no-proxy # Defaults matching application.yaml, so `docker compose up` with no variables set runs the # combination that needs no server at all. PERSISTENCE_TYPE: ${PERSISTENCE_TYPE:-jpa} VECTORSTORE_TYPE: ${VECTORSTORE_TYPE:-simple} # Service names rather than localhost: these are only read when the matching profile is active, # and are harmless otherwise. REDIS_HOST: redis REDIS_PORT: 6379 MILVUS_HOST: milvus MILVUS_PORT: 19530 # The knowledge base, which is a different thing from MILVUS_HOST above: that one is the tool # search index, this one is retrieval over what users have asked the agent to remember. They are # separate settings because they are separate collections and either can be absent — hence also # the separate `rag` profile, which is how you get a knowledge base without moving the tool index # off the simple in-heap store. # # Off unless asked for, matching application.yaml: turning it on without a reachable Milvus stops # the application from starting, so the profile and this flag are meant to be set together. RAG_ENABLED: ${RAG_ENABLED:-false} RAG_MILVUS_HOST: milvus RAG_MILVUS_PORT: 19530 # Relaxed binding onto spring.data.mongodb.uri. application.yaml has no placeholder for it, so # this is set in its canonical environment-variable spelling rather than through one. SPRING_DATA_MONGODB_URI: mongodb://mongodb:27017/spring-agent services: app: # Behind a profile of its own, so the backend profiles are useful on their own: leave `app` out # and compose brings up just the servers, for running the agent from an IDE or `./gradlew # bootRun` against them. Add it and the whole stack runs in containers. profiles: [app] image: ghcr.io/kezhenxu94/spring-agent:latest env_file: - path: .env required: false environment: *app-env ports: - "${APP_HOST_PORT:-8080}:8080" volumes: # /workspace is the buildpack image's working directory, and the SQLite database # (data/spring-agent.db) and the simple vector store's index (data/vectorstore.json) are both # written relative to it. Without this they live inside the container and vanish with it, # which would make the jpa and simple profiles lose everything on every restart. - agent-data:/workspace/data # Uncomment for TOOLS_SHELL_TYPE=docker, which runs the agent's Bash tool in sibling # containers. Reaching the Docker socket is equivalent to root on the host, so this is left # off by default rather than being a line to remember to remove. # - /var/run/docker.sock:/var/run/docker.sock depends_on: mongodb: condition: service_healthy required: false redis: condition: service_healthy required: false milvus: condition: service_healthy required: false app-web: # The browser surface, the third of the applications and a deployable of its own rather than a # second face on `app`: it carries no integration at all, so running both means two containers # and two images, not one with a flag. Its own profile for the same reason `app` has one. profiles: [app-web] image: ghcr.io/kezhenxu94/spring-agent-web:latest env_file: - path: .env required: false environment: *app-env ports: # A port of its own, so `app` and `app-web` can run side by side against the same backends. - "${APP_WEB_HOST_PORT:-8081}:8080" volumes: # The same reasoning as `app`: /workspace is the buildpack image's working directory, and both # the SQLite database and the simple vector store's index are written relative to it. - agent-web-data:/workspace/data depends_on: mongodb: condition: service_healthy required: false redis: condition: service_healthy required: false milvus: condition: service_healthy required: false mongodb: profiles: [mongodb] image: mongo:8 ports: - "${MONGODB_HOST_PORT:-27017}:27017" volumes: - mongodb-data:/data/db healthcheck: test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"] interval: 5s timeout: 5s retries: 20 redis: profiles: [redis] # Redis 8, not an earlier line: the chat memory repository stores messages as RedisJSON and # reads them through a RediSearch index, and 8 is the first release with both built in. Against # a plain redis:7 the application fails creating that index at startup. image: redis:8 # The two settings the Redis backend needs from its server, spelled out here so that what a # deployment owes it is visible rather than a paragraph in the docs. These are the agent's own # records — shell credentials, scheduled tasks, MCP server configs — not a cache, so a default # eviction policy would be free to throw them away. command: ["redis-server", "--appendonly", "yes", "--maxmemory-policy", "noeviction"] ports: - "${REDIS_HOST_PORT:-6379}:6379" volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 5s retries: 20 # Milvus standalone still needs an etcd for metadata and a MinIO for segment storage; the three # together are what the milvus and rag profiles both mean. One Milvus serves both — the tool # search index and the knowledge base keep separate collections on it, never separate servers — # so asking for both profiles starts these once, not twice. etcd: profiles: [milvus, rag] image: quay.io/coreos/etcd:v3.5.5 environment: # The healthcheck below dials this service by name, so it is subject to the same proxy # interception; see x-no-proxy above. NO_PROXY: *no-proxy no_proxy: *no-proxy ETCD_AUTO_COMPACTION_MODE: revision ETCD_AUTO_COMPACTION_RETENTION: "1000" ETCD_QUOTA_BACKEND_BYTES: "4294967296" ETCD_SNAPSHOT_COUNT: "50000" command: - etcd # The service name, not 127.0.0.1. This is the address etcd hands back to clients as its own, # and Milvus runs in a different container: told to come back on loopback, it dials itself, # finds nothing, and aborts the whole process with "error reading server preface: EOF" — # naming neither etcd nor the setting responsible. - --advertise-client-urls=http://etcd:2379 - --listen-client-urls=http://0.0.0.0:2379 - --data-dir=/etcd volumes: - etcd-data:/etcd healthcheck: # Through the same address and port Milvus uses, rather than etcdctl's default of # 127.0.0.1:2379. The loopback check passes whenever the process is up at all, including when # nothing outside the container can reach it — which made this report healthy while Milvus was # failing to connect, and sent every investigation to the wrong service. test: ["CMD", "etcdctl", "--endpoints=http://etcd:2379", "endpoint", "health"] interval: 10s timeout: 5s retries: 20 minio: profiles: [milvus, rag] image: minio/minio:RELEASE.2023-03-20T20-16-18Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin command: ["minio", "server", "/minio_data", "--console-address", ":9001"] volumes: - minio-data:/minio_data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 10s timeout: 5s retries: 20 milvus: profiles: [milvus, rag] # The same version VectorStoreMilvusTest pins, so what runs here and what the tests assert # against cannot diverge. image: milvusdb/milvus:v2.4.13 command: ["milvus", "run", "standalone"] environment: ETCD_ENDPOINTS: etcd:2379 MINIO_ADDRESS: minio:9000 # See x-no-proxy above: without this, Milvus reaches etcd and MinIO through whatever proxy # the host has configured, and fails at startup on a machine that has one. NO_PROXY: *no-proxy no_proxy: *no-proxy # Milvus does not wait for its own dependencies: it dials etcd and MinIO on startup and exits if # they are not answering yet. Without this it loses that race often enough on a cold machine to # look intermittent, and the failure surfaces as the agent being unable to reach Milvus rather # than as anything naming etcd. depends_on: etcd: condition: service_healthy minio: condition: service_healthy ports: - "${MILVUS_HOST_PORT:-19530}:19530" # Metrics and the health endpoint the healthcheck below uses. - "${MILVUS_METRICS_HOST_PORT:-9091}:9091" volumes: - milvus-data:/var/lib/milvus healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"] interval: 10s timeout: 10s # Generous: a cold Milvus takes appreciably longer to come up than the other two backends, # and the application creates its collection at startup, so giving up early means the agent # starts against a Milvus that is not listening yet. retries: 40 start_period: 60s volumes: agent-data: agent-web-data: mongodb-data: redis-data: etcd-data: minio-data: milvus-data: