Terraform (und OpenTofu) im Docker-Container ausführen

23.07.2026

DevOps, Docker, Terraform, OpenTofu, Container, Reproduzierbarkeit

Statt Terraform oder OpenTofu lokal zu installieren, kannst du das Binary in einem Docker-Container ausführen und per Shell-Alias so einbinden, dass es sich anfühlt wie lokal installiert. In diesem Tutorial baust du diesen Alias Flag für Flag auf, verstehst jede Option und räumst die typischen Stolpersteine aus Datei-Ownership, Credentials, der TTY in der CI und der Provider-Download bei --rm.

Inhaltsverzeichnis

Warum ein Tool im Container statt lokal?

Terraform und OpenTofu sind einzelne, statisch gelinkte Binaries. Trotzdem gibt es gute Gründe, sie nicht lokal zu installieren, sondern im Container auszuführen:

  1. Feste, reproduzierbare Version. Der Image-Tag pinnt die exakte Version. Jeder im Team, jede CI-Pipeline und du selbst in zwei Jahren verwenden dasselbe Binary, ohne Versions-Manager wie tfenv.
  2. Kein Zustand auf dem Host. Nach dem Lauf ist die Umgebung wieder sauber. Kein globaler Plugin-Cache, keine halb aktualisierte Installation.
  3. Mehrere Versionen parallel. Du kannst die eingefrorene freie Version 1.5.7 und ein aktuelles OpenTofu nebeneinander betreiben, ohne dass sie sich in die Quere kommen. Genau das braucht man, um eine Migration zu üben.

Die Technik ist tool-unabhängig: Was hier für Terraform gilt, gilt genauso für tofu, kubectl oder jedes andere CLI, das als Image vorliegt.

Der Alias im Überblick

Das Ziel ist ein Shell-Alias, der sich anfühlt wie ein lokal installiertes terraform:

alias terraform='docker run -it --rm \
  -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e HCLOUD_TOKEN \
  -u $(id -u):$(id -g) \
  -v $(pwd):/app \
  hashicorp/terraform:1.5.7 -chdir=/app'

Danach fühlt sich der Aufruf lokal an:

terraform init
terraform plan
terraform apply

Jeder dieser Befehle startet im Hintergrund einen frischen Container, führt genau einen Terraform-Lauf aus und räumt ihn wieder weg. Sehen wir uns an, warum jedes einzelne Flag da steht.

Der Alias Flag für Flag

-it: interaktives Terminal

-i hält die Standardeingabe offen, -t weist ein Pseudo-Terminal zu. Zusammen sorgen sie dafür, dass Farben korrekt dargestellt werden und interaktive Rückfragen funktionieren, etwa das yes bei terraform apply.

Wichtig für später: In einer CI-Pipeline gibt es kein TTY. Dort lässt du -t weg (nur -i oder gar nichts), sonst bricht der Lauf mit the input device is not a TTY ab. Mehr dazu im Abschnitt zur CI.

--rm: Container nach dem Lauf entfernen

Ohne --rm bliebe nach jedem terraform-Aufruf ein gestoppter Container liegen. Bei einem Tool, das du dutzende Male am Tag aufrufst, sammeln sich die schnell an. --rm räumt sofort auf. Der Zustand, der dich interessiert, liegt ohnehin nicht im Container, sondern im gemounteten Verzeichnis (siehe -v).

-e VAR ohne Wert: Credentials aus der Shell durchreichen

-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e HCLOUD_TOKEN

Ein -e VAR ohne =wert reicht den Wert aus deiner aktuellen Shell-Umgebung an den Container durch. Das ist der entscheidende Sicherheits-Trick: Die Secrets stehen nirgends im Alias und nirgends in einer Datei, sondern nur in deiner Shell:

export AWS_ACCESS_KEY_ID=...      # z. B. Hetzner S3 Access Key
export AWS_SECRET_ACCESS_KEY=...  # Hetzner S3 Secret
export HCLOUD_TOKEN=...           # Hetzner Cloud API Token

