 Autor: [Adam Nadolny](/autorzy/adam-nadolny) Ekspert DevOps i infrastruktury · Zweryfikowano Kwiecień 2026

1.  [Strona główna](/) ›
2.  [Baza wiedzy](/baza-wiedzy/) ›
3.  Terraform — VPS provisioning krok po kroku

# Terraform dla VPS — provisioning Hetzner Cloud i DigitalOcean krok po kroku

Opublikowano: 10 kwietnia 2026 · Kategoria: VPS

Ręczne tworzenie serwerów przez panel dostawcy działa dla jednego lub dwóch VPS-ów. Przy większej skali — wiele środowisk, wiele regionów, kilkanaście serwerów — ręczne zarządzanie staje się podatne na błędy i niepowtarzalne. Terraform rozwiązuje ten problem: opisujesz infrastrukturę w plikach tekstowych, wrzucasz do gita, a Terraform tworzy i zarządza zasobami przez API dostawcy. Ten artykuł pokazuje pełną konfigurację dla Hetzner Cloud (popularny dostawca VPS) i DigitalOcean, z remote state, modułami i workspace.

## Instalacja Terraform

\# Ubuntu / Debian
wget -O- https://apt.releases.hashicorp.com/gpg | \\
  sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb \[signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg\] \\
  https://apt.releases.hashicorp.com $(lsb\_release -cs) main" | \\
  sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform -y

# macOS
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

# Weryfikacja
terraform version

# Autocomplete (opcjonalnie)
terraform -install-autocomplete

## Struktura projektu Terraform

my-infra/
├── main.tf          # Zasoby (servers, networks, DNS)
├── variables.tf     # Deklaracje zmiennych
├── outputs.tf       # Outputy (IP, IDs)
├── providers.tf     # Konfiguracja providerów
├── backend.tf       # Remote state backend
├── terraform.tfvars # Wartości zmiennych (w .gitignore!)
└── modules/
    ├── vps/         # Modul reuzywany dla serwera
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    └── firewall/    # Modul dla firewall rules

## Provider Hetzner Cloud — kompletna konfiguracja

\# providers.tf
terraform {
  required\_version = ">= 1.6"
  required\_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = "~> 1.45"
    }
  }
}

provider "hcloud" {
  token = var.hcloud\_token
}

\# variables.tf
variable "hcloud\_token" {
  type        = string
  description = "Hetzner Cloud API token"
  sensitive   = true   # Ukrywa w logach
}

variable "server\_name" {
  type        = string
  description = "Nazwa serwera"
  default     = "web-prod-01"
}

variable "server\_type" {
  type        = string
  description = "Typ serwera (CX11, CX21, CX31...)"
  default     = "cx21"   # 2 vCPU, 4 GB RAM
}

variable "location" {
  type        = string
  description = "Datacenter Hetzner"
  default     = "nbg1"   # Nuremberg
  # Opcje: nbg1, fsn1, hel1, ash (US), hil (US), sin (Singapur)
}

variable "ssh\_key\_name" {
  type        = string
  description = "Nazwa klucza SSH w Hetzner (musi byc juz dodany)"
}

\# main.tf — zasoby Hetzner

# Klucz SSH (jesli chcesz go tez tworzyc przez Terraform)
resource "hcloud\_ssh\_key" "main" {
  name       = "my-key"
  public\_key = file("~/.ssh/id\_ed25519.pub")
}

# Siec prywatna (izolacja backendow)
resource "hcloud\_network" "private" {
  name     = "private-network"
  ip\_range = "10.0.0.0/8"
}

resource "hcloud\_network\_subnet" "private" {
  network\_id   = hcloud\_network.private.id
  type         = "cloud"
  network\_zone = "eu-central"
  ip\_range     = "10.0.1.0/24"
}

