Przejdź do treści

Etap 0 — Infrastructure bootstrap

Etap 0 stoi na jednym commicie (2944eb9 etap-0: infra bootstrap). To moment zerowy: pusta maszyna Ubuntu LTS → zestaw kontenerów za reverse-proxy z TLS + zewnętrzny Keycloak + Gitea + UI operacyjne. Każdy kolejny etap dokłada usługi do tej bazy, ale topologia z etapu 0 (jedna VM, jeden plik docker-compose.yml, Traefik na froncie) nie zmieniła się ani razu — i nie planujemy jej zmieniać aż do etapu 8.

Decyzje fundamentalne

  • Self-hosted, jedna VM. Brak managed cloud, brak rejestru kontenerów, brak zewnętrznego CI. Wszystko u nas, na maszynie którą sami administrujemy.
  • Docker Compose, nie Kubernetes / Swarm. Skala projektu (1 host, ~12 kontenerów) nie uzasadnia narzutu orkiestratora.
  • Traefik z automatycznym Let's Encrypt zamiast nginx + certbot — jeden plik docker-compose.yml opisuje routing, TLS i middleware.
  • Postgres jako współdzielony cluster dla SmartBox + Keycloak + Gitea. Per-app bazy + per-app usery, izolacja na poziomie ról Postgresa.

Maszyna produkcyjna

Parametr Wartość
Hostname sdi-proj-smartbox
OS Ubuntu 24.04 LTS
LAN IP 10.198.69.2 (dostęp tylko przez VPN do sieci dev)
Public IP 153.92.16.152
Domena bazowa smartbox.ergoflow.app
SSH alias (LAN) smartbox
SSH alias (publiczny) smartbox-pub
Repo na maszynie /opt/smartbox

Hairpin NAT

Router nie hair-pin-uje 443 na publiczny IP, więc backend rozwiązuje auth.smartbox.ergoflow.app przez extra_hosts: host-gateway → idzie do Traefika lokalnie zamiast skakać przez Internet. Patrz infra/README.md sekcja "Hairpin NAT note".

Stos kontenerów

graph LR
    classDef edge fill:#0AD6E8,stroke:#10F3FF,color:#06101A,stroke-width:2px
    classDef store fill:#1A2C42,stroke:#5EE6A0,color:#fff
    classDef ext fill:#101F33,stroke:#0AD6E8,color:#fff,stroke-width:2px
    classDef ops fill:#0A1628,stroke:#F5C16C,color:#fff

    Internet((Internet)) -->|443| Traefik[Traefik 3.5<br/>HTTP-01 LE]:::edge
    Traefik --> Keycloak[Keycloak 26]:::ext
    Traefik --> Gitea[Gitea 1.22]:::ext
    Traefik --> Portainer[Portainer CE]:::ops
    Traefik --> Dozzle[Dozzle]:::ops
    Traefik --> RabbitUI[RabbitMQ mgmt]:::store

    Keycloak --> Postgres[(Postgres 16<br/>cluster)]:::store
    Gitea --> Postgres

    Backend[Backend NestJS<br/>etap 1+]:::edge -.->|future| Postgres
    Backend -.->|future| Redis[(Redis 7)]:::store
    Backend -.->|future| Rabbit[(RabbitMQ 3.13)]:::store

Etap 0 odpalił dziewięć kontenerów. Wszystkie w tej samej sieci docker bridge smartbox, jedyne porty wystawione na hosta to 80/443 na Traefiku.

Serwis Obraz Subdomena Auth
Traefik traefik:latest traefik.smartbox.ergoflow.app basic auth
Postgres postgres:16-alpine tylko wewnętrznie per-app role
Redis redis:7-alpine tylko wewnętrznie hasło
RabbitMQ rabbitmq:3.13-management-alpine rabbit.smartbox.ergoflow.app RMQ user
Keycloak quay.io/keycloak/keycloak:26.0 auth.smartbox.ergoflow.app KC admin
Gitea gitea/gitea:1.22 git.smartbox.ergoflow.app własna
Portainer portainer/portainer-ce:lts ops.smartbox.ergoflow.app własna
Dozzle amir20/dozzle:latest logs.smartbox.ergoflow.app basic auth

Pre-rekwizyt: subdomeny w DNS

Każda subdomena to osobny rekord A → 153.92.16.152, ręcznie dodany przed pierwszym make up. Inaczej HTTP-01 challenge nie przejdzie i Let's Encrypt nie wystawi cert-a. To świadoma decyzja — operator ma kontrolę nad tym co publikujemy, brak wildcardów.

Procedura first-time deploy

