Files
dokploy/README.md
T

3.5 KiB

dokploy

Terraform + Ansible setup for a self-hosted Dokploy instance.

  • Terraform provisions a Hetzner Cloud VPS, registers an SSH key, creates a Cloudflare DNS record, and writes the Ansible inventory.
  • Ansible configures the host afterwards (base / users / security roles).

Repository layout

.
├── terraform/
│   ├── main.tf
│   ├── providers.tf
│   ├── variables.tf
│   ├── terraform.tfvars   # non-secret config (committed)
│   ├── .env.example       # template for required secrets (committed)
│   ├── .env               # real secrets (gitignored — created locally)
│   └── .envrc             # direnv loader (committed)
└── ansible/
    ├── playbook.yml
    ├── requirements.yml
    └── group_vars/

Secrets

Secrets are never committed and never written into .tf or .tfvars files. They live only in terraform/.env and are injected as TF_VAR_* environment variables, which Terraform maps to input variables automatically.

Variable Environment variable Committed to git
hcloud_token TF_VAR_hcloud_token No
cloudflare_api_token TF_VAR_cloudflare_api_token No
domain_name TF_VAR_domain_name No

Everything else (server type, image, location, project name, SSH public key, DNS record name, proxied flag) is non-secret and lives in terraform/terraform.tfvars.

.env format

terraform/.env:

TF_VAR_hcloud_token=...
TF_VAR_cloudflare_api_token=...
TF_VAR_domain_name=...

Local setup

Prerequisites

  • Terraform >= 1.0
  • direnv (recommended)
  • A Hetzner Cloud API token
  • A Cloudflare API token with Zone.DNS edit permission

Steps

# 1. Create your local secrets file from the template, then fill in the values.
cp terraform/.env.example terraform/.env

# 2. Allow direnv to load it (evaluates terraform/.envrc).
cd terraform
direnv allow

# 3. Provision.
terraform init
terraform plan
terraform apply

terraform/.envrc loads the .env file:

dotenv_if_exists .env

direnv loads the variables when you cd into terraform/ and unloads them when you leave, so secrets never linger in your shell.

Without direnv

Load the file manually, scoped to a single command:

cd terraform
set -a; . ./.env; set +a
terraform plan

Or use a wrapper that keeps the variables contained to the terraform subprocess:

#!/bin/sh
set -a
. ./.env
set +a
exec terraform "$@"

CI/CD

CI reads the same variables from its secret store — no .env file is needed.

GitHub Actions example:

env:
  TF_VAR_hcloud_token: ${{ secrets.TF_VAR_HCLOUD_TOKEN }}
  TF_VAR_cloudflare_api_token: ${{ secrets.TF_VAR_CLOUDFLARE_API_TOKEN }}
  TF_VAR_domain_name: ${{ secrets.TF_VAR_DOMAIN_NAME }}

steps:
  - uses: actions/checkout@v4
  - uses: hashicorp/setup-terraform@v3
  - run: terraform init
  - run: terraform plan
  - run: terraform apply -auto-approve

Security notes

  • Never commit terraform/.env — it is listed in .gitignore.
  • .env.example is a template only; it must never contain real values.
  • If a token is ever exposed (commit, paste, logs, screenshots), rotate it immediately in the Hetzner / Cloudflare consoles and update .env.
  • Terraform state files (*.tfstate) are gitignored; they can contain derived values such as the resulting DNS hostname.
  • For reproducible CI runs, consider committing terraform/.terraform.lock.hcl.