# Firewall
resource "hcloud\_firewall" "web" {
  name = "web-firewall"

  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "22"
    source\_ips = \["0.0.0.0/0", "::/0"\]
  }

  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "80"
    source\_ips = \["0.0.0.0/0", "::/0"\]
  }

  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "443"
    source\_ips = \["0.0.0.0/0", "::/0"\]
  }
}

# Serwer VPS
resource "hcloud\_server" "web" {
  name        = var.server\_name
  image       = "ubuntu-24.04"
  server\_type = var.server\_type
  location    = var.location
  ssh\_keys    = \[hcloud\_ssh\_key.main.id\]
  firewall\_ids = \[hcloud\_firewall.web.id\]
  backups     = true   # Automatyczny backup Hetzner (20% ceny serwera)

  # Podpnij do sieci prywatnej
  network {
    network\_id = hcloud\_network.private.id
    ip         = "10.0.1.10"
  }

  # Cloud-init — pierwsze uruchomienie
  user\_data = templatefile("${path.module}/cloud-init.yml", {
    hostname = var.server\_name
  })

  labels = {
    environment = "production"
    role        = "web"
    managed\_by  = "terraform"
  }
}

# Floating IP (statyczny IP — nie zmienia sie przy rebuild)
resource "hcloud\_floating\_ip" "web" {
  type          = "ipv4"
  home\_location = var.location
}

resource "hcloud\_floating\_ip\_assignment" "web" {
  floating\_ip\_id = hcloud\_floating\_ip.web.id
  server\_id      = hcloud\_server.web.id
}

\# outputs.tf
output "server\_ip" {
  description = "Publiczny IP serwera"
  value       = hcloud\_server.web.ipv4\_address
}

output "floating\_ip" {
  description = "Floating IP (nie zmienia sie przy rebuild)"
  value       = hcloud\_floating\_ip.web.ip\_address
}

output "private\_ip" {
  description = "IP w sieci prywatnej"
  value       = "10.0.1.10"
}

## Provider DigitalOcean — droplet i DNS

\# providers.tf — DigitalOcean
terraform {
  required\_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "~> 2.36"
    }
  }
}

provider "digitalocean" {
  token = var.do\_token
}

\# main.tf — DigitalOcean Droplet z DNS

data "digitalocean\_ssh\_key" "main" {
  name = "my-laptop"   # Klucz SSH juz dodany w DO console
}

resource "digitalocean\_droplet" "web" {
  name   = "web-prod"
  image  = "ubuntu-24-04-x64"
  size   = "s-2vcpu-4gb"   # 2 vCPU, 4 GB RAM
  region = "fra1"           # Frankfurt
  ssh\_keys = \[data.digitalocean\_ssh\_key.main.id\]
  backups  = true

  tags = \["production", "web", "terraform"\]
}

# DNS — podepnij domene do droplet
resource "digitalocean\_domain" "main" {
  name       = "example.com"
  ip\_address = digitalocean\_droplet.web.ipv4\_address
}

resource "digitalocean\_record" "www" {
  domain = digitalocean\_domain.main.id
  type   = "A"
  name   = "www"
  value  = digitalocean\_droplet.web.ipv4\_address
}

resource "digitalocean\_record" "mx" {
  domain   = digitalocean\_domain.main.id
  type     = "MX"
  name     = "@"
  value    = "mail.example.com."
  priority = 10
}

## Remote state — współdzielenie w zespole

\# backend.tf — S3-compatible remote state (dziala z MinIO, AWS S3, Cloudflare R2)
terraform {
  backend "s3" {
    bucket         = "terraform-state"
    key            = "prod/web/terraform.tfstate"
    region         = "us-east-1"   # Wymagane przez AWS SDK, moze byc dowolne
    endpoint       = "https://minio.example.com"   # MinIO endpoint
    access\_key     = "minio-access-key"
    secret\_key     = "minio-secret-key"
    skip\_credentials\_validation = true
    skip\_metadata\_api\_check     = true
    skip\_region\_validation      = true
    force\_path\_style            = true

    # State locking przez DynamoDB (AWS) lub nie uzywaj dla MinIO
    # dynamodb\_table = "terraform-state-lock"
  }
}

