Home / Articles / A Central Config Repo for Microservices: One-Command Onboarding and Sync

This article is published in English.

A Central Config Repo for Microservices: One-Command Onboarding and Sync

Build a central config repository with setup, sync and daily update scripts so every NestJS and React service shares linting, editor and Docker settings.

2452 words

When a team runs a dozen Node.js microservices, the hardest part of a new hire's first week is often not the code but the environment: the right Node version, matching ESLint and Prettier rules, the correct .env values and a Docker setup that behaves like everyone else's. Each service tends to drift a little, and senior engineers end up acting as a help desk. This article shows how to fix that with a single configuration repository plus a few Bash scripts that bootstrap a machine in one command, copy shared settings into every service and keep them current. You will also see where the scripts as written need hardening before you rely on them.

How configuration drift creeps in

Imagine an experienced developer joining a team that owns twelve services. The first day disappears into installing Node and Docker, working out the house ESLint style, guessing which variables belong in which .env file and asking in chat which NestJS version the services use. Next, auth-service fails with a CORS error caused by a slightly different local setting, and later the first pull request fails CI because the shared Prettier configuration changed a month ago and the local copy never did.

None of these failures is dramatic, but together they are expensive. When every service carries its own slightly different lint rules, Dockerfiles, folder layout and editor settings, the cost shows up as lost days for newcomers and lost hours every week for the senior engineers answering setup questions.

One repository as the source of truth

The remedy is a dedicated repository, here called dev-environment-config, that owns everything a development machine needs: project templates, lint and formatting rules, VS Code settings, base Docker configuration, automation scripts and a shared library. When someone updates it, every developer machine picks up the change automatically the next working morning.

The overall shape looks like this, with the central repository feeding each service:

dev-environment-config (Central Repo)
├── templates/          → Project scaffolding (NestJS, React)
├── shared/             → ESLint, Prettier, Git configs
├── vscode/             → Extensions & settings
├── docker/             → Base Docker configs
├── scripts/            → Automation (setup, sync, update)
├── common-lib/         → Shared NPM package
└── manifest.json       → Version tracking

         │  syncs to
         ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ user-svc │ │ auth-svc │ │ pay-svc  │  ... 12 services
│ .eslint  │ │ .eslint  │ │ .eslint  │
│ .vscode  │ │ .vscode  │ │ .vscode  │
│ Docker   │ │ Docker   │ │ Docker   │
└──────────┘ └──────────┘ └──────────┘

Building the system step by step

The central repository layout

Inside the repository, each concern gets its own folder. Templates hold complete NestJS and React starters, shared/ holds dotfiles for ESLint, Prettier, EditorConfig, Git ignore rules and commitlint, vscode/ holds editor settings, and scripts/ holds the automation.

dev-environment-config/
├── templates/
│   ├── nestjs-service/    # Complete NestJS starter
│   └── reactjs-service/   # Complete React starter
├── shared/
│   ├── .eslintrc.js
│   ├── .prettierrc
│   ├── .editorconfig
│   ├── .gitignore
│   └── commitlint.config.js
├── vscode/
│   ├── extensions.json
│   ├── settings.json
│   └── launch.json
├── scripts/
│   ├── setup-new-developer.sh
│   ├── sync-config.sh
│   ├── auto-update.sh
│   └── create-new-service.sh
├── common-lib/            # Shared NPM package
└── manifest.json          # Version tracker

The manifest.json file is what makes the rest of the automation possible. It records an overall version, when and by whom it was last changed, versions for each configuration area and the list of services that belong to the workspace.

{
  "version": "1.2.0",
  "lastUpdated": "2024-01-15T10:00:00Z",
  "updatedBy": "senior-dev",
  "configs": {
    "eslint": "3.2.0",
    "prettier": "2.1.0",
    "docker": "1.5.0"
  },
  "services": [
    "user-service",
    "auth-service",
    "payment-service"
  ]
}

