Phase1_Infrastructure/README.md
2026-09-04 11:49:24 +02:00

127 lines
6.3 KiB
Markdown

# 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 `<project>_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.