## Workspace — środowiska dev/staging/prod

\# Workspace pozwala uzywac tych samych plikow .tf dla roznych srodowisk
terraform workspace list     # Lista workspaces (domyslnie: default)
terraform workspace new dev  # Utwórz workspace "dev"
terraform workspace new prod
terraform workspace select prod   # Przejdz do prod

# Uzywaj workspace.name w zasobach
resource "hcloud\_server" "web" {
  name        = "web-${terraform.workspace}"   # web-prod, web-dev
  server\_type = terraform.workspace == "prod" ? "cx31" : "cx11"
  location    = terraform.workspace == "prod" ? "nbg1" : "hel1"
}

# Lub uzywaj osobnych .tfvars per workspace
# terraform apply -var-file="envs/${terraform.workspace}.tfvars"

## Workflow — plan, apply i destroy

\# Inicjalizacja projektu (pobiera providery)
terraform init

# Format i walidacja
terraform fmt -recursive    # Formatuje wszystkie pliki .tf
terraform validate          # Sprawdza skladnie bez api calls

# Plan — pokazuje co zostanie zmienione (BEZ WYKONANIA)
terraform plan
terraform plan -var-file="terraform.tfvars"
terraform plan -out=tfplan   # Zapisz plan do pliku

# Apply — wykonaj plan
terraform apply
terraform apply tfplan        # Wykonaj zapisany plan (bez pytania)
terraform apply -auto-approve # Bez potwierdzenia (uzyj w CI/CD)

# Sprawdz aktualny state
terraform show
terraform state list          # Lista zasobow
terraform state show hcloud\_server.web  # Szczegóły zasobu

# Importuj istniejacy zasob do state (gdy serwer jest juz utworzony recznie)
terraform import hcloud\_server.web 12345678

# Destroy — usun WSZYSTKO (UWAGA: nie ma cofniecia)
terraform destroy
terraform destroy -target=hcloud\_server.web  # Usun tylko jeden zasob

## Moduły — reużywalna infrastruktura

\# modules/vps/main.tf — modul reuzywany
resource "hcloud\_server" "this" {
  name        = var.name
  image       = var.image
  server\_type = var.server\_type
  location    = var.location
  ssh\_keys    = var.ssh\_key\_ids
  backups     = var.enable\_backups
}

# modules/vps/variables.tf
variable "name" { type = string }
variable "image" { type = string; default = "ubuntu-24.04" }
variable "server\_type" { type = string; default = "cx21" }
variable "location" { type = string; default = "nbg1" }
variable "ssh\_key\_ids" { type = list(string) }
variable "enable\_backups" { type = bool; default = false }

# modules/vps/outputs.tf
output "id" { value = hcloud\_server.this.id }
output "ipv4\_address" { value = hcloud\_server.this.ipv4\_address }

\# main.tf — uzycie modulu
module "web\_server" {
  source = "./modules/vps"

  name           = "web-prod"
  server\_type    = "cx31"
  location       = "nbg1"
  ssh\_key\_ids    = \[hcloud\_ssh\_key.main.id\]
  enable\_backups = true
}

module "db\_server" {
  source = "./modules/vps"

  name        = "db-prod"
  server\_type = "cx41"
  location    = "nbg1"
  ssh\_key\_ids = \[hcloud\_ssh\_key.main.id\]
}

# Publiczne registry modulow (np. terraform.io/modules)
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"
  # ...
}

## GitOps — CI/CD z Terraform

\# .github/workflows/terraform.yml
name: Terraform CI/CD

on:
  push:
    branches: \[main\]
  pull\_request:
    branches: \[main\]

jobs:
  terraform:
    runs-on: ubuntu-latest
    env:
      HCLOUD\_TOKEN: ${{ secrets.HCLOUD\_TOKEN }}

    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform\_version: "1.7.x"

      - name: Terraform Init
        run: terraform init

      - name: Terraform Validate
        run: terraform validate

      - name: Terraform Plan
        run: terraform plan -out=tfplan

      # Apply tylko na push do main (nie na PR)
      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event\_name == 'push'
        run: terraform apply -auto-approve tfplan

