OpenTofu Infrastructure as Code - Deploy a docker instance to Hetzner
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:
- EKS: Run Kubernetes clusters on AWS.
- Hetzner: Create VMs, firewalls, load balancers, and more.
- Explore: https://search.opentofu.org/providers
π₯ 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.