So wandert kein Zugangsschlüssel versehentlich in ein Git-Repository. Wer viele Variablen hat, kann alternativ eine Datei mit --env-file secrets.env laden. Diese Datei gehört dann aber zwingend in die .gitignore.

-u $(id -u):$(id -g): als du selbst, nicht als root

Das ist das wichtigste Flag für den Alltag. Ohne -u läuft der Prozess im Container als root. Alles, was Terraform in das gemountete Verzeichnis schreibt, etwa .terraform.lock.hcl, das .terraform-Verzeichnis oder eine lokale State-Datei, gehört danach auf dem Host root. Du brauchst sudo, um deine eigenen Dateien wieder aufzuräumen.

-u $(id -u):$(id -g) startet den Container mit deiner User- und Gruppen-ID. Die erzeugten Dateien gehören dir. id -u und id -g werden beim Aufruf des Alias von der Shell eingesetzt, nicht bei seiner Definition, deshalb funktioniert das auch, wenn du den Alias auf einem anderen Rechner verwendest.

Stolperstein Home-Verzeichnis: Eine numerische UID hat im Container keinen Eintrag in /etc/passwd, also fällt $HOME auf / zurück. Solange du nur init, plan und apply fährst, stört das nicht. Will ein Befehl aber ins Home schreiben (z. B. terraform login, das Credentials unter ~/.terraform.d ablegt), scheitert er an fehlenden Rechten. Abhilfe: ein beschreibbares Home mitgeben, -e HOME=/tmp.

-v $(pwd):/app: dein Projekt in den Container

-v $(pwd):/app mountet das aktuelle Verzeichnis nach /app im Container. Das ist die Brücke zwischen Host und Container: Deine .tf-Dateien werden im Container sichtbar, und alles, was Terraform erzeugt, landet wieder bei dir auf der Platte, nicht im flüchtigen Container.

Dadurch überlebt auch das .terraform-Verzeichnis den Lauf, obwohl der Container mit --rm verschwindet: Es liegt ja auf dem Host. So muss terraform init die Provider nicht bei jedem Aufruf neu herunterladen (siehe Stolpersteine).

hashicorp/terraform:1.5.7: das Image mit festem Tag

Der Tag pinnt die Version. 1.5.7 ist bewusst gewählt: Es ist die letzte unter der MPL 2.0 veröffentlichte und damit letzte quelloffene Terraform-Version. Alles danach steht unter der Business Source License.

Wähle den Tag also mit Absicht und niemals :latest, denn genau die Reproduzierbarkeit, wegen der du das Tool in den Container steckst, würde :latest wieder zunichtemachen.

-chdir=/app: das Arbeitsverzeichnis für Terraform

-chdir ist eine globale Option von Terraform selbst, keine von Docker. Sie muss vor dem Unterbefehl stehen. Da das Image terraform als Entrypoint hat, hängt Docker alles nach dem Image-Namen an dieses Binary an. Aus

terraform plan

wird also im Container

terraform -chdir=/app plan

Damit arbeitet Terraform in /app, wohin wir mit -v unser Projekt gemountet haben. Ohne -chdir liefe Terraform im Standard-Arbeitsverzeichnis des Images und fände deine Konfiguration nicht.

Dasselbe mit OpenTofu

Weil die Technik nur den Container-Aufruf betrifft und nicht das Tool, sieht der OpenTofu-Alias fast identisch aus. Es ändern sich nur der Image-Name und der Alias-Name:

alias tofu='docker run -it --rm \
  -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e HCLOUD_TOKEN \
  -u $(id -u):$(id -g) \
  -v $(pwd):/app \
  ghcr.io/opentofu/opentofu:1.12.5 -chdir=/app'

Genau diese Symmetrie ist praktisch: Du kannst terraform (auf 1.5.7 eingefroren) und tofu (aktuell) nebeneinander betreiben und beide gegen dieselbe Konfiguration laufen lassen, ohne irgendetwas lokal zu installieren. Das ist die Grundlage, um eine Migration gefahrlos zu üben, bevor du sie auf echte Infrastruktur anwendest.