## Najczęstsze pytania

Czym jest Terraform i do czego go używa się przy VPS? +

Terraform to narzędzie IaC (Infrastructure as Code) — opisujesz infrastrukturę w plikach .tf (HCL), a Terraform tworzy ją przez API providera. Dla VPS: automatyczne tworzenie serwerów, sieci, firewalli, DNS, backupów przez API Hetzner, DigitalOcean, OVH lub innych. Zalety: infrastruktura w git, powtarzalność (identyczne środowiska dev/staging/prod), dokumentacja przez kod, rollback przez git revert, planowanie zmian przed zastosowaniem.

Czym Terraform różni się od Ansible? +

Terraform zarządza ZASOBAMI — tworzy, modyfikuje, usuwa serwery, sieci, DNS (provisioning). Ansible zarządza KONFIGURACJĄ — instaluje pakiety, kopiuje pliki, konfiguruje usługi na istniejącym serwerze. W praktyce używa się obu: Terraform tworzy VPS, Ansible konfiguruje system i aplikacje. Terraform jest deklaratywny (opisujesz stan docelowy), Ansible jest proceduralny (opisujesz kroki).

Co to jest Terraform state i gdzie go przechowywać? +

State (terraform.tfstate) to plik JSON mapujący zasoby Terraform na rzeczywiste zasoby u providera. Terraform porównuje state z plikami .tf żeby wiedzieć co dodać, zmienić lub usunąć. Lokalny state (domyślny) działa dla single-user. W team lub CI/CD używaj remote state: S3 + DynamoDB (AWS), Terraform Cloud, MinIO (self-hosted S3). Remote state zapobiega konfliktom przez state locking i jest współdzielony między członkami zespołu.

Jak bezpiecznie przechowywać tokeny API w Terraform? +

NIGDY nie wpisuj tokenów API bezpośrednio w plikach .tf — trafią do gita. Opcje: (1) Zmienne środowiskowe: export HCLOUD\_TOKEN=xxx, Terraform automatycznie je czyta. (2) Plik .tfvars poza gitem (.gitignore): hcloud\_token = "xxx". (3) Vault dynamic credentials. (4) GitHub Actions/GitLab CI secrets jako env vars. Dodaj \*.tfvars, .terraform/, terraform.tfstate do .gitignore.

## Sprawdź oferty pasujące do tego scenariusza

Poniżej masz szybkie przejścia do ofert i stron z kodami rabatowymi tam, gdzie są dostępne.

Contabo

VPS Contabo — stwórz przez Terraform i zarządzaj infrastrukturą jako kod

IaC ready

[Aktywuj rabat →](/out/contabo)

#Reklama · link partnerski

[Zobacz kod rabatowy →](/kody-rabatowe/contabo)

Mikr.us

Budżetowy VPS do nauki Terraform i testowania pipeline IaC

Dev/Test

[Aktywuj rabat →](/out/mikrus)

#Reklama · link partnerski

[Zobacz kod rabatowy →](/kody-rabatowe/mikrus)

LH.pl

Hosting współdzielony dla projektów nie wymagających własnej infrastruktury

Managed

[Aktywuj rabat →](/out/lh-pl)

#Reklama · link partnerski

[Zobacz kod rabatowy →](/kody-rabatowe/lh-pl)

## Powiązane strony

-   [Ansible — podstawy automatyzacji serwerów](/baza-wiedzy/ansible-podstawy-automatyzacja)
-   [Docker na VPS — instalacja i konfiguracja](/baza-wiedzy/docker-na-vps)
-   [GitHub Actions — deployment na VPS](/baza-wiedzy/github-actions-deployment-vps)
-   [Wszystkie artykuły](/baza-wiedzy/)