# Container sind keine Architektur

URL: https://www.mikebild.dev/de/blog/container-sind-keine-architektur/

Ein schlecht geschnittener Monolith wird nicht besser, wenn man ihn in einen Container steckt. Er bleibt derselbe Monolith – dieselben verfilzten Abhängigkeiten, dieselbe geteilte Datenbank, dasselbe Deployment, bei dem alles auf einmal rausgeht. Neu ist nur das Dockerfile obendrauf. Trotzdem ist gerade überall dieselbe Gleichsetzung zu hören, auf Konferenzen wie in Projektbesprechungen: Wir führen Docker ein, also machen wir jetzt Microservices. Das eine hat mit dem anderen zunächst nichts zu tun. Docker verpackt Software. Es entwirft sie nicht.

Die Verwechslung ist kein Zufall, und sie ist auch nicht dumm. Sie hat einen Grund, und den lohnt es sich anzuschauen – genauso wie die Frage, was Container tatsächlich leisten. Denn das ist eine ganze Menge. Nur eben etwas anderes als Architektur.

## Warum die Gleichsetzung so gut funktioniert

Beide Themen sind zur selben Zeit groß geworden. Docker steht bei Version 1.6, die 1.7 ist in Arbeit, in anderthalb Wochen beginnt die DockerCon in San Francisco, und nach dem monatelangen öffentlichen Streit zwischen Docker und CoreOS über das „richtige" Containerformat formiert sich gerade eine Initiative für einen offenen, herstellerneutralen Standard. Bei der Orchestrierung herrscht Wildwuchs: Mesos auf der einen Seite, Dockers eigenes Swarm auf der anderen, und Google arbeitet an Kubernetes, das noch vor der Version 1.0 steht. Parallel dazu ist der Text von James Lewis und Martin Fowler über Microservices seit gut einem Jahr der Referenzpunkt jeder Architekturdiskussion.

Beide Wellen tragen dasselbe Vokabular vor sich her: kleine, unabhängige Einheiten, isoliert, einzeln deploybar. Wer die Texte aber nebeneinanderlegt, sieht die Lücke sofort. Lewis und Fowler reden über fachliche Schnitte, über Teams, über Datenhoheit. Die Docker-Dokumentation redet über Images, Layer und Prozesse. Dass ein Container eine „unabhängige Einheit" ist, gilt für den Prozess und sein Dateisystem – nicht für die Fachlichkeit, die darin steckt. Ein Container weiß nicht, ob in ihm ein sauber geschnittener Dienst läuft oder ein Gebirge aus zwölf Jahren gewachsenem Code. Ihm ist es auch egal.

Die Gleichsetzung funktioniert, weil sie bequem ist. Ein Dockerfile schreibt sich an einem Nachmittag. Einen Modulschnitt, über den zwei Teams nicht mehr streiten, bekommt man nicht an einem Nachmittag – der braucht Wochen an Gesprächen über Daten, Zuständigkeiten und Schnittstellen, und am Ende steht kein vorzeigbares Werkzeug, sondern nur ein System, das sich leichter ändern lässt. Wenn beides unter demselben Buzzword segelt, gewinnt der Nachmittag. Vorzeigen lässt sich schließlich nur einer von beiden.

## Was ein Container wirklich leistet

Anfang des Jahres habe ich eine kleine .NET-API in einen Container gepackt: Nancy als Web-Framework, selbst gehostet, LiteDB als Dateidatenbank, das Ganze auf Mono, damit es auf Linux läuft. Kein Kundenprojekt, ein Experiment – ich wollte wissen, ob der Weg von Windows-geprägtem .NET-Code zu einem Linux-Container ohne große Verrenkungen begehbar ist. Er ist es. Das Dockerfile war ernüchternd unspektakulär:

```dockerfile
FROM mono:3.12

COPY . /app
WORKDIR /app

RUN nuget restore shop-api.sln && \
    xbuild /p:Configuration=Release shop-api.sln

EXPOSE 8080
CMD ["mono", "src/ShopApi/bin/Release/ShopApi.exe"]
```

