From 2557728485c24767df5c0ae4fc7fcb3363758db6 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 4 Sep 2026 11:49:24 +0200 Subject: [PATCH] Add new files --- README.md | 126 ++++++++++++++ infrastructure-summary.html | 321 ++++++++++++++++++++++++++++++++++++ 2 files changed, 447 insertions(+) create mode 100644 README.md create mode 100644 infrastructure-summary.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..e898c95 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +# babypistachio.com — backend test environment + +## What this is + +A VPS set up as a backend test environment for mobile app development, +with a reverse proxy fronting everything, HTTPS via Let's Encrypt, and +isolated networking so the database is never internet-reachable. + +## Services + +| Service | Image | Reachable at | Purpose | +|-----------|---------------------------------|----------------------------|---------| +| traefik | traefik:v3.6 | ports 80/443 (public) | Reverse proxy, TLS termination, auto HTTPS via Let's Encrypt, routes all subdomains | +| socket-proxy | tecnativa/docker-socket-proxy:0.3 | internal only | Lets Traefik discover containers without mounting the raw Docker socket | +| apache | httpd:2.4 | babypistachio.com, www.babypistachio.com | Static website | +| gitea | gitea/gitea:1.22 | git.babypistachio.com | Git repository hosting | +| jenkins | jenkins/jenkins:lts-jdk21 | ci.babypistachio.com | CI/CD | +| api | built from ./api (node:22-alpine) | api.babypistachio.com/health | Placeholder API confirming the environment works end-to-end | +| postgres | postgres:16 | internal only, no public route | Database for the future app (not yet wired into any service) | + +## Network layout + +- **proxy** — internet-facing. Traefik + apache/gitea/jenkins/api sit here. +- **internal** (`internal: true`) — no internet access in or out. Postgres + lives here, with no published ports. `api` is also attached here so it + can reach `postgres:5432` once the real app is deployed, without ever + exposing Postgres itself externally. +- **socket-proxy** — isolated network between Traefik and `docker-socket-proxy`, + so Traefik never touches `/var/run/docker.sock` directly — it only gets + read-only container-listing access. + +All three networks are given fixed names (`proxy`, `internal`, `socket-proxy`) +in the compose file rather than left to Compose's auto-prefixing, so labels +like `traefik.docker.network=proxy` keep working regardless of what the +project folder is called. + +## Setup + +1. Copy `.env.example` to `.env` and fill in real values. Never commit `.env`. +2. Point DNS `A` records for `babypistachio.com`, `www.babypistachio.com`, + `ci.babypistachio.com`, `git.babypistachio.com`, `api.babypistachio.com` + at the VPS's IP before starting. +3. `docker compose up -d --build` +4. One-time Jenkins volume fix (see Troubleshooting notes below) — only + needed on first-ever startup. + +## Troubleshooting notes (from getting this running) + +These were the real issues hit during setup, kept here so future changes +don't reintroduce them: + +1. **`npm ci` failed in the API build** — no `package-lock.json` existed. + Fixed by generating one (`npm install --package-lock-only`) and + committing it alongside `package.json`. `npm audit` also caught a + moderate `qs`/body-parser vulnerability, fixed via `npm audit fix` + (now on express 4.22.2). + +2. **socket-proxy crash-looped** — `read_only: true` on that container + blocked it from writing its HAProxy config at startup. Removed + `read_only` for this one low-risk, internal-only container rather + than fight it with tmpfs mounts (a tmpfs over the config directory + also wiped out a template file baked into the image, causing a + second failure). + +3. **Traefik couldn't talk to Docker: "client version 1.24 is too old"** + — a known bug in Traefik v3.0-v3.5, which hardcode API version 1.24 + regardless of the actual Docker Engine version. Fixed by upgrading + the Traefik image to v3.6, which negotiates the API version properly. + (An earlier attempt to fix this via a `DOCKER_API_VERSION` env var + did nothing -- Traefik's Docker client doesn't read it.) + +4. **Jenkins crash-looped: "Permission denied" writing to `/var/jenkins_home`** + -- the official Jenkins image runs as UID 1000, but ships no seed files + for Docker to copy into a fresh named volume, so the volume was created + owned by root. Fixed once, permanently, with: + ``` + docker run --rm -v docker_jenkins_home:/data busybox chown -R 1000:1000 /data + ``` + Also corrected the `docker` group GID in `group_add` to match this + VPS's actual GID (988, found via `getent group docker`) -- needed for + Jenkins to use the mounted Docker socket for CI builds. + +5. **`api.babypistachio.com` returned Gateway Timeout** -- the `api` + service sits on two networks (`proxy` and `internal`), and Traefik + can't infer which one to route through when a container has more + than one. Fixed with the `traefik.docker.network=proxy` label. This + is also why network names are pinned explicitly in this file (see + Network layout above) -- without that, the label would need to match + Compose's auto-generated `_proxy` name, which changes if + the folder is renamed. + +## Known trade-off: Jenkins and the Docker socket + +Jenkins mounts `/var/run/docker.sock` directly (read-write) because CI/CD +needs to build and run Docker images -- more Docker API access than a +restricted socket-proxy can safely grant without mostly defeating the +purpose. In practice, **anyone who compromises Jenkins has root-equivalent +control of the host.** + +Options if you want to close this gap later: +- Run builds in isolated Docker-in-Docker (`docker:dind`) agents instead + of mounting the host socket +- Use Jenkins agents on separate VMs +- Rootless Docker + +Acceptable trade-off for a test environment; don't put production secrets +on this host as-is. + +## Other security decisions + +- No hardcoded secrets -- everything sensitive comes from `.env`. +- Traefik dashboard (`api.dashboard=true`) is enabled but has no router + exposing it externally -- not reachable from the internet as configured. +- Security headers (HSTS, X-Frame-Options, nosniff, XSS filter) applied + to every public router via the `sec-headers` middleware. +- `api` has its own Dockerfile with dependencies baked in and locked via + `package-lock.json` -- no reinstalling on every restart. +- VPS-level firewall (ufw) should still only allow 22/80/443 in -- don't + rely on Docker network isolation alone. + +## Next stage + +Postgres isn't wired into any service yet. When the real app is deployed, +`api` (or whatever replaces it) can reach it at `postgres:5432` over the +`internal` network -- no code changes needed to the network setup, just +add the connection logic. diff --git a/infrastructure-summary.html b/infrastructure-summary.html new file mode 100644 index 0000000..1f4db2c --- /dev/null +++ b/infrastructure-summary.html @@ -0,0 +1,321 @@ + + + + + +babypistachio.com — Infrastructure Summary + + + +
+ +
+
infrastructure summary — stage 1
+