The rule is simple: any change to shared configuration bumps the top-level version. The update script later compares versions to decide whether there is anything to sync, so forgetting to bump it means the change silently never propagates. A CI check that fails when files under shared/ change without a version bump is a cheap safeguard.

One-command onboarding

The biggest win is the bootstrap script. A new developer installs VS Code and then runs a single command:

curl -sSL https://raw.githubusercontent.com/company/dev-environment-config/main/scripts/setup-new-developer.sh | bash

Roughly ten minutes later the workspace is ready. Piping a remote script straight into bash is convenient but deserves caution: the script runs with the user's permissions and whatever is on the branch at that moment. For an internal tool that is usually acceptable, yet many teams prefer to have people clone the repository and run the script locally so they can read it first.

Under the hood the script works through a fixed sequence:

#!/bin/bash
set -e

WORKSPACE_DIR="$HOME/company-workspace"
CONFIG_REPO="https://github.com/company/dev-environment-config.git"

echo "🚀 Setting up your development environment..."

# 1. Check & install prerequisites
check_and_install "git" "Git"
check_and_install "node" "Node.js" "nvm install 20"
check_and_install "docker" "Docker"

# 2. Create workspace & clone everything
mkdir -p "$WORKSPACE_DIR" && cd "$WORKSPACE_DIR"
git clone "$CONFIG_REPO"

# 3. Clone all service repos from manifest
SERVICES=$(cat dev-environment-config/manifest.json | \
  python3 -c "import sys,json; \
  [print(s) for s in json.load(sys.stdin)['services']]")

for service in $SERVICES; do
  git clone "https://github.com/company/${service}.git"
done

# 4. Install VS Code extensions
EXTENSIONS=$(cat dev-environment-config/vscode/extensions.json | \
  python3 -c "import sys,json; \
  [print(e) for e in json.load(sys.stdin)['recommendations']]")

for ext in $EXTENSIONS; do
  code --install-extension "$ext" --force
done

# 5. Sync shared configs to all services
bash dev-environment-config/scripts/sync-config.sh

# 6. Install dependencies everywhere
for service in $SERVICES; do
  cd "$service" && npm install && cp .env.example .env && cd ..
done

# 7. Set up daily auto-update cron (8 AM weekdays)
CRON="0 8 * * 1-5 cd $WORKSPACE_DIR && \
  bash dev-environment-config/scripts/auto-update.sh"
(crontab -l 2>/dev/null; echo "$CRON") | crontab -

echo "✅ Setup complete! Open VS Code: code $WORKSPACE_DIR"

A few details are worth noticing. set -e aborts on the first failing command, which prevents a half-configured workspace from looking complete. The helper check_and_install is not shown and has to be written per operating system. Parsing JSON through an inline Python one-liner works, but the update script later uses jq, so standardising on one tool reduces prerequisites. Copying .env.example gives safe defaults, but it only works if the example files are kept complete and contain no real secrets.

Syncing shared configuration into every service

The sync script loops over the services and copies the shared dotfiles and VS Code settings into each one, overwriting what is there.

#!/bin/bash
# sync-config.sh

CONFIG_ROOT="$(dirname "$SCRIPT_DIR")"
WORKSPACE_DIR="$(dirname "$CONFIG_ROOT")"

for service in $SERVICES; do
  SERVICE_DIR="$WORKSPACE_DIR/$service"

  # Copy shared configs
  cp -f "$CONFIG_ROOT/shared/.eslintrc.js" "$SERVICE_DIR/"
  cp -f "$CONFIG_ROOT/shared/.prettierrc" "$SERVICE_DIR/"
  cp -f "$CONFIG_ROOT/shared/.editorconfig" "$SERVICE_DIR/"
  cp -f "$CONFIG_ROOT/shared/.gitignore" "$SERVICE_DIR/"

  # Copy VS Code settings
  mkdir -p "$SERVICE_DIR/.vscode"
  cp -f "$CONFIG_ROOT/vscode/settings.json" "$SERVICE_DIR/.vscode/"
  cp -f "$CONFIG_ROOT/vscode/extensions.json" "$SERVICE_DIR/.vscode/"

  echo "✅ $service synced"
