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.DNSedit 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.exampleis 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.