Stolpersteine

Provider-Download bei jedem Lauf

Weil jeder Alias-Aufruf mit --rm einen frischen Container startet, gibt es keinen containerinternen Zwischenspeicher. Terraform lädt seine Provider aber nach .terraform im Arbeitsverzeichnis, und das ist gemountet. Deshalb bleiben die Provider doch über Läufe hinweg erhalten, solange du im selben Projektverzeichnis bleibst. Löschst du .terraform, lädt das nächste init erneut.

Willst du den Cache über mehrere Projekte hinweg teilen, kannst du zusätzlich ein Plugin-Cache-Verzeichnis mounten und über die Umgebungsvariable TF_PLUGIN_CACHE_DIR darauf zeigen.

Der TTY in der CI

Das -t aus -it verlangt ein echtes Terminal. Interaktiv am Laptop ist das gegeben, in einer CI-Pipeline nicht. Dort brichst du sonst mit the input device is not a TTY ab. Für die CI also -t weglassen und Terraform nicht interaktiv fahren:

docker run -i --rm \
  -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e HCLOUD_TOKEN \
  -u $(id -u):$(id -g) \
  -v "$(pwd):/app" \
  hashicorp/terraform:1.5.7 -chdir=/app apply -auto-approve

-auto-approve ersetzt die interaktive yes-Rückfrage, die ohne TTY ohnehin nicht funktionieren würde.

Die Plattform im Lock-File

Terraform lädt die Provider im Container, also für dessen Plattform (typisch linux_amd64). Die daraus erzeugte .terraform.lock.hcl enthält genau die Hashes dieser Plattform. Läuft dein Host ebenfalls unter Linux (oder WSL2), passt das zusammen. Committen Kolleg:innen auf macOS aber ihre lokal erzeugten Hashes, kann es zu Abweichungen kommen.

Sauber löst man das, indem man die Hashes für alle relevanten Plattformen explizit in das Lock-File schreiben lässt:

terraform providers lock \
  -platform=linux_amd64 \
  -platform=darwin_arm64

Relative Pfade in der Konfiguration

Alles, worauf deine Konfiguration per relativem Pfad zugreift, muss unterhalb des gemounteten Verzeichnisses liegen, sonst ist es im Container nicht sichtbar. Ein ../shared/variables.tf außerhalb von $(pwd) existiert im Container nicht. Mounte in solchen Fällen ein übergeordnetes Verzeichnis und passe -chdir entsprechend an.

Fazit

Ein einzelner Shell-Alias genügt, um Terraform oder OpenTofu ohne lokale Installation zu betreiben, und zwar reproduzierbar und in einer klar gepinnten Version. Die wichtigsten Punkte:

  1. -e VAR ohne Wert reicht Secrets aus der Shell durch, sodass sie nie in einer Datei landen.
  2. -u $(id -u):$(id -g) sorgt dafür, dass erzeugte Dateien dir gehören und nicht root.
  3. -v $(pwd):/app zusammen mit -chdir=/app verbindet dein Projekt mit dem Container.
  4. Pinne den Tag bewusst und meide :latest. 1.5.7 ist die letzte freie Terraform-Version, für Neues nimm gleich OpenTofu.
  5. In der CI -t weglassen und -auto-approve setzen.

Damit hast du eine saubere, herstellerunabhängige Arbeitsumgebung, die auf jedem Rechner mit Docker identisch funktioniert.

Newsletter

Erhalte praxisnahe Tutorials, Best Practices und echte Lösungen für die häufigsten DevOps-Probleme direkt in Dein Postfach.

Datenschutz: Deine E-Mail-Adresse wird DSGVO-konform in Deutschland gespeichert. Keine Spam, nur relevante Inhalte. Eine E-Mail alle 1 bis 2 Wochen.