done

The snippet assumes SCRIPT_DIR and SERVICES are already defined, so the real script needs to derive them itself (for example from $0 and the manifest). The cp -f calls are deliberate: overwriting is how drift is removed. The trade-off is that any local edit to those files is lost on the next run, which is why per-service customisation has to live in separate files, as discussed below. Also consider whether a file like .gitignore should really be identical everywhere, since services often need their own entries.

The daily auto-update job

This is the piece that keeps machines aligned without anyone remembering to do anything. Each weekday at 8 AM the cron job runs auto-update.sh:

#!/bin/bash
# auto-update.sh — Runs daily at 8 AM

cd "$CONFIG_ROOT"

# 1. Check for config updates
OLD_VERSION=$(cat manifest.json | jq -r '.version')
git pull origin main
NEW_VERSION=$(cat manifest.json | jq -r '.version')

if [ "$OLD_VERSION" != "$NEW_VERSION" ]; then
  echo "🆕 Config updated: $OLD_VERSION → $NEW_VERSION"
  bash "$SCRIPT_DIR/sync-config.sh"
fi

# 2. Pull latest code for all services
for service in $SERVICES; do
  cd "$WORKSPACE_DIR/$service"

  # Stash local changes safely
  git stash push -m "auto-update-$(date +%Y%m%d)" 2>/dev/null

  # Pull latest
  git pull origin $(git branch --show-current) --rebase

  # Update dependencies if package.json changed
  if git diff HEAD~1 --name-only | grep -q "package.json"; then
    npm install
  fi

  # Update shared library
  npm update @company/common-lib
done

# 3. Notify the developer
notify-send "Dev Environment" "✅ Updated to config v$NEW_VERSION"

Treat this script as a starting point rather than something to deploy unchanged:

  • Changes are stashed but never popped, so a developer who left work uncommitted overnight has to know to run git stash pop; otherwise the work appears to vanish.
  • git diff HEAD~1 only inspects the last commit. If the pull brought in several commits, a package.json change in an earlier one is missed; comparing against the commit you had before pulling is more reliable.
  • A rebase that hits conflicts leaves the repository mid-rebase with no one watching. Running git pull --ff-only and skipping the service on failure is safer for an unattended job.
  • notify-send exists only on Linux desktops, and cron does not run on a machine that is asleep at 8 AM, so macOS users may need launchd or a different trigger.

The end-to-end flow connects a senior engineer's push to every developer's next morning:

Senior Dev pushes config change
         │
         ▼
   GitHub Actions
         │
    ┌────┴────┐
    │ Slack   │ Notify team
    │ Publish │ common-lib to NPM
    └────┬────┘
         │
    Next 8 AM
         │
         ▼
  Every developer's cron runs
         │
    ┌────┴────────────────┐
    │ Pull config repo    │
    │ Sync shared configs │
    │ Pull service repos  │
    │ Update dependencies │
    │ Desktop notification│
    └─────────────────────┘

A shared library instead of copy-paste

Guards, interceptors, exception filters and DTOs tend to be duplicated across services and then diverge. Packaging them as a private npm module, @company/common-lib, gives each service a single import:

// In any service, just import and use:
import {
  AuthGuard,
  RolesGuard,
  ResponseInterceptor,
  LoggingInterceptor,
  HttpExceptionFilter,
  PaginationDto,
  McpService,        // Qwen MCP integration
} from '@company/common-lib';

@Controller('users')
@UseGuards(AuthGuard, RolesGuard)
@UseInterceptors(ResponseInterceptor)
@UseFilters(HttpExceptionFilter)
export class UserController {
  constructor(private mcpService: McpService) {}