babypistachio.com

+

Backend mobile-development test environment on a single VPS, fronted by a reverse proxy with automatic HTTPS.

+
+ +
+

01Services

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ServiceImageReachable at
traefiktraefik:v3.6public ports 80 / 443 — reverse proxy, TLS termination, routes every subdomain
socket-proxytecnativa/docker-socket-proxy:0.3internal lets Traefik discover containers without touching the raw Docker socket
apachehttpd:2.4public babypistachio.com — static site
giteagitea/gitea:1.22public git.babypistachio.com — repository hosting
jenkinsjenkins/jenkins:lts-jdk21public ci.babypistachio.com — CI/CD
apibuilt from ./api (node:22-alpine)public api.babypistachio.com/health — placeholder service confirming the environment works end to end
postgrespostgres:16internal no public route, not yet wired into any service — ready for the real app
+
+ +
+

02Network layout

+
    +
  • + proxy — internet-facing +

    Traefik, apache, gitea, jenkins, and api sit here.

    +
  • +
  • + internal — no internet access, in or out +

    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.

    +
  • +
  • + socket-proxy — isolated +

    Connects Traefik to docker-socket-proxy only, so Traefik never mounts the host's Docker socket directly.

    +
  • +
+
+ +
+

03What got fixed along the way

+

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:

+
    +
  • Missing package-lock.json broke the API's Docker build (npm ci requires one); also caught and fixed a moderate qs vulnerability via npm audit fix.
  • +
  • socket-proxy crash-looped under read_only: true, which blocked it from writing its own config at startup.
  • +
  • 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.
  • +
  • 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 chown.
  • +
  • api.babypistachio.com returned Gateway Timeout because it sits on two Docker networks and Traefik couldn't infer which one to route through — fixed with an explicit traefik.docker.network label.
  • +
+
+ +
+

04Files

+
    +
  • docker-compose.yml — full stack definition
  • +
  • README.md — setup steps, network layout, troubleshooting log
  • +
  • .env.example — template for domain, ACME email, Postgres credentials
  • +
  • api/
  • +
  • Dockerfile, package.json, package-lock.json, server.js
  • +
  • website/
  • +
  • index.html
  • +
+
+ +
+ Security decisions worth knowing +

No hardcoded secrets — everything sensitive comes from .env. 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.

+
+ +
+ Open risk — not yet resolved +

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).

+
+ + + +
+ +