Production-grade Rust microservice skeleton with lifecycle management, observability, and Docker support.
Living document. This bootstrap evolves as better workflows and patterns are discovered. If you spot something that could be improved — a better pattern, a missing skill, a sharper rule — open an MR or leave a comment. Contributions welcome.
crates/
├── app/ # Binary crate — CLI, config, bootstrap wiring
│ └── src/
│ ├── main.rs # Thin entrypoint: parse CLI → bootstrap → run
│ ├── lib.rs # Bootstrap: health + metrics + tracing wiring
│ └── metrics.rs # Prometheus metrics, system & tokio collectors
├── app-core/ # Library crate — lifecycle framework
│ └── src/
│ ├── lib.rs # AppManager trait, RunningStatus, SubsystemHandle re-export
│ ├── manager.rs # App orchestrator (tokio-graceful-shutdown backend)
│ └── health.rs # Health HTTP endpoint (actix-web)
│
│ The crates below are example domain crates. Rename them to match your
│ service (e.g. replace "example" with "reporter", "ingestor", etc.).
│
├── example-config/ # Domain config structs (AppConfig, HealthConfig, …)
├── example-core/ # Domain error types and shared primitives
├── example-store/ # Storage trait + mockall feature-gate pattern
└── example-service/ # Domain logic wired to the store via trait abstraction
- Lifecycle management —
AppManagertrait backed bytokio-graceful-shutdown, subsystems as async functions - Health endpoint — actix-web on
:8080/health, backed byRunningStatus(200 when ready, 503 otherwise) - Prometheus metrics — actix-web on
:9090/metrics, usingmetricscrate +metrics-exporter-prometheus - System metrics — CPU and memory gauges via
sysinfo - Tokio runtime metrics — workers, parks, steals, polls, busy duration, queue depths via
tokio-metrics - OpenTelemetry tracing — optional OTLP/gRPC export via
opentelemetry-otlp+tonic - Structured logging —
tracing-subscriberwith env-filter, optional JSON output - CLI —
clapwith flags for ports, log level, OTel toggle, OTLP endpoint - Signal handling — SIGINT/SIGTERM handled automatically by
Toplevel::catch_signals() - Production lints — 11 clippy lints +
unsafe_code = "deny" - Pre-commit hooks — fmt + clippy on push
# Install pre-commit (once, if not already installed)
brew install pre-commit # macOS
# pip install pre-commit # alternative via pip
# Install pre-commit hooks into this repo
just pre-commit-install
# Build
just build
# Run locally
just run
# Check health & metrics
just health
just metrics
# Run CI checks locally
just ci| Port | Purpose |
|---|---|
| 8080 | Health check endpoint (/health) |
| 9090 | Prometheus metrics (/metrics) |
Returns 200 OK when the service is ready, 503 Service Unavailable otherwise.
curl http://localhost:8080/health
# OKPrometheus exposition format. Includes system and Tokio runtime metrics:
curl http://localhost:9090/metrics| Metric | Description |
|---|---|
system_cpu_usage_percent |
Overall CPU usage |
system_memory_used_bytes |
Physical memory in use |
system_memory_total_bytes |
Total physical memory |
tokio_workers_count |
Number of worker threads |
tokio_total_busy_duration_seconds |
Time workers spent executing tasks |
tokio_total_polls_count |
Total task polls across workers |
tokio_total_park_count |
Times worker threads parked |
tokio_total_steal_count |
Tasks stolen between workers |
tokio_global_queue_depth |
Tasks in the global queue |
tokio_total_local_queue_depth |
Tasks in all worker local queues |
tokio_blocking_queue_depth |
Tasks in the blocking pool queue |
tokio_budget_forced_yield_count |
Forced yields due to budget |
These metrics can be scraped by Prometheus and visualized in Grafana.
# Build image
just docker-build
# Start local stack (app + config)
just stack-up
# View logs
just stack-logs
# Stop and clean up
just stack-resetThis bootstrap includes ready-to-use AI context for both Cursor and Claude Code. Everything is self-contained — no global config required.
.cursor/rules/project.mdc is automatically loaded when you open the project.
It includes working-style rules, code style conventions, and project context.
Update the Project Context and Project-Specific Rules sections to reflect
your service after renaming the example-* crates.
CLAUDE.md at the repo root is automatically loaded by Claude Code.
Same content as the Cursor rule — working-style, QA, code style, and project context.
Three skills are available in .claude/skills/rust/:
| Skill | Trigger keywords | Purpose |
|---|---|---|
cargo-check |
"check rust", "cargo check", "run clippy", "before commit" | Run check + clippy + tests + fmt |
add-dependency |
"add crate", "add dependency", "cargo add" | Add crate with version lookup and features |
fix-error |
"fix error", "rust error", "compiler error" | Analyze and fix Rust compiler errors |
If you maintain a global ~/.claude/CLAUDE.md, you can slim down the project
CLAUDE.md to just the Project Context and Project-Specific Rules sections,
replacing the working-style content with @~/.claude/CLAUDE.md.
The rules in CLAUDE.md and project.mdc are intentionally generic. For
domain-specific work, add a section to the Project-Specific Rules block that
layers constraints on top of the generic ones.
For example, a financial service might add:
## Domain Rules — Finance
# Nick Leeson bankrupted a 233-year-old bank because nobody asked obvious questions.
# These rules exist so we have to ask them.
**Canonical crates**: Use [`rust_decimal`](https://crates.io/crates/rust_decimal)
for all monetary and decimal values — never `f32`/`f64`. Use
[`chrono`](https://crates.io/crates/chrono) for dates and times with explicit
timezone handling.
**Units and precision**: Never assume units (basis points, dollars, cents, percent).
Never assume decimal precision. Always confirm before implementing any numeric
type or calculation.
**No silent rounding**: Any rounding, truncation, or precision loss must be
explicit and confirmed. Flag it as a decision point, not an implementation detail.
**Audit trail**: Any function that mutates state or touches external systems
(DB, API, queue) must be discussed before writing. Consider whether it needs
logging or an audit record.
**Assumptions**: The bar for silent assumptions is zero.
If it touches money, positions, risk, or counterparties — ask.
If it compiles but feels wrong — ask.
If you're about to write a comment saying "this should be fine" — ask.The same pattern applies to any domain — canonical crates, invariants, and constraints that are specific to your context.
| Tool | Version |
|---|---|
| Rust | 1.85 (pinned via rust-toolchain.toml) |
| Edition | 2024 |
| Runtime | tokio (multi-thread, tokio_unstable enabled) |
Run just to see all available recipes.