322 lines
10 KiB
HTML
322 lines
10 KiB
HTML
<!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.0–v3.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>
|