Add tempo + portfolio

This commit is contained in:
2026-07-25 15:54:53 +00:00
parent e13bfe4502
commit cc2df9ea6b
8 changed files with 643 additions and 3 deletions
+1 -1
View File
@@ -7,5 +7,5 @@
"build": { "build": {
"dockerfile": "Dockerfile" "dockerfile": "Dockerfile"
}, },
"postAttachCommand": "docker context create prod --docker \"host=ssh://user@62.210.212.10\"" "postCreateCommand": "docker context create prod --docker \"host=ssh://user@62.210.212.10\""
} }
+2 -1
View File
@@ -13,7 +13,8 @@ qpismont.fr {
} }
www.qpismont.fr { www.qpismont.fr {
reverse_proxy my_cv:80 root * /www/html
file_server
} }
gitea.qpismont.fr { gitea.qpismont.fr {
+5
View File
@@ -10,6 +10,8 @@ services:
configs: configs:
- source: caddy_config - source: caddy_config
target: /etc/caddy/Caddyfile target: /etc/caddy/Caddyfile
- source: caddy_portfolio_html
target: /www/html/index.html
volumes: volumes:
- reversecaddydata:/data - reversecaddydata:/data
- reversecaddyconfig:/config - reversecaddyconfig:/config
@@ -152,6 +154,9 @@ configs:
wireguard_coredns_config: wireguard_coredns_config:
name: wireguard_coredns_config_${DATETIME} name: wireguard_coredns_config_${DATETIME}
file: ./wireguard/CoreDns file: ./wireguard/CoreDns
caddy_portfolio_html:
name: caddy_portfolio_html_${DATETIME}
file: ./portfolio/index.html
volumes: volumes:
giteadata: giteadata:
+17
View File
@@ -1,6 +1,19 @@
version: "3" version: "3"
services: services:
tempo:
image: grafana/tempo:2.10.7
command:
- -config.file=/etc/tempo/tempo.yml
configs:
- source: tempo_config
target: /etc/tempo/tempo.yml
volumes:
- tempodata:/var/tempo
networks:
- prometheus_network
- wireguard_network
perses: perses:
image: persesdev/perses:latest image: persesdev/perses:latest
volumes: volumes:
@@ -94,12 +107,16 @@ configs:
otel_collector_config: otel_collector_config:
name: otel_collector_config_${DATETIME} name: otel_collector_config_${DATETIME}
file: ./otel-collector/config.yaml file: ./otel-collector/config.yaml
tempo_config:
name: tempo_config_${DATETIME}
file: ./tempo/tempo.yml
volumes: volumes:
persesdata: persesdata:
prometheusdata: prometheusdata:
ntfydata: ntfydata:
alertmanagerdata: alertmanagerdata:
tempodata:
networks: networks:
reverse_network: reverse_network:
+7 -1
View File
@@ -27,10 +27,16 @@ processors:
exporters: exporters:
prometheusremotewrite: prometheusremotewrite:
endpoint: http://prometheus:9090/api/v1/write endpoint: http://prometheus:9090/api/v1/write
otlphttp/tempo:
endpoint: http://tempo:4318
service: service:
pipelines: pipelines:
metrics: metrics:
receivers: [otlp, prometheus] receivers: [otlp, prometheus]
processors: [memory_limiter, batch] processors: [memory_limiter, batch]
exporters: [prometheusremotewrite] exporters: [prometheusremotewrite]
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlphttp/tempo]
+586
View File
@@ -0,0 +1,586 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Portfolio of a platform / infrastructure engineer: distributed systems, observability, security and self-hosted CI/CD, tested in production.">
<title>Portfolio — Platform Engineer</title>
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:title" content="Portfolio — Platform Engineer">
<meta property="og:description" content="Self-hosted infrastructure running in production, a security-first AI agent in Rust, and a distributed media pipeline. Source code available.">
<style>
:root {
--bg: #0d1117;
--bg-soft: #161b22;
--bg-card: #161b22;
--border: #21262d;
--border-hover: #30363d;
--text: #e6edf3;
--text-dim: #8b949e;
--text-faint: #6e7681;
--accent: #58a6ff;
--accent-soft: rgba(88, 166, 255, 0.1);
--green: #3fb950;
--mono: 'SF Mono', 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--sans);
line-height: 1.65;
font-size: 16px;
-webkit-font-smoothing: antialiased;
}
.wrap {
max-width: 860px;
margin: 0 auto;
padding: 0 24px;
}
/* ---------- NAV ---------- */
nav {
position: sticky;
top: 0;
z-index: 100;
background: rgba(13, 17, 23, 0.8);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
}
nav .wrap {
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
}
nav .logo {
font-family: var(--mono);
font-weight: 600;
font-size: 15px;
color: var(--text);
}
nav .logo span { color: var(--accent); }
nav .links { display: flex; gap: 28px; }
nav a {
color: var(--text-dim);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
nav a:hover { color: var(--text); }
@media (max-width: 620px) { nav .links { display: none; } }
/* ---------- HERO ---------- */
header {
padding: 100px 0 80px;
border-bottom: 1px solid var(--border);
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--mono);
font-size: 13px;
color: var(--green);
background: rgba(63, 185, 80, 0.1);
border: 1px solid rgba(63, 185, 80, 0.2);
padding: 5px 12px;
border-radius: 20px;
margin-bottom: 28px;
}
.badge .dot {
width: 7px; height: 7px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 8px var(--green);
}
header h1 {
font-size: clamp(2rem, 5vw, 3rem);
line-height: 1.15;
letter-spacing: -0.02em;
margin-bottom: 20px;
font-weight: 700;
}
header h1 .grad {
background: linear-gradient(120deg, #58a6ff, #a371f7);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
header p.lead {
font-size: 1.15rem;
color: var(--text-dim);
max-width: 640px;
margin-bottom: 32px;
}
.cta {
display: flex;
gap: 14px;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
border: 1px solid transparent;
}
.btn-primary {
background: var(--accent);
color: #0d1117;
}
.btn-primary:hover { background: #79b8ff; transform: translateY(-1px); }
.btn-ghost {
background: transparent;
color: var(--text);
border-color: var(--border-hover);
}
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
/* ---------- SECTIONS ---------- */
section { padding: 80px 0; border-bottom: 1px solid var(--border); }
.section-head {
display: flex;
align-items: baseline;
gap: 12px;
margin-bottom: 40px;
}
.section-head .num {
font-family: var(--mono);
font-size: 14px;
color: var(--accent);
}
.section-head h2 {
font-size: 1.6rem;
letter-spacing: -0.01em;
font-weight: 600;
}
.section-intro {
color: var(--text-dim);
max-width: 640px;
margin: -24px 0 40px;
}
/* ---------- ABOUT ---------- */
.about-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
}
@media (max-width: 620px) { .about-grid { grid-template-columns: 1fr; gap: 28px; } }
.about-grid p { color: var(--text-dim); margin-bottom: 16px; }
.about-grid p strong { color: var(--text); font-weight: 500; }
.principles { list-style: none; }
.principles li {
padding: 10px 0 10px 24px;
position: relative;
color: var(--text-dim);
border-bottom: 1px solid var(--border);
}
.principles li:last-child { border-bottom: none; }
.principles li::before {
content: "→";
position: absolute;
left: 0;
color: var(--accent);
}
/* ---------- PROJECTS ---------- */
.project {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 32px;
margin-bottom: 24px;
transition: border-color 0.25s, transform 0.25s;
}
.project:hover {
border-color: var(--border-hover);
transform: translateY(-2px);
}
.project-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.project h3 {
font-size: 1.3rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.project .tag {
font-family: var(--mono);
font-size: 11px;
padding: 3px 10px;
border-radius: 20px;
background: var(--accent-soft);
color: var(--accent);
border: 1px solid rgba(88,166,255,0.2);
white-space: nowrap;
}
.tag.wip { background: rgba(210,153,34,0.1); color: #d29922; border-color: rgba(210,153,34,0.2); }
.tag.done { background: rgba(63,185,80,0.1); color: var(--green); border-color: rgba(63,185,80,0.2); }
.project .subtitle {
color: var(--text-dim);
font-size: 0.98rem;
margin-bottom: 24px;
}
.block { margin-bottom: 20px; }
.block-label {
font-family: var(--mono);
font-size: 12px;
color: var(--text-faint);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 8px;
}
.block p, .block ul { color: var(--text-dim); font-size: 0.95rem; }
.block ul { list-style: none; }
.block ul li {
padding: 4px 0 4px 20px;
position: relative;
}
.block ul li::before {
content: "▹";
position: absolute;
left: 0;
color: var(--accent);
font-size: 0.85em;
}
.block code {
font-family: var(--mono);
font-size: 0.85em;
background: var(--bg-soft);
padding: 2px 6px;
border-radius: 4px;
color: var(--accent);
border: 1px solid var(--border);
}
.stack {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--border);
}
.stack span {
font-family: var(--mono);
font-size: 12px;
color: var(--text-dim);
background: var(--bg-soft);
padding: 4px 10px;
border-radius: 6px;
border: 1px solid var(--border);
}
/* ---------- SKILLS ---------- */
.skills-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
}
.skill-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 22px;
}
.skill-card h4 {
font-size: 0.95rem;
margin-bottom: 12px;
color: var(--text);
display: flex;
align-items: center;
gap: 8px;
}
.skill-card h4 .ic { color: var(--accent); }
.skill-card p { color: var(--text-dim); font-size: 0.9rem; }
/* ---------- CONTACT ---------- */
#contact { text-align: center; border-bottom: none; }
#contact h2 { justify-content: center; }
#contact .section-head { justify-content: center; }
#contact p { color: var(--text-dim); max-width: 500px; margin: 0 auto 32px; }
.contact-links { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
/* ---------- FOOTER ---------- */
footer {
padding: 40px 0;
text-align: center;
color: var(--text-faint);
font-size: 13px;
font-family: var(--mono);
border-top: 1px solid var(--border);
}
/* Reveal animation */
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.visible { opacity: 1; transform: translateY(0); }
/* Accessibility: no animation if the user prefers reduced motion */
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.reveal { opacity: 1; transform: none; transition: none; }
.project:hover, .btn-primary:hover { transform: none; }
}
</style>
</head>
<body>
<nav>
<div class="wrap">
<div class="logo">~/<span>quentin_pismont</span></div>
<div class="links">
<a href="#about">About</a>
<a href="#projects">Projects</a>
<a href="#skills">Skills</a>
<a href="#contact">Contact</a>
</div>
</div>
</nav>
<header>
<div class="wrap">
<div class="badge"><span class="dot"></span> Open to opportunities</div>
<h1>I design systems<br><span class="grad">end to end</span>, built to hold up in production.</h1>
<p class="lead">
Platform / infrastructure engineer. From code to production, through CI/CD,
observability and security — an integrated ecosystem designed to be operated
over the long run.
</p>
<div class="cta">
<a href="#projects" class="btn btn-primary">View projects →</a>
<a href="https://gitea.qpismont.fr/explore/repos" target="_blank" class="btn btn-ghost">Gitea / Source code</a>
</div>
</div>
</header>
<!-- ABOUT -->
<section id="about">
<div class="wrap">
<div class="section-head reveal">
<span class="num">01</span>
<h2>About</h2>
</div>
<div class="about-grid reveal">
<div>
<p>
Developer for <strong>over 20 years</strong> (first lines of C at age 10),
working as an engineer for <strong>10 years</strong>. My specialty: designing coherent
distributed systems and keeping them operable over time.
</p>
<p>
What I show here is infrastructure
<strong>actually running in production</strong> for my daily use, <strong>tested under failure</strong>,
with full observability and an end-to-end CI/CD chain.
</p>
<p>
On the people side, I <strong>mentored two interns from the ground up</strong> on our stack —
both were hired at the end of their internship. Helping others level up
is part of how I approach the craft.
</p>
<p>
My areas of focus: <strong>Rust</strong>, container orchestration,
security / isolation, networking, and self-hosted observability.
</p>
</div>
<div>
<div class="block-label">Principles</div>
<ul class="principles">
<li>If it hasn't been tested under failure, it isn't resilient.</li>
<li>A technical choice is justified by its tradeoffs, not by trends.</li>
<li>Migrate for value, never for dogma.</li>
<li>Security is designed starting from the threat model.</li>
<li>A system must be observable before it's scaled.</li>
</ul>
</div>
</div>
</div>
</section>
<!-- PROJECTS -->
<section id="projects">
<div class="wrap">
<div class="section-head reveal">
<span class="num">02</span>
<h2>Selected projects</h2>
</div>
<p class="section-intro reveal">
Three pieces that, together, cover the essentials of a platform profile:
operating a system, designing a secure architecture, and thinking about scale.
</p>
<!-- PROJECT 1 : INFRA -->
<div class="project reveal">
<div class="project-top">
<h3>🏗️ Self-hosted infrastructure <span class="tag done">Complete &amp; tested</span></h3>
</div>
<p class="subtitle">A complete ecosystem — code → CI → production → observability — on bare metal, validated by real failure tests.</p>
<div class="block">
<div class="block-label">The problem</div>
<p>Having a reliable personal platform to host and operate my own services, with the standards of real production: resilience, alerting, zero data loss.</p>
</div>
<div class="block">
<div class="block-label">The architecture</div>
<ul>
<li><strong>Networking</strong> — WireGuard (the only entry point), Docker overlay, and <code>CoreDNS</code> for <code>*.internal</code> resolution with fallback to Docker's DNS.</li>
<li><strong>CI/CD</strong> — Gitea + Woodpecker: lint → tests → build → automatic push registry on tag/release → deploy on prod.</li>
<li><strong>Observability</strong> — OtelCollector (central hub), Prometheus (storage + alerting), Loki (logs) + Tempo (traces), Perses dashboards, Alertmanager → ntfy.</li>
<li><strong>Safety net</strong> — Blackbox scraped directly by Prometheus: monitoring survives a Collector failure.</li>
</ul>
</div>
<div class="block">
<div class="block-label">What sets it apart — validated resilience</div>
<ul>
<li>Gitea outage → ntfy alert received ✓</li>
<li>OtelCollector outage → alert received, monitoring maintained ✓</li>
<li>Multiple redeployments → zero data loss (persistent volumes) ✓</li>
</ul>
</div>
<div class="stack">
<span>Docker Swarm</span><span>WireGuard</span><span>CoreDNS</span><span>Gitea</span><span>Woodpecker CI</span><span>OpenTelemetry</span><span>Prometheus</span><span>Loki</span><span>Tempo</span><span>Perses</span><span>Alertmanager</span>
</div>
</div>
<!-- PROJECT 2 : HERALD -->
<div class="project reveal">
<div class="project-top">
<h3>🤖 Herald — AI agent for Gitea <span class="tag wip">In active development</span></h3>
</div>
<p class="subtitle">A code-review and AI-assistance bot for Gitea, written in Rust, with a <strong>security-first</strong> agentic architecture.</p>
<div class="block">
<div class="block-label">The problem</div>
<p>No AI assistance solution exists for Gitea. A real need: solo code review, implementing tests/features, usable day to day — including from a phone.</p>
</div>
<div class="block">
<div class="block-label">The security model</div>
<p>The challenge of an agent that executes code isn't making it run, but making it run <strong>without risk of exfiltration or compromise</strong>. The decisions:</p>
<ul>
<li>Agent loop on the host, <strong>side effects isolated in a container</strong></li>
<li><strong>Git handled exclusively by the host</strong> (Gitea token): no secret ever touches the sandbox.</li>
<li><code>.git</code> <strong>"disarmed"</strong> for reading (remotes removed, credentials cleared, hooks emptied) — on a copy, with the armed original staying on the host.</li>
<li><strong>Network cut during the AI phase</strong> (disconnect via bollard): exfiltration is impossible. Network active only for <code>postCreate</code>.</li>
<li>Persistence from the start (resume after stop).</li>
</ul>
</div>
<div class="block">
<div class="block-label">Roadmap (intentional semver)</div>
<ul>
<li><strong>1.0</strong> — Review + comments posted to the right line on the MR</li>
<li><strong>1.1</strong> — The 3 observability signals</li>
<li><strong>1.2</strong> — Sandboxing begins (read-only first)</li>
<li><strong>2.0</strong> — Feature creation (write / execute)</li>
<li><strong>2.x</strong> — Advanced persistence + web config dashboard</li>
</ul>
</div>
<div class="stack">
<span>Rust</span><span>Tokio</span><span>bollard</span><span>DevContainers</span><span>OpenRouter</span><span>OpenTelemetry</span>
</div>
</div>
<!-- PROJECT 3 : PIPELINE -->
<div class="project reveal">
<div class="project-top">
<h3>⚡ Distributed media pipeline <span class="tag wip">Design / upcoming</span></h3>
</div>
<p class="subtitle">An extended imgproxy-style media processing pipeline, designed for horizontal scaling and streaming.</p>
<div class="block">
<div class="block-label">The problem</div>
<p>Process media (images, streaming) compositionally, with an architecture able to scale out horizontally — and serve as a real load-testing playground.</p>
</div>
<div class="block">
<div class="block-label">The architecture</div>
<ul>
<li>Compositional API: a <strong>core</strong> + <strong>runners</strong> communicating over <code>NATS</code>.</li>
<li>Multi-level cache, horizontal scaling of runners.</li>
<li><strong>Claim-check pattern</strong>: files transit through an object store (S3/Minio); NATS carries only commands and metadata.</li>
</ul>
</div>
<div class="block">
<div class="block-label">The technical goal</div>
<p>An ideal candidate for <strong>load tests (k6) instrumented by my observability stack</strong> — validating system behavior at scale and documenting the breaking points.</p>
</div>
<div class="stack">
<span>Rust</span><span>Tokio</span><span>NATS</span><span>S3 / Minio</span><span>k6</span><span>OpenTelemetry</span><span>Docker Swarm</span>
</div>
</div>
</div>
</section>
<!-- SKILLS -->
<section id="skills">
<div class="wrap">
<div class="section-head reveal">
<span class="num">03</span>
<h2>Skills</h2>
</div>
<div class="skills-grid">
<div class="skill-card reveal">
<h4><span class="ic"></span> Languages</h4>
<p>Rust (async / Tokio), TypeScript / Node. Looking for the right tool for the right problem.</p>
</div>
<div class="skill-card reveal">
<h4><span class="ic"></span> Infrastructure &amp; Ops</h4>
<p>Docker Swarm, bare metal, Fedora Bootc (immutable OS), CI/CD (Gitea, Woodpecker).</p>
</div>
<div class="skill-card reveal">
<h4><span class="ic"></span> Observability</h4>
<p>OpenTelemetry, Prometheus, Loki, Tempo, Perses, Alertmanager. Full stack, tested under failure.</p>
</div>
<div class="skill-card reveal">
<h4><span class="ic"></span> Networking</h4>
<p>WireGuard, CoreDNS, overlay networks, Caddy. Designing self-hosted private networks.</p>
</div>
<div class="skill-card reveal">
<h4><span class="ic"></span> Security &amp; Isolation</h4>
<p>Container sandboxing, privilege separation (DBus / Polkit), threat modeling.</p>
</div>
<div class="skill-card reveal">
<h4><span class="ic"></span> Distributed &amp; Messaging</h4>
<p>NATS, distributed architectures, claim-check patterns, horizontal scaling.</p>
</div>
</div>
</div>
</section>
<!-- CONTACT -->
<section id="contact">
<div class="wrap">
<div class="section-head reveal">
<span class="num">04</span>
<h2>Contact</h2>
</div>
<p class="reveal">
Interested in platform / infrastructure / staff roles where technical quality
and autonomy matter. The code for everything shown here is available to review.
</p>
<div class="contact-links reveal">
<a href="mailto:pismont.quentin@protonmail.com" class="btn btn-primary">Get in touch</a>
<a href="https://gitea.qpismont.fr/explore/repos" class="btn btn-ghost">Gitea</a>
<a href="#" class="btn btn-ghost">LinkedIn</a>
</div>
</div>
</section>
<footer>
<div class="wrap">
© 2026
</div>
</footer>
<script>
// Reveal on scroll
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.12 });
document.querySelectorAll('.reveal').forEach((el, i) => {
// slight cascade offset
el.style.transitionDelay = (i % 4 * 60) + 'ms';
observer.observe(el);
});
</script>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
ingester:
max_block_duration: 5m
compactor:
compaction:
block_retention: 48h
storage:
trace:
backend: local
local:
path: /var/tempo/traces
wal:
path: /var/tempo/wal
+1
View File
@@ -1,6 +1,7 @@
.:53 { .:53 {
rewrite name perses.internal perses rewrite name perses.internal perses
rewrite name prometheus.internal prometheus rewrite name prometheus.internal prometheus
rewrite name tempo.internal tempo
forward . 127.0.0.11 forward . 127.0.0.11
errors errors
cache 10 cache 10