Previously named OpenTF, OpenTofu is a fork of Terraform that is open-source, community-driven, and managed by the Linux Foundation.

OpenTofu lets you define your infrastructure as code (IaC) and manage it with providers like:

πŸ”₯ Let’s deploy a Docker instance together! πŸ”₯

Start with defining the files we need for OpenTofu:

### Get started ###

# Install OpenTofu
# https://opentofu.org/docs/intro/install/

tofu -version

### Create the tofu setup ###
mkdir docker_instance
cd docker_instance

# Create the files
touch provider.tf main.tf

The content of the main.tf and provider.tf file.

# provider.tf
# Define the hetzner cloud provider
terraform {
  required_providers {
    hcloud = {
      source  = "opentofu/hcloud"
      version = "1.26.1"
    }
  }
}

# main.tf
# Set the variable value in *.tfvars file
# or using the -var="hcloud_token=..." CLI option
# or use env vars like TF_VAR_hcloud_token=
variable "hcloud_token" {
  type    = string
  default = ""
}

variable "ssh_key" {
  type = string

  # Set the default to your key or override
  default = "~/.ssh/id_ed25519.pub"
}

# Configure the Hetzner Cloud Provider
provider "hcloud" {
  token = var.hcloud_token
}

# Create a new SSH key
resource "hcloud_ssh_key" "sudo" {
  name       = "ssh key"
  public_key = file(var.ssh_key)
}

# Set up firewalls 
## Port 80 for unencrypted http
resource "hcloud_firewall" "http" {
  name = "allow_http"
  rule {
    direction = "in"
    protocol  = "tcp"
    port      = 80
    source_ips = [
      "0.0.0.0/0",
      "::/0"
    ]
  }
}

## Allow SSL (https)
resource "hcloud_firewall" "https" {
  name = "allow_https"
  rule {
    direction = "in"
    protocol  = "tcp"
    port      = 443
    source_ips = [
      "0.0.0.0/0",
      "::/0"
    ]
  }
}

## Set the SSH access
resource "hcloud_firewall" "ssh" {
  name = "allow_ssh"
  rule {
    direction = "in"
    protocol  = "tcp"
    port      = 22
    # Limit this to your IP
    # 0.0.0.0/0 means open for all IPs
    source_ips = ["0.0.0.0/0"]
  }
}
# end firewall setup

# Create the docker server
resource "hcloud_server" "docker" {
  name = "docker"
  # Using the predefined docker image from Hetzner
  image        = "docker"
  server_type  = "cpx11"
  location     = "hel1"
  firewall_ids = [hcloud_firewall.http.id, hcloud_firewall.https.id, hcloud_firewall.ssh.id]
  ssh_keys     = [hcloud_ssh_key.sudo.id]

  labels = {
    "purpose" = "Docker installation"
  }
}

# Output the IP addresses of the created servers
output "server_ips" {
  value = hcloud_server.docker.ipv4_address
}

Install the tofu provider, and apply the infrastructure to Hetzner.

### Create the Hetzner instance ###
# Install the tofu providers
tofu init

# Dry run with a plan
# this will not apply it, only view the output 
# planned to be run
tofu plan

# Apply it to Hetzner

# Ideally you should not have this env var within the shell historu
TF_VAR_hcloud_token=1337 tofu apply

hcloud_ssh_key.sudo: Creating...
hcloud_firewall.ssh: Creating...
hcloud_firewall.https: Creating...
hcloud_firewall.http: Creating...
hcloud_ssh_key.sudo: Creation complete after 1s [id=24835347]
hcloud_firewall.http: Creation complete after 1s [id=1756096]
hcloud_firewall.https: Creation complete after 2s [id=1756097]
hcloud_firewall.ssh: Creation complete after 2s [id=1756098]
hcloud_server.docker: Creating...
hcloud_server.docker: Still creating... [10s elapsed]
hcloud_server.docker: Still creating... [20s elapsed]
hcloud_server.docker: Still creating... [30s elapsed]
hcloud_server.docker: Still creating... [40s elapsed]
hcloud_server.docker: Creation complete after 46s [id=56559157]

Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

Outputs:

server_ips = "65.108.80.226"

Here is an example Dockerfile where we can deploy an nginx instance that will be exposed on port 80.

# Use the official nginx image as base
FROM nginx:alpine

# Create index.html directly in the container
RUN echo '<!DOCTYPE html>\
<html>\
<head>\
    <title>Hello, World</title>\
</head>\
<body>\
    <div class="container">\
        <h1>Welcome to my app!</h1>\
        <p>If you see this page, the nginx web server is successfully installed and working.</p>\
        <p>This page was created from within the Dockerfile.</p>\
    </div>\
</body>\
</html>' > /usr/share/nginx/html/index.html

# Expose port 80
EXPOSE 80

# Start nginx in the foreground
CMD ["nginx", "-g", "daemon off;"]

Since we have the docker instance up and running, and a Dockerfile ready.

Install copepod and run this command to deploy the docker image to the docker instance

-> github.com/bjarneo/copepod

copepod --host=65.108.80.226 \
  --user=root \
  --image=hello-world \
  --tag=v1.0.0 \
  --ssh=~/.ssh/id_ed25519.pub \
  --container-name=hello-world \
  --host-port=80 \
  --container-port=80

# ... more logs
[2024-11-23T13:29:23Z] INFO: Verifying container status...
[2024-11-23T13:29:23Z] INFO: Executing: ssh root@65.108.80.226 "docker ps --filter name=hello-world --format '{{.Status}}'"
[2024-11-23T13:29:24Z] INFO: Deployment completed successfully! πŸš€

Verify that the app is running as expected

http -h 65.108.80.226

HTTP/1.1 200 OK
Accept-Ranges: bytes
Connection: keep-alive
Content-Length: 327
Content-Type: text/html
Date: Sat, 23 Nov 2024 13:43:27 GMT
ETag: "6734bbf1-147"
Last-Modified: Wed, 13 Nov 2024 14:47:13 GMT
Server: nginx/1.27.2

Perfect. Well done!

Next step you can try for yourself is to -> Use Cloudflare and point your domain with SSL to the server ip -> Profit

You can destroy the entire server with one command to clean up after the testing.

tofu destroy

hcloud_server.docker: Destroying... [id=56559157]
hcloud_server.docker: Destruction complete after 0s
hcloud_ssh_key.sudo: Destroying... [id=24835347]
hcloud_firewall.ssh: Destroying... [id=1756098]
hcloud_firewall.http: Destroying... [id=1756096]
hcloud_firewall.https: Destroying... [id=1756097]
hcloud_ssh_key.sudo: Destruction complete after 1s
hcloud_firewall.https: Destruction complete after 1s
hcloud_firewall.http: Destruction complete after 2s
hcloud_firewall.ssh: Destruction complete after 2s

Destroy complete! Resources: 5 destroyed.