Phase1_Infrastructure/infrastructure-summary.html
2026-09-04 11:49:24 +02:00

322 lines
10 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>babypistachio.com — Infrastructure Summary</title>
<style>
:root {
--paper: #f7f5f0;
--ink: #1c1b19;
--ink-soft: #55524a;
--rule: #d8d3c6;
--accent: #3a5a52;
--accent-soft: #e4ebe8;
--mono-bg: #eeeae0;
--tag-internal: #6b5a3e;
--tag-public: #3a5a52;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: "Iowan Old Style", "Palatino Linotype", Georgia, serif;
line-height: 1.6;
}
.wrap {
max-width: 760px;
margin: 0 auto;
padding: 4rem 1.5rem 6rem;
}
header.doc-head {
border-bottom: 1px solid var(--rule);
padding-bottom: 1.75rem;
margin-bottom: 2.5rem;
}
.doc-kicker {
font-family: "SF Mono", "Menlo", monospace;
font-size: 0.78rem;
color: var(--ink-soft);
letter-spacing: 0.02em;
}
h1 {
font-size: 2.1rem;
font-weight: 600;
margin: 0.4rem 0 0.6rem;
letter-spacing: -0.01em;
}
.doc-sub {
color: var(--ink-soft);
font-size: 1.02rem;
max-width: 58ch;
}
h2 {
font-size: 1.25rem;
font-weight: 600;
margin: 3rem 0 1rem;
padding-top: 0.25rem;
}
h2 .num {
font-family: "SF Mono", "Menlo", monospace;
color: var(--accent);
font-size: 0.95rem;
margin-right: 0.6rem;
}
p { margin: 0.9rem 0; }
code {
font-family: "SF Mono", "Menlo", monospace;
background: var(--mono-bg);
padding: 0.12em 0.4em;
border-radius: 3px;
font-size: 0.88em;
}
table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0 0.5rem;
font-size: 0.95rem;
}
th {
text-align: left;
font-family: "SF Mono", "Menlo", monospace;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ink-soft);
font-weight: 500;
padding: 0 0.9rem 0.6rem 0;
border-bottom: 1px solid var(--rule);
}
td {
padding: 0.75rem 0.9rem 0.75rem 0;
border-bottom: 1px solid var(--rule);
vertical-align: top;
}
td.svc code {
font-weight: 600;
}
tr:last-child td { border-bottom: none; }
.tag {
display: inline-block;
font-family: "SF Mono", "Menlo", monospace;
font-size: 0.7rem;
padding: 0.15em 0.55em;
border-radius: 3px;
letter-spacing: 0.02em;
}
.tag.public { background: var(--accent-soft); color: var(--tag-public); }
.tag.internal { background: #f1ebe0; color: var(--tag-internal); }
.net-list {
list-style: none;
padding: 0;
margin: 1rem 0;
}
.net-list li {
padding: 0.9rem 0;
border-bottom: 1px solid var(--rule);
}
.net-list li:last-child { border-bottom: none; }
.net-list .net-name {
font-family: "SF Mono", "Menlo", monospace;
font-weight: 600;
color: var(--accent);
}
.net-list p { margin: 0.3rem 0 0; color: var(--ink-soft); font-size: 0.95rem; }
.callout {
border-left: 3px solid var(--accent);
background: var(--accent-soft);
padding: 1rem 1.25rem;
margin: 1.5rem 0;
border-radius: 0 4px 4px 0;
}
.callout p { margin: 0; font-size: 0.96rem; }
.callout .callout-label {
font-family: "SF Mono", "Menlo", monospace;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--accent);
display: block;
margin-bottom: 0.4rem;
}
.risk {
border-left: 3px solid var(--tag-internal);
background: #f1ebe0;
padding: 1rem 1.25rem;
margin: 1.5rem 0;
border-radius: 0 4px 4px 0;
}
.risk p { margin: 0; font-size: 0.96rem; }
.risk .risk-label {
font-family: "SF Mono", "Menlo", monospace;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--tag-internal);
display: block;
margin-bottom: 0.4rem;
}
.filelist {
list-style: none;
padding: 0;
margin: 1rem 0;
font-family: "SF Mono", "Menlo", monospace;
font-size: 0.9rem;
}
.filelist li {
padding: 0.4rem 0;
color: var(--ink-soft);
}
.filelist li strong {
color: var(--ink);
font-weight: 600;
}
.filelist .indent { padding-left: 1.4rem; }
footer {
margin-top: 3.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--rule);
font-size: 0.85rem;
color: var(--ink-soft);
}
</style>
</head>
<body>
<div class="wrap">
<header class="doc-head">
<div class="doc-kicker">infrastructure summary — stage 1</div>
<h1>babypistachio.com</h1>
<p class="doc-sub">Backend mobile-development test environment on a single VPS, fronted by a reverse proxy with automatic HTTPS.</p>
</header>
<section>
<h2><span class="num">01</span>Services</h2>
<table>
<thead>
<tr><th>Service</th><th>Image</th><th>Reachable at</th></tr>
</thead>
<tbody>
<tr>
<td class="svc"><code>traefik</code></td>
<td>traefik:v3.6</td>
<td><span class="tag public">public</span> ports 80 / 443 — reverse proxy, TLS termination, routes every subdomain</td>
</tr>
<tr>
<td class="svc"><code>socket-proxy</code></td>
<td>tecnativa/docker-socket-proxy:0.3</td>
<td><span class="tag internal">internal</span> lets Traefik discover containers without touching the raw Docker socket</td>
</tr>
<tr>
<td class="svc"><code>apache</code></td>
<td>httpd:2.4</td>
<td><span class="tag public">public</span> babypistachio.com — static site</td>
</tr>
<tr>
<td class="svc"><code>gitea</code></td>
<td>gitea/gitea:1.22</td>
<td><span class="tag public">public</span> git.babypistachio.com — repository hosting</td>
</tr>
<tr>
<td class="svc"><code>jenkins</code></td>
<td>jenkins/jenkins:lts-jdk21</td>
<td><span class="tag public">public</span> ci.babypistachio.com — CI/CD</td>
</tr>
<tr>
<td class="svc"><code>api</code></td>
<td>built from ./api (node:22-alpine)</td>
<td><span class="tag public">public</span> api.babypistachio.com/health — placeholder service confirming the environment works end to end</td>
</tr>
<tr>
<td class="svc"><code>postgres</code></td>
<td>postgres:16</td>
<td><span class="tag internal">internal</span> no public route, not yet wired into any service — ready for the real app</td>
</tr>
</tbody>
</table>
</section>
<section>
<h2><span class="num">02</span>Network layout</h2>
<ul class="net-list">
<li>
<span class="net-name">proxy</span> — internet-facing
<p>Traefik, apache, gitea, jenkins, and api sit here.</p>
</li>
<li>
<span class="net-name">internal</span> — no internet access, in or out
<p>Postgres lives here with no published ports. api is also attached so it can reach postgres:5432 once the real app is deployed, without ever exposing the database externally.</p>
</li>
<li>
<span class="net-name">socket-proxy</span> — isolated
<p>Connects Traefik to docker-socket-proxy only, so Traefik never mounts the host's Docker socket directly.</p>
</li>
</ul>
</section>
<section>
<h2><span class="num">03</span>What got fixed along the way</h2>
<p>Five real issues surfaced while bringing this up, each documented in the repo's README so the fix doesn't get silently undone by a future change:</p>
<ul>
<li>Missing <code>package-lock.json</code> broke the API's Docker build (<code>npm ci</code> requires one); also caught and fixed a moderate <code>qs</code> vulnerability via <code>npm audit fix</code>.</li>
<li><code>socket-proxy</code> crash-looped under <code>read_only: true</code>, which blocked it from writing its own config at startup.</li>
<li>Traefik couldn't talk to Docker at all — a known bug in Traefik v3.0v3.5 that hardcodes an old API version. Fixed by upgrading to v3.6.</li>
<li>Jenkins crash-looped on a permission error — the named volume was created root-owned since the image ships no seed files. Fixed with a one-time <code>chown</code>.</li>
<li><code>api.babypistachio.com</code> returned Gateway Timeout because it sits on two Docker networks and Traefik couldn't infer which one to route through — fixed with an explicit <code>traefik.docker.network</code> label.</li>
</ul>
</section>
<section>
<h2><span class="num">04</span>Files</h2>
<ul class="filelist">
<li><strong>docker-compose.yml</strong> — full stack definition</li>
<li><strong>README.md</strong> — setup steps, network layout, troubleshooting log</li>
<li><strong>.env.example</strong> — template for domain, ACME email, Postgres credentials</li>
<li><strong>api/</strong></li>
<li class="indent">Dockerfile, package.json, package-lock.json, server.js</li>
<li><strong>website/</strong></li>
<li class="indent">index.html</li>
</ul>
</section>
<div class="callout">
<span class="callout-label">Security decisions worth knowing</span>
<p>No hardcoded secrets — everything sensitive comes from <code>.env</code>. Traefik's dashboard is enabled but has no external router, so it isn't reachable from the internet. Security headers (HSTS, X-Frame-Options, nosniff, XSS filter) apply to every public router. The VPS firewall should still restrict inbound traffic to 22/80/443 — Docker network isolation isn't a substitute for that.</p>
</div>
<div class="risk">
<span class="risk-label">Open risk — not yet resolved</span>
<p>Jenkins mounts the host Docker socket directly to support CI/CD builds. Anyone who compromises Jenkins has root-equivalent control of the host. Reasonable trade-off for a test environment, but not something to carry into production as-is — see the README for mitigation options (Docker-in-Docker agents, separate build VMs, rootless Docker).</p>
</div>
<footer>
Stage 1 — infrastructure — complete. Postgres is provisioned but not yet connected to any service; that's the starting point for the next stage.
</footer>
</div>
</body>
</html>