Genau diese Unspektakularität ist der Punkt. Alles, was der Dienst zum Laufen braucht, steht in dreizehn Zeilen: die Laufzeitumgebung samt Version, der Build-Weg, der Port, das eine Kommando, mit dem der Prozess startet. Vorher stand dieses Wissen verteilt in einer Wiki-Seite („Entwicklungsumgebung aufsetzen, 14 Schritte"), in den Köpfen von zwei Kollegen und in der Konfiguration eines Build-Servers, den niemand mehr anfassen wollte. Jetzt steht es im Repository, wird mit dem Code versioniert und im Review gelesen.

Das ist der ehrliche Nutzen von Containern, und er ist erheblich:

- Dieselbe Umgebung auf dem Notebook, im CI und auf dem Server – „läuft bei mir" verliert seinen Schrecken.
- Ein expliziter Einstiegspunkt: genau ein Kommando startet den Prozess, nichts hängt an einem angemeldeten Benutzer oder einer Terminal-Session.
- Abhängigkeiten stehen im Image statt im Wiki – wer wissen will, was der Dienst braucht, liest das Dockerfile, nicht die Erinnerung des dienstältesten Kollegen.
- Ein Artefakt, das sich unverändert von Umgebung zu Umgebung schieben lässt, statt auf jeder Maschine neu zusammengebaut zu werden.

Man beachte, was in dieser Liste nicht vorkommt: fachliche Grenzen, Datenhoheit, unabhängige Releases. Das Dockerfile oben verrät nicht, ob `ShopApi.exe` ein fokussierter Dienst ist oder ein Koloss. Es baut beides gleich klaglos.

## Der Monolith zieht nur um

Wie wenig die Verpackung am Inhalt ändert, zeigt sich, sobald man einen echten Monolithen containerisiert. Nehmen wir einen Shop, wie er zigfach existiert: Bestellungen, Rechnungen und Katalog in einer Solution, eine gemeinsame Datenbank, ein Deployment. Mit Docker Compose ist die lokale Umgebung dafür schnell beschrieben:

```yaml
shop-api:
  build: .
  ports:
    - "8080:8080"
  links:
    - shop-db
shop-db:
  image: postgres:9.4
  environment:
    POSTGRES_DB: shop
```

Diese Datei ist nützlich – jeder im Team startet den Stand mit einem Kommando. Aber sie ist vor allem eines: ehrlich. Sie dokumentiert schwarz auf weiß, dass es genau eine deploybare Einheit gibt und eine Datenbank, in die alle Teile hineinschreiben. Wer im Rechnungs-Code eine Zeile ändert, baut und deployt weiterhin den kompletten Shop. Wer das Bestell-Schema migriert, hält weiterhin den Atem an, weil auch der Katalog auf denselben Tabellen sitzt. Der Release-Takt, die Kopplung, die Angst vor der Migration – alles unverändert.

```mermaid
flowchart LR
  subgraph before["Vorher"]
    M1["Monolith<br/>Orders + Billing + Catalog"] --> D1[("eine Datenbank")]
  end
  subgraph after["Nachher – mit Docker"]
    M2["Container<br/>Monolith<br/>Orders + Billing + Catalog"] --> D2[("eine Datenbank")]
  end
  before -. "docker build" .-> after
```

Das Diagramm ist absichtlich langweilig. Links und rechts steht dasselbe System. Der Container hat die Struktur sichtbar und transportabel gemacht – erzeugt hat er keine neue. Und Sichtbarkeit ist nicht wenig! Aber sie wird gern mit Veränderung verwechselt, so wie ein Umzugskarton mit Aufräumen verwechselt wird. Erst wenn man ihn packt, sieht man, was sich angesammelt hat. Weggeworfen ist damit noch nichts.

## Grenzen sichtbar machen heißt nicht Grenzen ziehen

Die eigentliche Arbeit heißt Modulschnitt, und sie findet vor jedem Dockerfile statt. Drei Fragen entscheiden, ob ein Schnitt trägt. Wem gehören welche Daten – wer darf die Bestelltabellen ändern, und wer bekommt sie nur über eine Schnittstelle zu sehen? Wie sieht der Vertrag zwischen den Teilen aus – welche Aufrufe sind erlaubt, und wie stabil bleiben sie, wenn sich innen etwas ändert? Und: Was lässt sich unabhängig ausliefern, ohne dass zwei Teams ihre Kalender abgleichen müssen?

Nichts davon braucht Docker. Ein sauberer Schnitt lässt sich mitten im Monolithen ziehen, in derselben Solution, im selben Prozess. Zwischen dem Bestell-Modul und dem Rechnungs-Modul steht dann ein bewusst schmaler Vertrag:

```csharp
// the only allowed path from Orders into Billing –
// no shared tables, no reaching into internals
public interface IBillingService
{
    Invoice CreateInvoice(OrderId orderId, Money total);
}
```

Dazu gehört die unbequemere Hälfte: `Orders` schreibt ausschließlich in seine eigenen Tabellen, `Billing` in seine. Kein Join quer über die Modulgrenze, auch nicht „nur für den Report". Wenn diese Disziplin eine Weile gehalten hat, ist der Rest Mechanik. Dann – und erst dann – wird aus dem Modul bei Bedarf ein eigener Container, und die Compose-Datei sieht plötzlich nach dem aus, was Lewis und Fowler beschreiben:

```yaml
orders:
  build: ./orders
  ports:
    - "8080:8080"
  links:
    - orders-db
    - billing
orders-db:
  image: postgres:9.4
billing:
  build: ./billing
  links:
    - billing-db
billing-db:
  image: postgres:9.4
```

Der springende Punkt: Diese Datei ist das Ergebnis der Entwurfsarbeit, nicht ihr Ersatz. Wer sie hinschreibt, ohne den Schnitt vorher im Code und in den Daten vollzogen zu haben, bekommt zwei Container, die sich gegenseitig in die Datenbank greifen – denselben Monolithen, jetzt mit Netzwerklatenz dazwischen. Das ist die teuerste Variante eines Monolithen, die ich kenne: verteilt, aber nicht entkoppelt. Jedes Orchestrierungswerkzeug, ob Mesos, Swarm oder demnächst Kubernetes, multipliziert diesen Zustand nur. Es verteilt, was man ihm gibt. Gibt man ihm einen verfilzten Schnitt, verteilt es Verfilzung.

```mermaid
flowchart LR
  subgraph om["Modul Orders"]
    O["orders"] --> ODB[("orders-db")]
  end
  subgraph bm["Modul Billing"]
    B["billing"] --> BDB[("billing-db")]
  end
  O -- "Vertrag:<br/>CreateInvoice(orderId, total)" --> B
```

Dieses Bild ist der Unterschied. Nicht die Kästen sind interessant, sondern die Pfeile, die fehlen: kein Pfeil von `orders` auf `billing-db`, kein Rückkanal an der Schnittstelle vorbei. Solche fehlenden Pfeile zeichnet einem kein Werkzeug. Die muss man sich erarbeiten, gegen den Widerstand des Tagesgeschäfts, das immer einen guten Grund für die eine kleine Abkürzung hat.

## Der Container als Prüfstein

Wenn Container beim Entwurf überhaupt helfen, dann als Prüfstein. Bei dem Mono-Experiment ist mir aufgefallen, dass die unangenehmste Frage nicht „Wie baue ich das Image?" war, sondern „Was gehört hinein?". Der Dienst zog Konfiguration aus drei Quellen, erwartete ein Netzlaufwerk und schrieb Logs an einen fest verdrahteten Pfad. Nichts davon war vorher ein Problem gewesen, weil die Maschine, auf der er lief, all das stillschweigend bereitstellte. Der Container stellt nichts stillschweigend bereit. Er zwingt dazu, jede Annahme entweder ins Image zu schreiben oder als Umgebungsvariable an die Oberfläche zu holen. Jede dieser Annahmen war im Grunde eine unsichtbare Kopplung – an eine Maschine, an eine Abteilung, an ein Stück Betriebsfolklore, das nirgends aufgeschrieben war.

Das ist dieselbe Sorte Zwang, die ein guter Modulschnitt ausübt, nur eine Ebene tiefer. Wer für einen Dienst nicht sagen kann, was in sein Image gehört und was von außen kommt, kennt die Grenzen dieses Dienstes nicht – und sollte über Microservices noch gar nicht reden. Insofern taugt die Containerisierung des Monolithen als erster Schritt durchaus: nicht weil sie Architektur wäre, sondern weil sie die Inventur erzwingt, ohne die kein späterer Schnitt gelingt. Man lernt dabei mehr über sein System, als einem lieb ist.

Nur sollte man den Schritt dann auch als das behandeln, was er ist. Ein containerisierter Monolith ist ein völlig legitimer Zustand – reproduzierbar gebaut, überall gleich startbar, mit dokumentierten Abhängigkeiten. Für viele Systeme ist das auf Jahre hinaus genug. Verkauft man ihn dagegen als „Microservices-Migration, Phase eins", hat man ein Versprechen in die Welt gesetzt, das die Verpackung nicht einlösen kann.

## Was ich daraus mitnehme

Für mich hat sich eine Reihenfolge herausgeschält, die sich in Diskussionen bewährt: erst den Monolithen containerisieren und die betrieblichen Vorteile mitnehmen – gleiche Umgebung überall, expliziter Prozess-Einstieg, Abhängigkeiten im Image. Parallel dazu die Grenzen im Code ziehen, wo sie nichts kosten: Module, eigene Tabellen, schmale Verträge wie das `IBillingService`-Beispiel oben. Und erst wenn eine Grenze über mehrere Releases stabil geblieben ist und ein konkreter Grund existiert – anderer Release-Takt, andere Last, anderes Team – wird aus dem Modul ein eigener Dienst mit eigenem Container.

Docker macht auf diesem Weg zwei Dinge hervorragend: Es transportiert die Einheiten, die man geschnitten hat, und es deckt schonungslos auf, wo man noch gar nicht geschnitten hat. Beides ist wertvoll. Keines davon ist Entwurf. Die Standardisierung, die sich gerade um das Containerformat abzeichnet, wird die Verpackung noch selbstverständlicher machen – umso wichtiger wird es, sie nicht mit dem Inhalt zu verwechseln. Ein Dockerfile beantwortet die Frage, wie ein Prozess startet. Wer die Frage beantworten will, wo ein System endet und das nächste beginnt, sitzt weiterhin vor einem Whiteboard, nicht vor einer YAML-Datei.

## Weiterführende Quellen

- [MikeBild/dotnet-docker auf GitHub](https://github.com/MikeBild/dotnet-docker) – das erwähnte Experiment: Nancy, Self-Hosting und LiteDB auf Mono im Container
- [Microservices – James Lewis und Martin Fowler](https://martinfowler.com/articles/microservices.html) – der Referenztext zu fachlichem Schnitt, Datenhoheit und unabhängiger Auslieferung
- [Dockerfile-Referenz](https://docs.docker.com/reference/dockerfile/) – die offizielle Beschreibung aller Build-Anweisungen
- [Docker Compose Dokumentation](https://docs.docker.com/compose/) – Grundlagen zur Beschreibung mehrerer Dienste in einer Datei
