--- title: Environment Variables Explained --- # Environment Variables Explained IoT DC3 has two separate sets of environment files, and they target different consumers. The root `.env` feeds Docker Compose interpolation; `dc3/env/dev.env(.sh)` feeds local Java processes. By the end of this page you'll know which variable belongs in which file, why `localhost` ports differ from the ports a container sees, and which two secrets you must change before going to production. > You are here: you've already [developed locally from source](./) or brought the stack up with Compose. Next, > see [Deployment Modes and Image Sources](../guide/usage) to learn how the whole stack starts. ## Why Two Sets of Files The same variable name (say `POSTGRES_HOST`) means different things inside a container versus in a Java process on your laptop. Containers reach each other by service name on the Compose network (`dc3-postgres`). A Java process running in your IDE lives on the host, so it can only connect through the **ports Compose publishes to the host** (such as `localhost:35432`). The two file sets exist for these two non-overlapping paths. Mix them up and a local process tries to resolve a container name it can't reach, or a container tries to talk to a `localhost` it can't see. | File | Consumer | Purpose | Injection Method | |----------------------|-------------------------|------------------------------------------------------------------------------------|-------------------------------------| | `.env.example` | Docker Compose template | Copied to root `.env`; defines image registry, image tag, published ports | Compose variable interpolation | | `.env` | Docker Compose | Untracked local config, interpolated by `dc3/docker-compose*.yml` | Compose variable interpolation | | `dc3/env/dev.env` | IDE (EnvFile plugin) | Environment variables for local Java processes, **without** `export` | Read by the IDE EnvFile plugin | | `dc3/env/dev.env.sh` | Shell | Environment variables for local Java processes, with `export`, loaded via `source` | Injected into the shell environment | ::: warning The root `.env` is not injected into local Java processes The root `.env` serves Docker Compose only. It is **not** automatically passed to Java processes started by your IDE or command line. Running from source locally requires `dc3/env/dev.env(.sh)`, which points the process at the dependency ports Compose publishes on `localhost`. Setting `POSTGRES_HOST=localhost` in the root `.env` changes nothing about any container's runtime environment. ::: ## Two Isolated Paths The diagram shows how each file set takes effect along its own path. The key point: these two paths **never cross**. Compose does not read `dev.env.sh`, and local Java processes do not read the root `.env`. ```mermaid flowchart LR subgraph ComposePath["Compose Path"] Env[".env (root)"] -->|"Compose interpolation"| Yml["docker-compose*.yml"] Yml -->|"environment / env_file"| Ctn["Container process
sees dc3-postgres:5432"] end subgraph LocalPath["Local Source Path"] Sh["dc3/env/dev.env.sh"] -->|"source (export)"| Proc["Local Java process
sees localhost:35432"] DevEnv["dc3/env/dev.env"] -->|"IDE EnvFile plugin"| Proc end Field["host ports 35432/35672/31883"] -.->|"host-published"| Proc Ctn -.->|"internal ports 5432/5672/1883"| Field ``` The mapping between host ports and internal ports is the core of this diagram: | Dependency | host (for local processes) | internal (for container-to-container) | |---------------|----------------------------|---------------------------------------| | PostgreSQL | `localhost:35432` | `dc3-postgres:5432` | | RabbitMQ AMQP | `localhost:35672` | `dc3-rabbitmq:5672` | | EMQX MQTT | `localhost:31883` | `dc3-emqx:1883` (conventional value) | ## How to Use ### Bring the Stack Up with Compose Create a local `.env` from the template, then bring it up with `make` (backed by `podman compose`): ::: code-group ```bash [make] cp .env.example .env make up-db && make up-optional && make up-dev ``` ```bash [podman compose] cp .env.example .env podman compose -f dc3/docker-compose-dev.yml config --quiet ``` ::: Variables in the root `.env` interpolate the compose files, for example images and published ports: ```yaml image: ${DC3_IMAGE_REGISTRY:-pnoker}/dc3-gateway:${DC3_IMAGE_TAG:-2026.6} ports: - "${DC3_BIND_HOST:-127.0.0.1}:${DC3_GATEWAY_PORT:-8000}:8000" ``` Compose does not inject every variable in `.env` into every container. Only the variables the compose files explicitly reference via `environment` or `env_file` reach a container. ### Run Locally from Source `source` the file before starting a Java process from the command line: ```bash source dc3/env/dev.env.sh ``` Without it, the local process falls back to container service names (`dc3-postgres`, `dc3-rabbitmq`, `dc3-center-manager`) or default ports, so it can't reach the dependencies on your machine. JetBrains IDEA users should use `dc3/env/dev.env` instead (same content, no `export`): install the EnvFile plugin, enable EnvFile in the Run Configuration, and add `dc3/env/dev.env`. Don't use `.env.example` directly as an IDEA environment file — it's a Compose template, not Java runtime configuration. ## Key Variables Grouped by Scenario The groups below cover what you actually need for a given scenario. The full long table is collapsed under `::: details` at the end of the page; here we list only the variables you'll really change or look up when troubleshooting. The three `Scope` values mean: `Runtime` (read by both local and in-container Java processes), `Compose only` (only the root `.env` for Compose), and `Per-process` (a single-service override). ### Security Keys (Runtime) Two secrets are the platform's root of identity. Their defaults exist only for first-run convenience and must never go to production. | Variable | Default | Purpose | |--------------------|------------------------------------------|----------------------------------------------------------------------------------| | `DC3_SECURITY_KEY` | `dc3.security.key.2026.io.github.pnoker` | Signing key the auth center uses to generate and verify login tokens | | `AUTH_HMAC_SECRET` | `io.github.pnoker.dc3` | HMAC-SHA256 key the gateway uses to sign `X-Auth-Principal` for backend services | ::: danger Change to strong random values in production, and never commit or print them `DC3_SECURITY_KEY` and `AUTH_HMAC_SECRET` ship with defaults. In production, change them to strong random values and never commit them to the repository, write them to logs, or print them as-is. When the Spring profile is `pre` or `pro` and `AUTH_HMAC_SECRET` is empty or still equals the default `io.github.pnoker.dc3`, the service throws `IllegalStateException` and refuses to start. This is an intentional security gate — don't bypass it. ::: ### PostgreSQL (Runtime) Local processes connect to `localhost:35432`; in-container processes connect to `dc3-postgres:5432`. `POSTGRES_SCHEMA` overrides the schema only inside a single-service process (such as `dc3_manager` or `dc3_data`) — don't set it globally. | Variable | Default | Scope | Purpose | |---------------------|-------------|--------------|--------------------------------------------------| | `POSTGRES_HOST` | `localhost` | Runtime | `localhost` locally, `dc3-postgres` in-container | | `POSTGRES_PORT` | `35432` | Runtime | host-published port; internal is `5432` | | `POSTGRES_USERNAME` | `dc3` | Runtime | Username | | `POSTGRES_PASSWORD` | `dc3dc3dc3` | Runtime | Password | | `POSTGRES_DB` | `dc3` | Runtime | Database name | | `POSTGRES_SCHEMA` | (unset) | Per-process | Single-service schema override | | `DC3_POSTGRES_PORT` | `35432` | Compose only | Port the container publishes to the host | ### RabbitMQ (Runtime) AMQP host port `35672`, internal `5672`. With TLS on, the internal port switches to `5671`. | Variable | Default | Scope | Purpose | |--------------------------------|-------------|--------------|--------------------------------------------------| | `RABBITMQ_HOST` | `localhost` | Runtime | `localhost` locally, `dc3-rabbitmq` in-container | | `RABBITMQ_PORT` | `35672` | Runtime | AMQP host port; internal `5672` | | `RABBITMQ_USERNAME` | `dc3` | Runtime | Username | | `RABBITMQ_PASSWORD` | `dc3dc3dc3` | Runtime | Password | | `RABBITMQ_VIRTUAL_HOST` | `dc3` | Runtime | virtual host | | `RABBITMQ_SSL_ENABLED` | `false` | Runtime | Enable TLS (uses 5671 when true) | | `DC3_RABBITMQ_PORT` | `35672` | Compose only | AMQP published port | | `DC3_RABBITMQ_MANAGEMENT_PORT` | `15672` | Compose only | Management UI published port | ### EMQX / MQTT (Runtime) MQTT broker host port `31883`. EMQX also publishes several other ports (WebSocket, Dashboard) — see the collapsed long table for the full list. | Variable | Default | Scope | Purpose | |---------------------------|-------------|--------------|---------------------------------------------------------| | `MQTT_BROKER_HOST` | `localhost` | Runtime | broker host | | `MQTT_BROKER_PORT` | `31883` | Runtime | broker port (published by EMQX; internal around `1883`) | | `MQTT_USERNAME` | `dc3` | Runtime | Username | | `MQTT_PASSWORD` | `dc3dc3dc3` | Runtime | Password | | `DC3_EMQX_MQTT_PORT` | `31883` | Compose only | MQTT published port | | `DC3_EMQX_DASHBOARD_PORT` | `18083` | Compose only | Dashboard published port | ### gRPC / facade (Runtime) Center services talk to each other through facades. Distributed deployments default to `DC3_FACADE_MODE=grpc`, and local processes point `CENTER_*_HOST` at `localhost`. | Variable | Default | Scope | Purpose | |-------------------------------|-------------|---------|-------------------------------------------------------------| | `CENTER_AUTH_HOST` | `localhost` | Runtime | Auth center host | | `CENTER_MANAGER_HOST` | `localhost` | Runtime | Manager center host | | `CENTER_DATA_HOST` | `localhost` | Runtime | Data center host | | `CENTER_AGENTIC_HOST` | `localhost` | Runtime | Agentic center host | | `DC3_FACADE_MODE` | `grpc` | Runtime | facade protocol mode | | `DC3_FACADE_GRPC_DEADLINE_MS` | `3000` | Runtime | Per-request gRPC deadline; `0` disables the client deadline | ### Gateway and Service Ports (Compose only) The gateway is the only external HTTP entry point (`8000`). The HTTP and gRPC published ports of each center are listed below. `SERVER_PORT` and `GRPC_SERVER_PORT` are single-process overrides, useful only when you run multiple services locally and need to avoid port collisions. | Variable | Default | Scope | Purpose | |----------------------------------|---------|--------------|-------------------------------------------| | `DC3_GATEWAY_PORT` | `8000` | Compose only | Gateway HTTP published port (entry point) | | `DC3_AUTH_PORT` | `8300` | Compose only | Auth center HTTP | | `DC3_MANAGER_PORT` | `8400` | Compose only | Manager center HTTP | | `DC3_DATA_PORT` | `8500` | Compose only | Data center HTTP | | `DC3_AGENTIC_PORT` | `8600` | Compose only | Agentic center HTTP | | `DC3_AUTH_GRPC_PORT` | `9300` | Compose only | Auth center gRPC | | `DC3_MANAGER_GRPC_PORT` | `9400` | Compose only | Manager center gRPC | | `DC3_DATA_GRPC_PORT` | `9500` | Compose only | Data center gRPC | | `DC3_LISTENING_VIRTUAL_TCP_PORT` | `6270` | Compose only | Listening Virtual driver TCP publish | | `DC3_LISTENING_VIRTUAL_UDP_PORT` | `6271` | Compose only | Listening Virtual driver UDP publish | | `SERVER_PORT` | (unset) | Per-process | Single-service HTTP port override | | `GRPC_SERVER_PORT` | (unset) | Per-process | Single-center gRPC port override | ::: warning `DC3_LISTENING_VIRTUAL_*_PORT` are host-published ports `DC3_LISTENING_VIRTUAL_TCP_PORT` and `DC3_LISTENING_VIRTUAL_UDP_PORT` are the ports Compose publishes to the host. The process's internal ports use `TCP_PORT` and `UDP_PORT` (Per-process). Don't confuse the two. ::: ### Agentic / AI (Runtime) The `AGENTIC_FALLBACK_OPENAI_*` group only kicks in as a fallback when `dc3_model_provider` has no usable provider configured. Conversation memory is off by default. | Variable | Default | Purpose | |---------------------------------------|--------------------------------|---------------------------------------------------------------------------------| | `AGENTIC_FALLBACK_OPENAI_BASE_URL` | `https://api.openai.com` | Fallback OpenAI-compatible API address | | `AGENTIC_FALLBACK_OPENAI_API_KEY` | (empty) | Fallback API key (fill in when the endpoint requires authentication) | | `AGENTIC_FALLBACK_OPENAI_MODEL` | `gpt-4o` | Fallback model name | | `AGENTIC_FALLBACK_OPENAI_TEMPERATURE` | `0.7` | Sampling temperature (0.0–2.0) | | `AGENTIC_FALLBACK_OPENAI_MAX_TOKENS` | `2048` | Maximum output tokens | | `AGENTIC_MEMORY_SCHEMA_INIT` | `never` | Spring AI JDBC memory table init mode (`always`/`never`/`create_if_not_exists`) | | `AGENTIC_MEMORY_ENABLED` | `false` | Whether persistent conversation memory is on | | `AGENTIC_TOOL_CALLING_ENABLED` | `true` | Whether tool calling is on | | `AGENTIC_MEMORY_MAX_MESSAGES` | `50` | Maximum messages retained per conversation window | | `AGENTIC_ATTACHMENT_STORAGE_PATH` | `dc3/data/agentic/attachments` | Attachment storage path | ::: info The table binding for `AGENTIC_MEMORY_SCHEMA_INIT` is governed by the code `AGENTIC_MEMORY_SCHEMA_INIT` defaults to `never` and is meant to control the memory table init mode. But after Compose injects it into the container, no `application*.yml` in the repository binds it to Spring AI's `initialize-schema`; the memory tables are actually pre-created by the initdb scripts. Whether setting it to `always` truly triggers automatic table creation needs to be determined from the code — don't treat it as a wired, working switch. ::: ::: danger Real API keys must not enter docs, logs, or commit history Sensitive values like `AGENTIC_FALLBACK_OPENAI_API_KEY` must never be written into docs, logs, or commit history. Configure production providers in the `dc3_model_provider` table; the fallback is just a safety net. ::: ::: info Defaults in the table come from `.env.example` and differ from the in-code fallback defaults The values above for `AGENTIC_MEMORY_ENABLED` (`false`) and `AGENTIC_ATTACHMENT_STORAGE_PATH` ( `dc3/data/agentic/attachments`) come from `.env.example`. On the `cp .env.example .env` + `source` path, these values are explicitly injected. But the in-code fallback defaults in `application-agentic.yml` differ: when the environment variables are unset, memory defaults to enabled and the attachment path is `dc3/data/upload/agentic/attachment`. When you're not on the `.env.example` path, the code takes precedence. ::: ### Batch Processing (Runtime) MQTT and point values each have a "count threshold + interval" pair. A Quartz schedule flushes the accumulated buffer all at once every `interval` seconds, where `speed = count / interval`. | Variable | Default | Purpose | |------------------------|---------|-------------------------------------------| | `MQTT_BATCH_SPEED` | `100` | MQTT batch size threshold (records/batch) | | `MQTT_BATCH_INTERVAL` | `5` | MQTT batch interval (seconds) | | `POINT_BATCH_SPEED` | `100` | Point value batch size threshold | | `POINT_BATCH_INTERVAL` | `5` | Point value batch interval (seconds) | ### Image Sources (Compose only) On mainland China networks, set `REGISTRY` to `cn` to use the Aliyun mirror. Note: the Makefile reads `REGISTRY` and Compose interpolation reads `DC3_IMAGE_REGISTRY` — each governs its own segment. | Variable | Default | Purpose | |----------------------|-------------|--------------------------------------------------------------------------------------------| | `REGISTRY` | `auto` | Makefile image-source selector (accepts only `auto`/`global`/`cn`; other values error out) | | `DC3_IMAGE_REGISTRY` | `pnoker` | Image namespace | | `DC3_IMAGE_TAG` | `2026.6` | Image tag for all services and dependencies | | `DC3_BIND_HOST` | `127.0.0.1` | Published-port bind address (`0.0.0.0` for external access) | ### Observability (Compose only / Runtime) The optional stack (EMQX, ELK, Prometheus, Grafana) comes up via `make up-optional`. Its ports and JVM parameters are below. | Variable | Default | Scope | Purpose | |----------------------|-------------------------|--------------|----------------------------------| | `GF_SERVER_ROOT_URL` | `http://localhost:3000` | Runtime | Grafana external root URL | | `DC3_GRAFANA_PORT` | `3000` | Compose only | Grafana published port | | `DC3_KIBANA_PORT` | `5601` | Compose only | Kibana published port | | `DC3_ES_JAVA_OPTS` | `-Xms512m -Xmx512m` | Runtime | Elasticsearch JVM heap | | `DC3_LS_JAVA_OPTS` | `-Xms256m -Xmx256m` | Runtime | Logstash JVM heap | | `APM_AGENT_ENABLE` | `false` | Runtime | Whether the Java APM agent is on | ::: details Full Variable Reference (collapsed) #### Security & Authentication (Runtime) | Variable | Default | Purpose | |--------------------|------------------------------------------|------------------------------------| | `DC3_SECURITY_KEY` | `dc3.security.key.2026.io.github.pnoker` | Login token signing key | | `AUTH_HMAC_SECRET` | `io.github.pnoker.dc3` | `X-Auth-Principal` HMAC-SHA256 key | #### PostgreSQL | Variable | Default | Scope | |---------------------|-------------|--------------| | `POSTGRES_HOST` | `localhost` | Runtime | | `POSTGRES_PORT` | `35432` | Runtime | | `POSTGRES_USERNAME` | `dc3` | Runtime | | `POSTGRES_PASSWORD` | `dc3dc3dc3` | Runtime | | `POSTGRES_DB` | `dc3` | Runtime | | `POSTGRES_SCHEMA` | (unset) | Per-process | | `DC3_POSTGRES_PORT` | `35432` | Compose only | #### RabbitMQ | Variable | Default | Scope | |--------------------------------------------|--------------|--------------| | `RABBITMQ_HOST` | `localhost` | Runtime | | `RABBITMQ_PORT` | `35672` | Runtime | | `RABBITMQ_USERNAME` | `dc3` | Runtime | | `RABBITMQ_PASSWORD` | `dc3dc3dc3` | Runtime | | `RABBITMQ_VIRTUAL_HOST` | `dc3` | Runtime | | `RABBITMQ_MQTT_EXCHANGE` | `dc3.e.mqtt` | Runtime | | `RABBITMQ_SSL_ENABLED` | `false` | Runtime | | `RABBITMQ_SSL_ALGORITHM` | `TLS` | Runtime | | `RABBITMQ_SSL_VALIDATE_SERVER_CERTIFICATE` | `false` | Runtime | | `RABBITMQ_SSL_VERIFY_HOSTNAME` | `false` | Runtime | | `RABBITMQ_CONTAINER_PORT` | `5672` | Runtime | | `DC3_RABBITMQ_PORT` | `35672` | Compose only | | `DC3_RABBITMQ_TLS_PORT` | `35671` | Compose only | | `DC3_RABBITMQ_MANAGEMENT_PORT` | `15672` | Compose only | #### EMQX / MQTT | Variable | Default | Scope | |---------------------------|-------------|--------------| | `MQTT_BROKER_HOST` | `localhost` | Runtime | | `MQTT_BROKER_PORT` | `31883` | Runtime | | `MQTT_USERNAME` | `dc3` | Runtime | | `MQTT_PASSWORD` | `dc3dc3dc3` | Runtime | | `MQTT_BATCH_SPEED` | `100` | Runtime | | `MQTT_BATCH_INTERVAL` | `5` | Runtime | | `DC3_EMQX_WS_PORT` | `38083` | Compose only | | `DC3_EMQX_WSS_PORT` | `38084` | Compose only | | `DC3_EMQX_MQTT_PORT` | `31883` | Compose only | | `DC3_EMQX_MQTTS_PORT` | `38883` | Compose only | | `DC3_EMQX_DASHBOARD_PORT` | `18083` | Compose only | #### gRPC / facade | Variable | Default | Scope | |-------------------------------|-------------|--------------| | `CENTER_AUTH_HOST` | `localhost` | Runtime | | `CENTER_MANAGER_HOST` | `localhost` | Runtime | | `CENTER_DATA_HOST` | `localhost` | Runtime | | `CENTER_AGENTIC_HOST` | `localhost` | Runtime | | `DC3_FACADE_MODE` | `grpc` | Runtime | | `DC3_FACADE_GRPC_DEADLINE_MS` | `3000` | Runtime | | `DC3_AUTH_GRPC_PORT` | `9300` | Compose only | | `DC3_MANAGER_GRPC_PORT` | `9400` | Compose only | | `DC3_DATA_GRPC_PORT` | `9500` | Compose only | #### HTTP Gateway & Service Ports | Variable | Default | Scope | |----------------------------------|---------|--------------| | `DC3_GATEWAY_PORT` | `8000` | Compose only | | `DC3_AUTH_PORT` | `8300` | Compose only | | `DC3_MANAGER_PORT` | `8400` | Compose only | | `DC3_DATA_PORT` | `8500` | Compose only | | `DC3_AGENTIC_PORT` | `8600` | Compose only | | `SERVER_PORT` | (unset) | Per-process | | `GRPC_SERVER_PORT` | (unset) | Per-process | | `DC3_LISTENING_VIRTUAL_TCP_PORT` | `6270` | Compose only | | `DC3_LISTENING_VIRTUAL_UDP_PORT` | `6271` | Compose only | | `TCP_PORT` | (unset) | Per-process | | `UDP_PORT` | (unset) | Per-process | | `GATEWAY_ROUTE_AUTH_TOKEN_URI` | (unset) | Per-process | | `GATEWAY_ROUTE_AUTH_URI` | (unset) | Per-process | | `GATEWAY_ROUTE_MANAGER_URI` | (unset) | Per-process | | `GATEWAY_ROUTE_DATA_URI` | (unset) | Per-process | | `GATEWAY_ROUTE_AGENTIC_URI` | (unset) | Per-process | #### Agentic / AI (Runtime) | Variable | Default | |---------------------------------------|--------------------------------| | `AGENTIC_FALLBACK_OPENAI_BASE_URL` | `https://api.openai.com` | | `AGENTIC_FALLBACK_OPENAI_API_KEY` | (empty) | | `AGENTIC_FALLBACK_OPENAI_MODEL` | `gpt-4o` | | `AGENTIC_FALLBACK_OPENAI_TEMPERATURE` | `0.7` | | `AGENTIC_FALLBACK_OPENAI_MAX_TOKENS` | `2048` | | `AGENTIC_MEMORY_SCHEMA_INIT` | `never` | | `AGENTIC_MEMORY_ENABLED` | `false` | | `AGENTIC_MEMORY_MAX_MESSAGES` | `50` | | `AGENTIC_TOOL_CALLING_ENABLED` | `true` | | `AGENTIC_ATTACHMENT_STORAGE_PATH` | `dc3/data/agentic/attachments` | #### Batch Processing / Images / Observability | Variable | Default | Scope | |------------------------|-------------------------|--------------| | `POINT_BATCH_SPEED` | `100` | Runtime | | `POINT_BATCH_INTERVAL` | `5` | Runtime | | `REGISTRY` | `auto` | Compose only | | `DC3_IMAGE_REGISTRY` | `pnoker` | Compose only | | `DC3_IMAGE_TAG` | `2026.6` | Compose only | | `DC3_LOG_MAX_SIZE` | `10M` | Compose only | | `DC3_LOG_MAX_FILE` | `20` | Compose only | | `DC3_BIND_HOST` | `127.0.0.1` | Compose only | | `GF_SERVER_ROOT_URL` | `http://localhost:3000` | Runtime | | `DC3_GRAFANA_PORT` | `3000` | Compose only | | `DC3_KIBANA_PORT` | `5601` | Compose only | | `DC3_ES_JAVA_OPTS` | `-Xms512m -Xmx512m` | Runtime | | `DC3_LS_JAVA_OPTS` | `-Xms256m -Xmx256m` | Runtime | | `APM_AGENT_ENABLE` | `false` | Runtime | | `NODE_ENV` | `dev` | Runtime | ::: ## Constraints and Common Pitfalls - Editing `.env.example` does nothing at runtime — you must `cp .env.example .env` first. - `dc3/env/dev.env` and the root `.env` serve different purposes. They are not the same file; don't copy one into the other. - Use `DC3_*_PORT` consistently for service published ports. The process internals still use Spring Boot's native names such as `SERVER_PORT` and `GRPC_SERVER_PORT`. - Per-process variables (`POSTGRES_SCHEMA`, `SERVER_PORT`, `TCP_PORT`, and so on) are single-service overrides only — don't set them globally. - The Compose application stack can use a different `NODE_ENV` than local source runs. ## Further Reading - [Develop Locally from Source](./) — the full path of bringing up the stack, logging in, and running your first device end to end - [Deployment Modes and Image Sources](../guide/usage) — how the whole stack starts and how to use `REGISTRY=cn`