A polyglot microservices + observability lab designed to showcase modern container and operational best practices and a complete metrics / logs / traces pipeline you can run locally — with a structure that maps cleanly to cloud production patterns.
- Go / Gin →
golang-gin/ - Java / Spring Boot →
java-springboot/ - Python / Django →
python-django/
- Alloy (OpenTelemetry ingest + Docker log shipping)
- Prometheus + Alertmanager (metrics + alerting)
- Loki (logs)
- Tempo (traces + metrics-generator)
- Grafana (dashboards + Explore)
Tip
The canonical “run everything” entrypoints are the Docker Compose stacks under docker/README.md#tldr—canonical-entrypoints.
Important
Where to start (fast navigation)
- Want to run the stack? Start with
docker/README.md→ TL;DR — canonical entrypoints. - Want to understand the services? Read the module docs:
golang-gin/README.md,java-springboot/README.md,python-django/README.md. - Want pinned tooling + local checks? Jump to
#ci—.ci-tool-versions.shis the single source of truth for tool pins.
- What this repo demonstrates
- Why a hybrid observability approach
- Production template for cloud
- Quick start
- Endpoints
- Traffic generator
- Common operations
- System architecture
- Service contract
- Environments
- Secrets bootstrap
- CI
- Repository structure
- Versions
- Troubleshooting
- Status
- License
This project is intentionally built as a production-minded reference lab (not just a demo wiring exercise). It demonstrates:
- Production containerization patterns: multi-stage Dockerfiles, slim runtime stages, non-root execution, and build metadata injection (e.g.,
SERVICE_NAME,VERSION,BUILD_TIME). - Operator-facing hardening behaviors: explicit timeouts + graceful shutdown semantics (
SIGTERM), request/payload limits (clear413behavior), trusted proxy controls for correctX-Forwarded-*handling, and health/readiness checks enforced via Compose. - Composable environment “shapes” with strong local parity: development (apps only), integration (apps + observability), and staging (registry pulls / “build once, deploy many”) using modular Compose building blocks.
- Observability you can rely on: Prometheus scrape for metrics, JSON logs shipped via Alloy → Loki, and OTLP traces via Alloy → Tempo — correlated in Grafana.
- High-signal CI with pinned tooling and local confidence checks:
.ci-tool-versions.shis the single source of truth for pinned toolchains; it’s consumed by both.ci-local.shand.github/workflows/ci.yamlto reduce drift..ci-local.shruns a fixed set of gates locally (not an event simulator).
Note
For the implementation details of each item (Dockerfiles, Compose hardening, CI gates, and service-specific behaviors), see the per-module README.md docs.
This repo intentionally uses the best model per signal and correlates everything in Grafana:
- Metrics (Prometheus scrape):
/metricsscraping is simple, reliable, and well-suited for RED/USE-style service signals without forcing a push pipeline. - Traces (OTLP push): traces are naturally push-based; services export OTLP to Alloy, which batches/routes and forwards to Tempo.
- Logs (agent-based tailing): services emit structured JSON logs to stdout/stderr; Alloy tails container logs and ships them to Loki without per-app log agents.
Goal: simple where possible, centralized where it pays off, with clean correlation (request/trace/span IDs).
The repo is shaped so it can serve as a production reference template:
- Contracts map cleanly to orchestrators:
/healthand/readybecome liveness/readiness probes;/metricsremains the Prometheus boundary. - Signal boundaries remain stable: OTLP for traces, JSON logs for platform logging collection, and Prometheus scraping (or managed Prometheus equivalents).
- Staging mirrors real deployments: build once, deploy many via registry pulls.
It also bakes in reusable “production-shaped” service and runtime behaviors:
- Graceful shutdown + timeouts: clean
SIGTERMhandling and explicit shutdown budgets. - Safety limits by default: request/payload limits with clear
413behavior. - Trusted proxy controls: correct
X-Forwarded-*handling without spoofable client IP headers. - Hardened containers: non-root runtime plus Compose-level hardening (healthchecks, cap drops, read-only/tmpfs patterns where applicable).
- Build provenance:
/info+ OCI labels to expose version/build metadata, and repeatable dev → staging workflows.
You can swap Docker Compose for Kubernetes / ECS / Nomad while keeping the same operational shape and observability boundaries.
Important
This repo contains multiple Compose entrypoints. For the full matrix (dev vs. integration vs. staging, secrets vs. no-secrets), read docker/README.md#tldr—canonical-entrypoints.
| Type | Requirements |
|---|---|
| Runtime | Docker Engine / Docker Desktop (recent) + Docker Compose v2 |
| Host ports | 8081–8083, 3000, 9090, 9093, 3100, 3200, 4317, 4318, 12345 |
| Recommended | 4+ CPU, 6–8GB RAM, 10GB+ disk (volumes + images) |
Note
All Compose stacks are designed to bind host ports to 127.0.0.1 (localhost-only) by default.
This is the fastest on-ramp and mirrors “build once, deploy many.”
Because Compose is invoked with --project-directory docker, the natural place to keep env vars is docker/.env (do not commit).
cat > docker/.env <<'EOF'
APP_ENV=staging
APP_VERSION=2.0.2
REGISTRY=docker.io/johnjaysonlopez
EOFdocker compose --project-directory docker \
-p polyglot-lab-staging \
-f docker/compose.staging.nosecrets.yaml \
up --pull always --remove-orphansTip
Run detached: add -d to the up command.
curl -fsS http://127.0.0.1:8081/health >/dev/null && echo "svc-a OK"
curl -fsS http://127.0.0.1:8082/health >/dev/null && echo "svc-b OK"
curl -fsS http://127.0.0.1:8083/health >/dev/null && echo "svc-c OK"- Grafana:
http://127.0.0.1:3000 - Prometheus targets:
http://127.0.0.1:9090/targets
Grafana credentials depend on which Compose entrypoint you use:
- No-secrets stacks: Grafana typically uses upstream defaults
admin/adminunless overridden in Compose. - Secrets-enabled stacks: credentials come from Docker secrets created by
.bootstrap-local.sh.
Tip
If you can’t sign in, check docker/README.md#operational-knobs and/or inspect the running Grafana container configuration.
| Component | URL |
|---|---|
| Go (Gin) | http://127.0.0.1:8081 |
| Java (Spring Boot) | http://127.0.0.1:8082 |
| Python (Django) | http://127.0.0.1:8083 |
| Component | URL |
|---|---|
| Grafana | http://127.0.0.1:3000 |
| Prometheus | http://127.0.0.1:9090 |
| Alertmanager | http://127.0.0.1:9093 |
| Tempo (query/frontend) | http://127.0.0.1:3200 |
| Loki | http://127.0.0.1:3100 |
| Alloy (OTLP/gRPC ingest) | http://127.0.0.1:4317 |
| Alloy (OTLP/HTTP ingest) | http://127.0.0.1:4318 |
| Alloy UI/status | http://127.0.0.1:12345 |
If the system is idle, generate traffic so metrics / logs / traces populate quickly.
Tip
After running this, check Prometheus targets and Grafana Explore (Prometheus / Loki / Tempo).
Click to expand traffic generator script
TARGETS=(
"svc-a root (200)|http://127.0.0.1:8081/"
"svc-b root (200)|http://127.0.0.1:8082/"
"svc-c root (200)|http://127.0.0.1:8083/"
"svc-a WRONG /nope (404)|http://127.0.0.1:8081/nope"
"svc-b WRONG /nope (404)|http://127.0.0.1:8082/nope"
"svc-c WRONG /nope (404)|http://127.0.0.1:8083/nope"
"svc-a info (200)|http://127.0.0.1:8081/info"
"svc-b info (200)|http://127.0.0.1:8082/info"
"svc-c info (200)|http://127.0.0.1:8083/info"
"svc-a WRONG /infoo (404)|http://127.0.0.1:8081/asd"
"svc-b WRONG /infoo (404)|http://127.0.0.1:8082/asd"
"svc-c WRONG /infoo (404)|http://127.0.0.1:8083/asd"
)
REQUESTS_PER_TARGET=25
for entry in "${TARGETS[@]}"; do
IFS='|' read -r label url <<< "$entry"
echo "Hitting: $label -> $url x$REQUESTS_PER_TARGET"
for ((i=1; i<=REQUESTS_PER_TARGET; i++)); do
code="$(curl -sS -o /dev/null -w '%{http_code}' "$url" || echo '000')"
echo " [$i/$REQUESTS_PER_TARGET] HTTP $code"
done
doneThen validate:
- Prometheus targets:
http://127.0.0.1:9090/targets - Grafana:
http://127.0.0.1:3000→ Dashboards / Explore (Prometheus / Loki / Tempo)
Tip
For the full operator/runbook commands (including ps, logs, restart, and config rendering), see docker/README.md#common-operator-commands.
Click to expand quick common operations (root convenience)
[!TIP] The examples below use the staging / no-secrets entrypoint. Keep
-pand-fconsistent per run, so you’re operating on the same project.
docker compose --project-directory docker \
-p polyglot-lab-staging \
-f docker/compose.staging.nosecrets.yaml \
down --remove-orphansUse this if you want a clean slate for Prometheus/Loki/Tempo/Grafana state.
docker compose --project-directory docker \
-p polyglot-lab-staging \
-f docker/compose.staging.nosecrets.yaml \
down -v --remove-orphansdocker compose --project-directory docker \
-p polyglot-lab-staging \
-f docker/compose.staging.nosecrets.yaml \
psdocker compose --project-directory docker \
-p polyglot-lab-staging \
-f docker/compose.staging.nosecrets.yaml \
logs -f --tail=200[!NOTE] If you forget a service name,
docker compose ... psis the fastest way to discover it.
Note
Scope: this diagram represents integration/staging runs where observability is enabled. The apps-only development stack does not start Prometheus/Grafana/Loki/Tempo/Alertmanager/Alloy.
Click to expand architecture diagram
User/Operator
|
| HTTP (host ports; bound to 127.0.0.1)
v
Host Ports
|---- 127.0.0.1:8081 --------> golang-gin-app (container :8080)
|---- 127.0.0.1:8082 --------> java-springboot-app (container :8080)
|---- 127.0.0.1:8083 --------> python-django-app (container :8080)
|
|---- 127.0.0.1:3000 --------> Grafana
|---- 127.0.0.1:9090 --------> Prometheus
|---- 127.0.0.1:9093 --------> Alertmanager
|---- 127.0.0.1:3200 --------> Tempo query/frontend
|---- 127.0.0.1:3100 --------> Loki
|---- 127.0.0.1:4317 --------> Alloy (OTLP/gRPC ingest)
|---- 127.0.0.1:4318 --------> Alloy (OTLP/HTTP ingest; POST /v1/traces)
|---- 127.0.0.1:12345 -------> Alloy UI/status
METRICS:
Prometheus ---scrape /metrics---> Go/Gin app
Prometheus ---scrape /metrics---> Java/Spring Boot app
Prometheus ---scrape /metrics---> Python/Django app
Tempo --metrics-generator remote_write--> Prometheus
TRACES:
Go/Gin app ------------ OTLP ----\
Java/Spring Boot app -- OTLP -----+--> Alloy --> Tempo (trace store) <-- Tempo query/frontend
Python/Django app------ OTLP ----/
LOGS:
Docker daemon (container logs) --> Alloy --> Loki
DASHBOARDS:
Grafana --> Prometheus (metrics)
Grafana --> Loki (logs)
Grafana --> Tempo query/frontend (traces)
ALERTING:
Prometheus --> Alertmanager --> (optional) Telegram / other receivers
HEALTHCHECKS:
toolbox-init (busybox to shared volume) --> healthchecks --> services (apps, Prom, AM, TempoQ, Grafana)
toolbox-init— stages BusyBox into the sharedtoolboxvolume for healthchecksloki-init— prepares Loki runtime directories/permissions for the volume-backed storetempo-init— prepares Tempo runtime directories/permissions for the volume-backed store
For deeper breakdowns (compose entrypoints, overlays, healthchecks, and secrets workflow), see docker/README.md#compose-layout and docker/README.md#implementation-map-where-to-look.
All three services expose the same HTTP surface area:
| Endpoint | Meaning | Used by |
|---|---|---|
GET / |
Banner (“service is running”) | Humans / smoke checks |
GET /info |
Build/service metadata | Humans / dashboards |
GET /health |
Liveness probe | Orchestrator liveness checks |
GET /ready |
Readiness probe | Compose healthchecks / orchestrator readiness |
GET /metrics |
Prometheus metrics (text format) | Prometheus scraping / alerting |
Note
This consistency is intentional: it keeps health checks, scraping, dashboards, and alerts predictable in a polyglot stack.
This repo is deliberately structured around environment “shapes” that mirror real deployments:
- development — apps only; fastest inner-loop
- integration — apps + full observability (builds locally; typically no registry pulls)
- staging (prod-like) — pulls prebuilt images from a registry; mirrors production “build once, deploy many”
All authoritative run commands (including secrets-enabled stacks and overlays) live in:
Common entrypoints:
- Apps-only (local builds):
docker/compose.development.yaml - Apps + observability (no secrets overlays):
docker/compose.integration.nosecrets.yaml - Apps + observability (with secrets overlays):
docker/compose.integration.yaml - Staging pulls (no secrets / with secrets):
docker/compose.staging*.yaml
If you use the secrets-enabled Compose entrypoints:
docker/compose.integration.yamldocker/compose.staging.yaml
…run the bootstrap script once from the repo root. It creates the expected Docker secrets files in docker/secrets/ and sets ownership/permissions so Grafana/Alertmanager can read them safely.
GRAFANA_ADMIN_USERGRAFANA_ADMIN_PASSWORDTELEGRAM_BOT_TOKENTELEGRAM_CHAT_ID
./.bootstrap-local.sh --help # or: -h
export GRAFANA_ADMIN_USER=admin
export GRAFANA_ADMIN_PASSWORD='supersecret'
export TELEGRAM_BOT_TOKEN='...'
export TELEGRAM_CHAT_ID='...'
./.bootstrap-local.shTip
For the overlay mechanics and the exact secret file wiring, see docker/README.md#compose-layout
This repo uses GitHub Actions for quality gates (format/lint/tests/coverage) and security scanning.
- Main CI: runs on pushes, PRs, and release tags.
- On release tags (
v*), CI also builds images, scans them, and pushes/promotes them to Docker Hub.
- On release tags (
- Weekly Security: runs on a schedule (and can be triggered manually) to execute security scans regularly.
See:
.github/workflows/ci.yaml(main CI).github/workflows/weekly-security.yaml(scheduled security).github/actions/(shared composite actions)
Note
GitHub Actions only discovers workflows from .github/workflows/. If you’re viewing this project from an archive that strips dot-directories, double-check the repo still contains .github/workflows when pushed to GitHub.
To protect the integrity of CI itself, PRs that modify CI control files are blocked unless the PR author is listed in CI_MAINTAINERS (configured in ci.yaml).
Protected paths include:
.github/workflows/.github/actions/.ci-tool-versions.sh.ci-local.sh.bootstrap-local.sh
This guard is designed to be fork-safe and fail-closed: if the workflow cannot reliably compute the PR diff, it fails.
Note
Some security features are constrained on fork PRs due to GitHub permission rules (for example, uploading SARIF to Code Scanning may be skipped/blocked depending on the workflow inputs and PR source).
Note
.ci-local.sh is a local checker that runs a fixed set of high-signal gates (format/lint/test/coverage/security).
It does not simulate CI events (push/PR/schedule) and does not attempt to match GitHub Actions runtime details
(caching, SARIF upload, dependency review, release/tag behavior, etc.).
The GitHub workflows remain the source of truth for CI-only behavior.
./.ci-local.sh --help # or: -h
./.ci-local.sh # default: trivy_repo + go + java + python
./.ci-local.sh trivy_repo # repo scan only
./.ci-local.sh docker # docker builds + image scans only
./.ci-local.sh doctor all --summary # preflight checks (recommended)Tool pins live in: .ci-tool-versions.sh — the single source of truth consumed by .ci-local.sh and the GitHub Actions workflow (via .github/actions/load-tool-versions).
| Path | Purpose |
|---|---|
docker/ |
Compose stacks (apps-only + full observability), configs for Alloy/Prometheus/Loki/Tempo/Grafana/Alertmanager, secret overlays, staging pulls |
golang-gin/ |
Go + Gin service |
java-springboot/ |
Spring Boot service |
python-django/ |
Django service |
.github/workflows/ci.yaml |
Main CI workflow (quality gates + security + release tag builds/push) |
.github/workflows/weekly-security.yaml |
Scheduled security workflows (weekly scans + manual trigger) |
.github/actions/load-tool-versions/ |
Composite action: loads .ci-tool-versions.sh into GITHUB_ENV (strict allowlist + per-key validation) |
.bootstrap-local.sh |
Local bootstrap for secrets + permissions |
.ci-local.sh |
Local CI runner |
.ci-tool-versions.sh |
Tool/version pins (single source of truth) consumed by .ci-local.sh and GitHub Actions |
.gitignore |
Secret hygiene + build artifact ignores |
| Runtime / framework | Version |
|---|---|
| Go | 1.25.7 |
| Gin | v1.11.0 |
| Java | 25 |
| Spring Boot | 4.0.2 |
| Python | 3.12 |
| Django | 6.0.2 |
| Component | Image |
|---|---|
| Alloy | grafana/alloy:v1.12.2 |
| Prometheus | prom/prometheus:v3.9.1 |
| Alertmanager | prom/alertmanager:v0.31.0 |
| Loki | grafana/loki:3.6.4 |
| Tempo | grafana/tempo:2.10.0 |
| Grafana | grafana/grafana:12.3.2 |
Symptom: Compose fails with ports are already allocated.
Fix: stop the conflicting process, or change the host port mapping(s) in the relevant Compose file.
- Generate traffic (see
#traffic-generator). - Confirm Prometheus is scraping:
http://127.0.0.1:9090/targets - In Grafana, verify datasources are reachable (you can also inspect container logs).
Common causes:
- insufficient CPU/RAM assigned to Docker Desktop
- old Docker / Compose versions
- stale volumes from a previous run (try a full reset via
down -v)
Run the bootstrap step and ensure the required env vars are set.
See: #secrets-bootstrap
This is a personal lab project. There are no stability or backwards-compatibility guarantees: directory layout, APIs, and Docker tags may change at any time.
Use it as a reference or template at your own risk.
MIT — see LICENSE.