# dokploy Terraform + Ansible setup for a self-hosted [Dokploy](https://dokploy.com/) 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`: ```env TF_VAR_hcloud_token=... TF_VAR_cloudflare_api_token=... TF_VAR_domain_name=... ``` ## Local setup ### Prerequisites - Terraform `>= 1.0` - [direnv](https://direnv.net/) (recommended) - A Hetzner Cloud API token - A Cloudflare API token with `Zone.DNS` edit permission ### Steps ```sh # 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: ```sh 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: ```sh cd terraform set -a; . ./.env; set +a terraform plan ``` Or use a wrapper that keeps the variables contained to the `terraform` subprocess: ```sh #!/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: ```yaml 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`.