  @Get()
  findAll(@Query() pagination: PaginationDto) {
    return this.userService.findAll(pagination);
  }

  @Post('analyze')
  async analyze(@Body() dto: AnalyzeDto) {
    return this.mcpService.callTool('text-analysis', {
      text: dto.text,
    });
  }
}

A controller can apply the shared authentication and role guards, the response interceptor and the HTTP exception filter, accept a common pagination DTO and use an McpService wrapper for a Qwen MCP integration. When the library gains a new guard or a fix to the exception filter, the auto-update job pulls it into every service.

Be aware that npm update respects the semver range in each package.json, so a major release will not be picked up automatically, which is usually what you want. Shared libraries also create coupling between services; the article on when shared code should exist covers how to decide what belongs in one.

Scaffolding new services

Creating a new service used to mean a day of copying an existing one and deleting what did not apply. A generator script turns that into a short interactive prompt:

bash scripts/create-new-service.sh

# Prompts:
# Service name: order-service
# Description: Handles order processing
# Port: 3002
# Type: nestjs

# ✅ Done! Complete service with:
# - src/common/ (filters, guards, interceptors, DTOs)
# - Docker setup (Dockerfile + docker-compose)
# - VS Code settings
# - ESLint + Prettier
# - Health check endpoint
# - Swagger docs
# - MCP integration
# - Auto-update scripts

Because every new service starts from the same template, it begins life without drift.

What the team gains

A team that adopted this setup reported these effects:

  • Onboarding in minutes: one command replaced a forty-step wiki page, cutting time to running every service from about three days to roughly fifteen minutes, and setup questions in chat all but disappeared.
  • No more drift: services had quietly diverged (ESLint 8 in one, 7 in another, stale Dockerfiles); scheduled overwrites keep them identical.
  • Senior time back: around an hour a day of setup support and formatting-only CI failures, about five hours a week, returned to feature work.
  • Consistent quality signals: the same lint fixes and formatting on save, Husky-enforced commit messages and auto-installed extensions, so reviews discuss design rather than whitespace.
  • Updates without announcements: a new rule, Docker base image, extension or library fix reaches everyone the next day.
  • Predictable structure: the same src/common/ patterns, Docker setup and health check path in every service.

Lessons for rolling it out

Start with the smallest useful slice

There is no need to build all of this at once. Syncing only ESLint and Prettier settings delivers visible value quickly. Docker configuration, editor settings, the shared library and the scheduled updates can follow once the team trusts the approach.

Leave room for local overrides

A shared baseline should not be a straitjacket. A service that needs an extra rule can add a .eslintrc.local.js that extends the shared configuration; because the sync script never touches that file, the override survives every run.

Make updates visible

The desktop notification seems like a minor touch, but it tells developers that the system exists and that something changed. Silent updates tend to confuse people when their tooling behaves differently overnight.

Version everything

Without the manifest version, the update job cannot tell whether anything changed and would resync on every run. Versioning also gives you something concrete to reference when a change causes trouble.

The stack behind the example

The example setup combined NestJS and React (both in TypeScript), Qwen accessed through MCP, Docker and Docker Compose, Node.js 20 LTS, PostgreSQL 16, Redis 7, GitHub Actions, VS Code, and Bash plus cron for synchronisation. Those versions reflect the setup at the time; check current LTS releases before copying them.

Key takeaways

  • Once you have more than a handful of services and developers, a central configuration repository pays for itself quickly.
  • The core pieces are a config repo with templates and shared files, a one-command setup script, a sync script, a scheduled updater, a shared npm library and a service generator.
  • Overwriting shared files is what eliminates drift, so design explicit override files from day one.
  • Unattended Git operations are the riskiest part; prefer fast-forward pulls, restore stashes and surface failures instead of hiding them.
  • The payoff is that developers spend their time on product code rather than on reconstructing each other's environments.