Node.js Integration
Consul Guardian runs as a sidecar next to your Node.js application. Your app reads configuration from Consul KV using the consul npm package or HTTP calls. Guardian independently watches those same keys, commits every change to Git, and lets you restore any key to a previous value.
No code changes are required in your Node.js application.
How Node.js apps typically read from Consul
| Package | Description |
|---|---|
| consul | Full-featured Consul client for Node.js |
node-fetch / built-in fetch | Direct HTTP calls to Consul API |
Example: consul npm package
npm install consul
const Consul = require("consul");
const consul = new Consul({ host: "consul", port: 8500 });
async function getConfig(key) {
const result = await consul.kv.get(key);
return result ? result.Value : null;
}
async function loadConfig() {
return {
dbHost: await getConfig("config/myapp/db_host"),
dbPort: parseInt(await getConfig("config/myapp/db_port"), 10),
cacheEnabled: (await getConfig("config/myapp/cache_enabled")) === "true",
maxRetries: parseInt(await getConfig("config/myapp/max_retries"), 10) || 3,
};
}
Example: Express app
const express = require("express");
const Consul = require("consul");
const app = express();
const consul = new Consul({ host: "consul", port: 8500 });
let config = {};
async function refreshConfig() {
const keys = [
"config/myapp/db_host",
"config/myapp/db_port",
"config/myapp/api_timeout",
];
for (const key of keys) {
const result = await consul.kv.get(key);
if (result) {
const shortKey = key.split("/").pop();
config[shortKey] = result.Value;
}
}
}
// Load config at startup, refresh every 30s
refreshConfig();
setInterval(refreshConfig, 30000);
app.get("/health", (req, res) => {
res.json({ status: "ok", config });
});
app.listen(3000);
Example: NestJS with Consul
// config.service.ts
import { Injectable, OnModuleInit } from "@nestjs/common";
import * as Consul from "consul";
@Injectable()
export class ConfigService implements OnModuleInit {
private consul: Consul.Consul;
private cache: Record<string, string> = {};
constructor() {
this.consul = new Consul({ host: "consul", port: 8500 });
}
async onModuleInit() {
await this.loadAll();
}
async loadAll() {
const keys = await this.consul.kv.keys("config/myapp/");
for (const key of keys) {
const result = await this.consul.kv.get(key);
if (result) {
this.cache[key] = result.Value;
}
}
}
get(key: string): string | undefined {
return this.cache[`config/myapp/${key}`];
}
}
Adding Guardian
Docker Compose
version: "3.8"
services:
consul:
image: hashicorp/consul:1.17
ports:
- "8500:8500"
command: agent -dev -client=0.0.0.0
myapp:
build: .
ports:
- "3000:3000"
environment:
- CONSUL_HOST=consul
- CONSUL_PORT=8500
depends_on:
- consul
guardian:
image: ghcr.io/consul-guardian/consul-guardian:latest
environment:
- CONSUL_GUARDIAN_CONSUL_ADDRESS=http://consul:8500
- CONSUL_GUARDIAN_WATCH_PREFIXES=config/,feature-flags/
- CONSUL_GUARDIAN_GIT_REPO_PATH=/data/repo
volumes:
- guardian-data:/data
depends_on:
- consul
volumes:
guardian-data:
PM2 + Consul scenario
If you run Node.js with PM2 in production and read config from Consul, Guardian fits into the same infrastructure:
version: "3.8"
services:
consul:
image: hashicorp/consul:1.17
command: agent -dev -client=0.0.0.0
myapp:
build: .
command: pm2-runtime ecosystem.config.js
environment:
- CONSUL_HOST=consul
depends_on:
- consul
guardian:
image: ghcr.io/consul-guardian/consul-guardian:latest
environment:
- CONSUL_GUARDIAN_CONSUL_ADDRESS=http://consul:8500
- CONSUL_GUARDIAN_WATCH_PREFIXES=config/,feature-flags/
- CONSUL_GUARDIAN_GIT_REPO_PATH=/data/repo
volumes:
- guardian-data:/data
depends_on:
- consul
volumes:
guardian-data:
Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myregistry/myapp:latest
ports:
- containerPort: 3000
env:
- name: CONSUL_HOST
value: "consul.consul.svc.cluster.local"
- name: CONSUL_PORT
value: "8500"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: consul-guardian
spec:
replicas: 1
selector:
matchLabels:
app: consul-guardian
template:
metadata:
labels:
app: consul-guardian
spec:
containers:
- name: guardian
image: ghcr.io/consul-guardian/consul-guardian:latest
env:
- name: CONSUL_GUARDIAN_CONSUL_ADDRESS
value: "http://consul.consul.svc.cluster.local:8500"
- name: CONSUL_GUARDIAN_WATCH_PREFIXES
value: "config/,feature-flags/"
- name: CONSUL_GUARDIAN_GIT_REPO_PATH
value: "/data/repo"
volumeMounts:
- name: guardian-data
mountPath: /data
volumes:
- name: guardian-data
persistentVolumeClaim:
claimName: guardian-pvc
Restore scenario
A deploy script accidentally overwrites config/myapp/api_timeout from "5000" to "5" (5ms instead of 5 seconds). Every outbound API call starts timing out.
1. Find what happened
consul-guardian log --key config/myapp/api_timeout
# 2024-03-15T13:22:00Z bad1234 config/myapp/api_timeout modified
# 2024-03-01T10:00:00Z good567 config/myapp/api_timeout modified
2. Restore
consul-guardian restore --key config/myapp/api_timeout --commit good567
3. Verify
consul kv get config/myapp/api_timeout
# 5000 ← correct value restored
If your app refreshes config on an interval (like the Express example above), it picks up the fix automatically. If it reads config only at startup, restart the process or send a signal.
Zero code changes
Guardian does not add a dependency to your package.json, does not require middleware in Express or NestJS, and does not proxy your Consul reads. It is a separate process that watches Consul KV independently.