Provision virtual machine infrastructure for a deployment environment.
Creates and configures VM infrastructure using OpenTofu (Terraform). This command takes an environment from the "Created" state to the "Provisioned" state with running VM instances.
The provision command works with all supported providers:
- LXD - Creates local VMs for development and testing
- Hetzner Cloud - Creates cloud servers for production deployments
torrust-tracker-deployer provision <ENVIRONMENT><ENVIRONMENT>(required) - Name of the environment to provision
Control the amount of progress detail displayed during provisioning with the global -v flag. This helps you see what's happening under the hood when you need more visibility.
| Level | Flag | Shows | Use Case |
|---|---|---|---|
| Normal | (default) | Essential progress and results | Regular usage, clean output |
| Verbose | -v |
+ Detailed progress (9 provisioning steps) | Understanding the provisioning workflow |
| VeryVerbose | -vv |
+ Context details (paths, status, retries) | Troubleshooting common issues |
| Debug | -vvv |
+ Technical details (commands, parameters) | Deep troubleshooting, development debugging |
Important: Verbosity controls only progress messages. For internal diagnostic logs, use the RUST_LOG environment variable (see Logging Guide).
Shows essential progress with minimal output:
torrust-tracker-deployer provision my-envOutput:
⏳ [1/3] Validating environment...
⏳ ✓ Environment name validated: my-env (took 0ms)
⏳ [2/3] Creating command handler...
⏳ ✓ Done (took 0ms)
⏳ [3/3] Provisioning infrastructure...
⏳ ✓ Infrastructure provisioned (took 26.5s)
✅ Environment 'my-env' provisioned successfully
Instance Connection Details:
IP Address: 10.140.190.42
SSH Port: 22
...
Shows the 9 internal provisioning steps:
torrust-tracker-deployer provision my-env -vOutput:
⏳ [1/3] Validating environment...
⏳ ✓ Environment name validated: my-env (took 0ms)
⏳ [2/3] Creating command handler...
⏳ ✓ Done (took 0ms)
⏳ [3/3] Provisioning infrastructure...
📋 [Step 1/9] Rendering OpenTofu templates...
📋 [Step 2/9] Initializing OpenTofu...
📋 [Step 3/9] Validating infrastructure configuration...
📋 [Step 4/9] Planning infrastructure changes...
📋 [Step 5/9] Applying infrastructure changes...
📋 [Step 6/9] Retrieving instance information...
📋 [Step 7/9] Rendering Ansible templates...
📋 [Step 8/9] Waiting for SSH connectivity...
📋 [Step 9/9] Waiting for cloud-init completion...
⏳ ✓ Infrastructure provisioned (took 26.5s)
✅ Environment 'my-env' provisioned successfully
When to use: Understanding the provisioning workflow, seeing which step is taking time, or confirming the command is making progress.
Adds contextual details like paths, validation results, and retry attempts:
torrust-tracker-deployer provision my-env -vvOutput:
⏳ [3/3] Provisioning infrastructure...
📋 [Step 1/9] Rendering OpenTofu templates...
📋 → Generated OpenTofu configuration files
📋 [Step 2/9] Initializing OpenTofu...
📋 → Initialized OpenTofu backend
📋 [Step 3/9] Validating infrastructure configuration...
📋 → Configuration is valid ✓
📋 [Step 4/9] Planning infrastructure changes...
📋 → Plan: 2 to add, 0 to change, 0 to destroy.
📋 [Step 5/9] Applying infrastructure changes...
📋 → Infrastructure resources created successfully
📋 [Step 6/9] Retrieving instance information...
📋 → Instance IP: 10.140.190.42
📋 [Step 7/9] Rendering Ansible templates...
📋 → Template directory: ./build/my-env/ansible
📋 → Generated inventory and playbooks
📋 [Step 8/9] Waiting for SSH connectivity...
📋 → Testing connection to 10.140.190.42:22
📋 → SSH connection established ✓
📋 [Step 9/9] Waiting for cloud-init completion...
📋 → Cloud-init status: done ✓
⏳ ✓ Infrastructure provisioned (took 26.5s)
When to use: Troubleshooting SSH connectivity issues, verifying file locations, understanding why a step failed, or monitoring retry attempts.
Shows technical implementation details including commands executed:
torrust-tracker-deployer provision my-env -vvvOutput:
⏳ [3/3] Provisioning infrastructure...
📋 [Step 1/9] Rendering OpenTofu templates...
🔍 → Template generator: torrust_tracker_deployer_lib::infrastructure::templating::tofu::...
📋 → Generated OpenTofu configuration files
📋 [Step 2/9] Initializing OpenTofu...
🔍 → Working directory: ./build/my-env/tofu/lxd
🔍 → Executing: tofu init
🔍 → Command completed successfully
📋 → Initialized OpenTofu backend
📋 [Step 3/9] Validating infrastructure configuration...
🔍 → Working directory: ./build/my-env/tofu/lxd
🔍 → Executing: tofu validate
🔍 → Validation output: Success! The configuration is valid.
📋 → Configuration is valid ✓
📋 [Step 4/9] Planning infrastructure changes...
🔍 → Working directory: ./build/my-env/tofu/lxd
🔍 → Executing: tofu plan -var-file=variables.tfvars
📋 → Plan: 2 to add, 0 to change, 0 to destroy.
📋 [Step 5/9] Applying infrastructure changes...
🔍 → Working directory: ./build/my-env/tofu/lxd
🔍 → Executing: tofu apply -var-file=variables.tfvars -auto-approve
📋 → Infrastructure resources created successfully
...
When to use: Deep debugging, understanding exactly what commands are executed, verifying working directories, or reporting issues with detailed context.
Symbol Legend:
- ⏳ = Major progress milestone (all levels)
- ✅ = Success message (all levels)
- 📋 = Detailed progress (Verbose
-vand above) - 🔍 = Technical details (Debug
-vvvonly)
Verbosity works with all other flags:
# Verbose output with JSON result format
torrust-tracker-deployer provision my-env -v --output-format json
# Debug verbosity with file-only logging
torrust-tracker-deployer provision my-env -vvv --log-output file-only
# Very verbose in CI environment
LOG_OUTPUT=file-only torrust-tracker-deployer provision my-env -vvThe provision command supports two output formats for command results:
- Text (default) - Human-readable formatted output
- JSON - Machine-readable JSON for automation
Use the global --output-format flag to control the format.
The default output format provides human-readable information with visual formatting:
torrust-tracker-deployer provision full-stack-docsOutput:
⏳ [1/3] Validating environment...
⏳ ✓ Environment name validated: full-stack-docs (took 0ms)
⏳ [2/3] Creating command handler...
⏳ ✓ Done (took 0ms)
⏳ [3/3] Provisioning infrastructure...
⏳ ✓ Infrastructure provisioned (took 26.5s)
✅ Environment 'full-stack-docs' provisioned successfully
Instance Connection Details:
IP Address: 10.140.190.211
SSH Port: 22
SSH Private Key: /home/josecelano/Documents/git/committer/me/github/torrust/torrust-tracker-deployer-agent-01/fixtures/testing_rsa
SSH Username: torrust
Connect using:
ssh -i /home/josecelano/Documents/git/committer/me/github/torrust/torrust-tracker-deployer-agent-01/fixtures/testing_rsa torrust@10.140.190.211 -p 22
⚠️ DNS Setup Required:
Your configuration uses custom domains. Remember to update your DNS records
to point your domains to the server IP: 10.140.190.211
Configured domains:
- tracker1.example.com
- tracker2.example.com
- api.example.com
- grafana.example.com
- health.example.com
Features:
- Progress indicators (✓)
- Numbered list format
- Clear section organization
- Color-coded status messages
Use --output-format json for machine-readable output ideal for automation, scripts, and programmatic processing:
torrust-tracker-deployer provision my-environment --output-format jsonOutput:
{
"environment_name": "my-environment",
"instance_name": "torrust-tracker-vm-my-environment",
"instance_ip": "10.140.190.42",
"ssh_private_key_path": "/home/user/.ssh/id_rsa",
"ssh_public_key_path": "/home/user/.ssh/id_rsa.pub",
"ssh_username": "torrust",
"ssh_port": 22,
"provider": "lxd",
"domains": [],
"provisioned_at": "2026-02-16T13:38:02.446056727Z"
}Features:
- Valid, parseable JSON
- Pretty-printed for readability
- ISO 8601 timestamps
- Consistent field ordering
- Ready for immediate SSH automation
| Field | Type | Description | Example |
|---|---|---|---|
environment_name |
string | Name of the environment | "production" |
instance_name |
string | Full VM instance name | "torrust-tracker-vm-production" |
instance_ip |
string | IP address of provisioned VM | "10.140.190.42" |
ssh_private_key_path |
string | Path to SSH private key | "/home/user/.ssh/id_rsa" |
ssh_public_key_path |
string | Path to SSH public key | "/home/user/.ssh/id_rsa.pub" |
ssh_username |
string | SSH username for VM access | "torrust" |
ssh_port |
number | SSH port number | 22 |
provider |
string | Provider used ("lxd" or "hetzner") | "lxd" |
domains |
string[] | Configured domains (HTTPS only) | ["tracker.example.com"] |
provisioned_at |
string | ISO 8601 timestamp of provisioning | "2026-02-16T13:38:02.446056727Z" |
Use the -o alias for shorter commands:
torrust-tracker-deployer provision my-environment -o json#!/bin/bash
# Provision environment and capture JSON output
JSON_OUTPUT=$(torrust-tracker-deployer provision my-environment \
--output-format json \
--log-output file-only)
# Extract instance IP and SSH credentials using jq
INSTANCE_IP=$(echo "$JSON_OUTPUT" | jq -r '.instance_ip')
SSH_KEY=$(echo "$JSON_OUTPUT" | jq -r '.ssh_private_key_path')
SSH_USER=$(echo "$JSON_OUTPUT" | jq -r '.ssh_username')
echo "VM provisioned at: $INSTANCE_IP"
# Connect to the instance
ssh -i "$SSH_KEY" "$SSH_USER@$INSTANCE_IP"name: Deploy Tracker
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Provision Infrastructure
id: provision
run: |
OUTPUT=$(torrust-tracker-deployer provision ${{ env.ENVIRONMENT_NAME }} \
--output-format json \
--log-output file-only)
echo "ip=$(echo $OUTPUT | jq -r '.instance_ip')" >> $GITHUB_OUTPUT
echo "ssh_key=$(echo $OUTPUT | jq -r '.ssh_private_key_path')" >> $GITHUB_OUTPUT
echo "ssh_user=$(echo $OUTPUT | jq -r '.ssh_username')" >> $GITHUB_OUTPUT
- name: Configure DNS
if: ${{ steps.provision.outputs.ip != '' }}
run: |
# Update DNS A record with provisioned IP
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
-H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
-H "Content-Type: application/json" \
--data '{"type":"A","name":"tracker","content":"${{ steps.provision.outputs.ip }}"}'
- name: Deploy Application
run: |
ssh -i "${{ steps.provision.outputs.ssh_key }}" \
"${{ steps.provision.outputs.ssh_user }}@${{ steps.provision.outputs.ip }}" \
'cd /opt/torrust && docker compose up -d'import json
import subprocess
from typing import Dict, List
def provision_environment(env_name: str) -> Dict[str, any]:
"""Provision an environment and return provisioning details."""
result = subprocess.run(
[
"torrust-tracker-deployer",
"provision",
env_name,
"--output-format", "json",
"--log-output", "file-only"
],
capture_output=True,
text=True,
check=True
)
return json.loads(result.stdout)
def deploy_multi_region(regions: List[str]):
"""Deploy tracker to multiple regions in parallel."""
deployments = []
for region in regions:
env_name = f"tracker-{region}"
print(f"Provisioning {env_name}...")
details = provision_environment(env_name)
deployments.append({
"region": region,
"ip": details["instance_ip"],
"ssh_user": details["ssh_username"],
"ssh_key": details["ssh_private_key_path"],
"provisioned_at": details["provisioned_at"]
})
# Save deployment manifest
with open("deployments.json", "w") as f:
json.dump(deployments, f, indent=2)
print(f"\nDeployed to {len(deployments)} regions")
for deployment in deployments:
print(f" {deployment['region']}: {deployment['ip']}")
# Deploy to multiple regions
deploy_multi_region(["us-east", "eu-west", "ap-southeast"])# external-data.tf
# Use provisioned VM in downstream Terraform configuration
data "external" "provision_vm" {
program = ["bash", "-c", <<-EOF
torrust-tracker-deployer provision my-environment \
--output-format json \
--log-output file-only
EOF
]
}
resource "cloudflare_record" "tracker_a_record" {
zone_id = var.cloudflare_zone_id
name = "tracker"
value = data.external.provision_vm.result.instance_ip
type = "A"
ttl = 300
}
output "vm_ip" {
value = data.external.provision_vm.result.instance_ip
}
output "ssh_command" {
value = "ssh -i ${data.external.provision_vm.result.ssh_private_key_path} ${data.external.provision_vm.result.ssh_username}@${data.external.provision_vm.result.instance_ip}"
}#!/bin/bash
# Provision HTTPS environment
JSON_OUTPUT=$(torrust-tracker-deployer provision https-tracker \
--output-format json \
--log-output file-only)
# Extract domains array
DOMAINS=$(echo "$JSON_OUTPUT" | jq -r '.domains[]')
if [ -z "$DOMAINS" ]; then
echo "No domains configured - HTTP-only deployment"
else
echo "Configured domains:"
echo "$DOMAINS"
# Generate SSL certificates for each domain
for domain in $DOMAINS; do
echo "Requesting certificate for $domain..."
certbot certonly --standalone -d "$domain" --non-interactive --agree-tos
done
fi- Environment created - Must run
create environmentfirst - Provider-specific requirements:
- LXD: Local LXD installation configured
- Hetzner: Valid API token in environment configuration
- OpenTofu installed - OpenTofu CLI available in PATH
- SSH keys - SSH key pair referenced in environment configuration
📖 See Provider Guides for provider-specific setup.
[Created] --provision--> [Provisioned]
When you provision an environment:
- Renders OpenTofu templates - Generates provider-specific infrastructure-as-code files
- Initializes OpenTofu - Sets up backend and providers (
tofu init) - Creates execution plan - Validates configuration (
tofu plan) - Applies infrastructure - Creates VM resources (
tofu apply) - Retrieves instance info - Gets IP address and instance details
- Renders Ansible templates - Generates configuration management files
- Waits for SSH - Verifies network connectivity
- Waits for cloud-init - Ensures VM initialization is complete
- Updates environment state - Transitions to "Provisioned"
# Provision the environment with human-readable output
torrust-tracker-deployer provision my-environment
# Output:
# ✓ Rendering OpenTofu templates...
# ✓ Initializing infrastructure...
# ✓ Planning infrastructure changes...
# ✓ Applying infrastructure...
# ✓ Retrieving instance information...
# ✓ Instance IP: 10.140.190.42
# ✓ Rendering Ansible templates...
# ✓ Waiting for SSH connectivity...
# ✓ Waiting for cloud-init completion...
# ✓ Environment provisioned successfully
#
# Provisioning Details:
# 1. Environment name: my-environment
# 2. Instance name: torrust-tracker-vm-my-environment
# 3. Instance IP: 10.140.190.42
# ...# Provision the environment with JSON output for automation
torrust-tracker-deployer provision my-environment --output-format json
# Output (structured JSON):
# {
# "environment_name": "my-environment",
# "instance_name": "torrust-tracker-vm-my-environment",
# "instance_ip": "10.140.190.42",
# "ssh_private_key_path": "/home/user/.ssh/id_rsa",
# "ssh_public_key_path": "/home/user/.ssh/id_rsa.pub",
# "ssh_username": "torrust",
# "ssh_port": 22,
# "provider": "lxd",
# "domains": [],
# "provisioned_at": "2026-02-16T13:38:02.446056727Z"
# }# Provision and immediately extract IP address
IP=$(torrust-tracker-deployer provision my-environment \
--output-format json \
--log-output file-only | jq -r '.instance_ip')
echo "Provisioned VM at: $IP"
# Use IP in subsequent automation
ansible-playbook -i "$IP," deploy.yml# Development (local)
torrust-tracker-deployer provision dev-local
# Staging (cloud)
torrust-tracker-deployer provision staging --output-format json
# Production (Hetzner with JSON for automation)
torrust-tracker-deployer provision production -o jsonThe provision command creates provider-specific resources:
- VM instance - LXD virtual machine (
torrust-tracker-vm-<env-name>) - LXD profile - Custom profile with cloud-init configuration
- Network configuration - Bridged network with IP assignment
- OpenTofu state - Infrastructure state in
build/<env>/tofu/lxd/
- Cloud server - Hetzner Cloud server instance
- Firewall - Hetzner firewall with SSH access
- SSH key - Uploaded SSH public key
- OpenTofu state - Infrastructure state in
build/<env>/tofu/hetzner/
- Ansible inventory - Generated inventory in
build/<env>/ansible/ - Environment state update - State file updated to "Provisioned"
After provisioning:
# 1. Configure the infrastructure (install Docker, Docker Compose)
torrust-tracker-deployer configure my-environment
# 2. Verify infrastructure readiness
torrust-tracker-deployer test my-environmentProblem: Cannot find environment with the specified name
Solution: Verify the environment was created
# Check environment data directory exists
ls -la data/my-environment/
# If not, create the environment first
torrust-tracker-deployer create environment -f config.jsonProblem: LXD is not properly initialized
Solution: Initialize LXD
# Initialize LXD with default settings
sudo lxd init --auto
# Add your user to lxd group
sudo usermod -a -G lxd $USER
newgrp lxdProblem: OpenTofu CLI is not installed or not in PATH
Solution: Install OpenTofu
# Install OpenTofu
curl -fsSL https://get.opentofu.org/install-opentofu.sh | sudo bash
# Verify installation
tofu versionProblem: Cannot establish SSH connection to provisioned VM
Solution: Check network connectivity and cloud-init status
# Get VM IP address
lxc list
# Try to connect manually
ssh -i <path-to-private-key> torrust@<vm-ip>
# Check cloud-init status
lxc exec <instance-name> -- cloud-init statusProblem: LXD profile or instance name already exists
Solution: Clean up existing resources
# List existing instances
lxc list
# Delete old instance if needed
lxc delete <instance-name> --force
# List profiles
lxc profile list
# Delete old profile if needed
lxc profile delete <profile-name># Create, provision, and configure in sequence
torrust-tracker-deployer create environment -f dev.json
torrust-tracker-deployer provision dev-local
torrust-tracker-deployer configure dev-local#!/bin/bash
set -e
ENV_NAME="ci-${CI_JOB_ID}"
# Create environment
torrust-tracker-deployer create environment -f ci-config.json
# Provision infrastructure
torrust-tracker-deployer provision ${ENV_NAME}
# Run tests...
# Cleanup is handled by destroy commandIf you need to reprovision (destroy and create again):
# Destroy existing environment
torrust-tracker-deployer destroy my-environment
# Create fresh environment
torrust-tracker-deployer create environment -f config.json
# Provision again
torrust-tracker-deployer provision my-environmentLXD Resources:
- Instance:
torrust-tracker-vm-<environment-name> - Profile:
torrust-profile-<environment-name> - Network: Bridged network with DHCP
File Artifacts:
- OpenTofu files:
build/<env>/tofu/lxd/ - Ansible inventory:
build/<env>/ansible/inventory.yml - Instance info: Stored in environment state
The provisioned VM includes cloud-init configuration for:
- User account creation (SSH username from config)
- SSH key deployment (public key from config)
- Network configuration
- Initial system setup