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.ymlopisuje 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¶
```
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 | sshzostanie zastąpiony pipeline-em który robi to samo automatycznie po push namain. - 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.