```bash

rsync / tar / git clone — cokolwiek; cel /opt/smartbox

sudo mkdir -p /opt/smartbox && sudo chown $USER /opt/smartbox cd /opt/smartbox git clone https://git.smartbox.ergoflow.app/mikolaj/smartbox.git . ```

```bash cd infra make bootstrap

→ instaluje Docker + Docker Compose plugin + ufw rules

→ loguj się jeszcze raz, żeby grupa docker zaczęła obowiązywać

```

```bash make env

→ tworzy infra/.env z 32-bajtowymi sekretami z /dev/urandom

→ drukuje hasło admina Traefika (NIE ląduje w .env, tylko w htpasswd)

→ skopiuj je teraz, nie ma drugiej szansy

```

```bash make up

→ poczekaj ~30 s żeby cert-y się wystawiły (LE HTTP-01)

→ backend wystartuje błędem do póki etap 1 nie wleciał — to OK

```

```bash bash keycloak/bootstrap-realm.sh

→ tworzy realm smartbox, klientów web-panel + backend

→ tworzy seed admina, drukuje jego hasło JEDEN RAZ

```

Struktura plików

infra/ ├── docker-compose.yml jeden manifest dla całego stosu ├── .env wygenerowany przez gen-env.sh, gitignored ├── .env.example template z opisanymi zmiennymi ├── Makefile skróty: bootstrap / env / up / down / docs / … ├── keycloak/ │ └── bootstrap-realm.sh jednorazowa konfiguracja realmu + klientów ├── postgres/ │ └── init.sh tworzy per-app role + bazy przy pierwszym boot-cie ├── scripts/ │ ├── bootstrap-server.sh instaluje Docker, ufw, make │ └── gen-env.sh generuje .env + htpasswd dla Traefika └── traefik/ └── dynamic/ ├── middlewares.yml security headers + admin basic-auth └── users.htpasswd generated, gitignored

TLS i certyfikaty

Traefik trzyma acme.json w volume traefik_certs — to jeden plik z wszystkimi cert-ami. Nie commit-ować nigdzie, nigdy nie kopiować w plain-text. Backup tego volume to backup wszystkich cert-ów.

yaml - --certificatesresolvers.le.acme.email=${ACME_EMAIL} - --certificatesresolvers.le.acme.storage=/certs/acme.json - --certificatesresolvers.le.acme.httpchallenge=true - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web

HTTP→HTTPS redirect jest globalny:

yaml - --entrypoints.web.address=:80 - --entrypoints.web.http.redirections.entrypoint.to=websecure - --entrypoints.web.http.redirections.entrypoint.scheme=https - --entrypoints.websecure.address=:443

Postgres — wspólny cluster, izolowane bazy

postgres/init.sh odpala się tylko przy pierwszym starcie kontenera (kiedy /var/lib/postgresql/data jest puste). Tworzy:

keycloak / keycloak DB gitea / gitea DB smartbox / smartbox DB

Każda rola ma własne hasło z .env, każda widzi tylko swoją bazę. To bezpieczniejsze niż wspólny user — Keycloak nie widzi shipments, backend nie widzi gitea repos.

Volume pierwszego boot-u

Jeśli skasujesz volume postgres_data i postawisz cluster od nowa, init.sh pójdzie na nowo z nowymi hasłami z .env — a stare bazy w bekapie tego nie wiedzą. Restore robisz przez pg_dump → pg_restore z hasłami z aktualnego .env, nie przez restore katalogu.

Co się zmieniło od etapu 0

Topologia: nic. Te same 9 serwisów, w tej samej kompozycji, za tym samym Traefikiem.

Co dołożyły kolejne etapy do docker-compose.yml:

Etap Dodane serwisy
1 backend (NestJS API)
2 frontend-web (Next.js panel)
4 workers (NestJS, ten sam image, inny entrypoint)
etap-doc-site docs (MkDocs Material → nginx)

Dziesięć kontenerów dziś, wszystko w jednym docker-compose.yml, te same wzorce labels Traefika dla każdego z nich.

Co dalej dotyka etapu 0

  • etap 8 — hardening. Plan: Prometheus + Grafana + Loki, dashboards per usługa, alertowanie. Wszystko jako kolejne kontenery w tym samym Compose, dalej za Traefikiem.
  • etap 8 — Gitea Actions runner. Wystarczy jeden runner-container w stosie + repo-side workflow YAML. Deploy tar | ssh zostanie zastąpiony pipeline-em który robi to samo automatycznie po push na main.
  • etap 8 — restic do bekapów. Dziś bekap to ręczny pg_dump + tar volume-ów. Plan: kontener restic z cronem na S3-kompatybilny bucket.

Pełen aktualny runbook → Infrastruktura.