Running one Pterodactyl node is simple. Log in via SSH, install Wings, point it at the panel, done.
Running ten nodes, or responding to a customer purchasing a server at 3am and expecting it to be ready in seconds, is a different problem. Manual execution at that scale creates inconsistency and operational overhead that compounds as you grow.
This guide covers building an automated deployment pipeline that takes a fresh VPS and turns it into a functioning Pterodactyl node without any manual SSH sessions, using Python and Paramiko.
What Paramiko Is
Paramiko is a Python library that implements the SSH2 protocol. It lets Python scripts open SSH connections, execute commands, transfer files, and handle interactive prompts, all programmatically.
Install it:
pip install paramiko
That is the only external dependency for the SSH execution layer. The rest of this guide uses standard Python libraries.
The Deployment Workflow
The automated pipeline performs these steps in sequence:
- Open an SSH connection to the new VPS
- Update the system and install Docker
- Download and install the Wings binary
- Write the Wings configuration file with the correct panel address and token
- Register the node in Pterodactyl via the API
- Start and enable the Wings service
- Report status back to a monitoring system
Each step is idempotent: running it twice produces the same result as running it once. This lets the pipeline recover from partial failures by re-running from the beginning.
Basic SSH Execution With Paramiko
import paramiko
import time
def run_ssh_command(client, command, timeout=120):
"""Execute a command over SSH and return stdout, stderr, and exit code."""
stdin, stdout, stderr = client.exec_command(command, timeout=timeout)
# Wait for the command to finish
exit_code = stdout.channel.recv_exit_status()
output = stdout.read().decode('utf-8').strip()
errors = stderr.read().decode('utf-8').strip()
return output, errors, exit_code
def connect(hostname, username, private_key_path):
"""Open a persistent SSH connection."""
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
key = paramiko.RSAKey.from_private_key_file(private_key_path)
client.connect(hostname, username=username, pkey=key, timeout=30)
return client
Use key-based authentication, not passwords. Passwords in scripts are a security problem. Generate a deploy key pair and add the public key to each new VPS automatically at provisioning time.
Installing Docker
def install_docker(client):
commands = [
"apt-get update -y",
"apt-get install -y curl ca-certificates gnupg",
"install -m 0755 -d /etc/apt/keyrings",
"curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg",
'echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null',
"apt-get update -y",
"apt-get install -y docker-ce docker-ce-cli containerd.io",
"systemctl enable docker",
"systemctl start docker",
]
for cmd in commands:
output, errors, code = run_ssh_command(client, cmd)
if code != 0:
raise RuntimeError(f"Docker install failed at: {cmd}\nError: {errors}")
print("Docker installed successfully")
Installing Wings
def install_wings(client, panel_url, wings_token, node_id):
# Download the Wings binary
commands = [
"mkdir -p /etc/pterodactyl",
"curl -L -o /usr/local/bin/wings https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_amd64",
"chmod u+x /usr/local/bin/wings",
]
for cmd in commands:
output, errors, code = run_ssh_command(client, cmd)
if code != 0:
raise RuntimeError(f"Wings install failed: {errors}")
# Write Wings configuration
config = f"""debug: false
uuid: {node_id}
token_id: "placeholder"
token: "{wings_token}"
api:
host: 0.0.0.0
port: 443
ssl:
enabled: true
cert: /etc/letsencrypt/live/your-node.example.com/fullchain.pem
key: /etc/letsencrypt/live/your-node.example.com/privkey.pem
upload_limit: 100
system:
data: /var/lib/pterodactyl/volumes
sftp:
bind_port: 2022
allowed_mounts: []
remote: "{panel_url}"
"""
# Write config via heredoc
escaped = config.replace('"', '\\"').replace('$', '\\$')
cmd = f'cat > /etc/pterodactyl/config.yml << \'ENDOFCONFIG\'\n{config}\nENDOFCONFIG'
run_ssh_command(client, cmd)
Creating a Systemd Service
def setup_wings_service(client):
service_content = """[Unit]
Description=Pterodactyl Wings Daemon
After=docker.service
Requires=docker.service
PartOf=docker.service
[Service]
User=root
WorkingDirectory=/etc/pterodactyl
LimitNOFILE=4096
PIDFile=/var/run/wings/daemon.pid
ExecStart=/usr/local/bin/wings
Restart=on-failure
StartLimitInterval=180
StartLimitBurst=30
RestartSec=5s
[Install]
WantedBy=multi-user.target
"""
cmd = f"cat > /etc/systemd/system/wings.service << 'EOF'\n{service_content}\nEOF"
run_ssh_command(client, cmd)
run_ssh_command(client, "systemctl daemon-reload")
run_ssh_command(client, "systemctl enable wings")
run_ssh_command(client, "systemctl start wings")
# Wait for Wings to start and verify
time.sleep(5)
output, _, code = run_ssh_command(client, "systemctl is-active wings")
if output.strip() != "active":
raise RuntimeError("Wings failed to start")
print("Wings service started and enabled")
Registering the Node via the Pterodactyl API
Python's requests library handles the REST API calls to Pterodactyl:
import requests
def register_node_in_panel(panel_url, api_key, node_config):
"""Create a node record in Pterodactyl via the application API."""
headers = {
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
"Content-Type": "application/json",
}
payload = {
"name": node_config["name"],
"location_id": node_config["location_id"],
"fqdn": node_config["hostname"],
"scheme": "https",
"memory": node_config["memory_mb"],
"memory_overallocate": 0,
"disk": node_config["disk_mb"],
"disk_overallocate": 0,
"upload_size": 100,
"daemon_sftp": 2022,
"daemon_listen": 443,
}
response = requests.post(
f"{panel_url}/api/application/nodes",
headers=headers,
json=payload,
timeout=30
)
response.raise_for_status()
node_data = response.json()["attributes"]
return node_data["id"], node_data["daemon_token"]
Putting the Pipeline Together
def deploy_node(hostname, ssh_key, panel_url, api_key, node_config):
"""Full automated node deployment."""
print(f"Starting deployment for {hostname}")
# Connect
client = connect(hostname, "root", ssh_key)
try:
# Install dependencies
install_docker(client)
# Register node in panel first to get the token
node_id, wings_token = register_node_in_panel(panel_url, api_key, node_config)
# Install Wings with panel credentials
install_wings(client, panel_url, wings_token, node_id)
# Set up and start the service
setup_wings_service(client)
print(f"Node {hostname} deployed successfully. Node ID: {node_id}")
return node_id
except Exception as e:
print(f"Deployment failed for {hostname}: {e}")
raise
finally:
client.close()
Adding Monitoring and Alerting
After deployment, send a status report to a monitoring system or Discord webhook:
import requests
def report_status(webhook_url, hostname, node_id, success):
payload = {
"content": f"Node deployment {'succeeded' if success else 'FAILED'} for `{hostname}` (Node ID: {node_id})"
}
requests.post(webhook_url, json=payload)
Run this pipeline from a Discord bot, a cron job triggered by billing events, or a CI/CD system. The result is a freshly provisioned, fully functional Pterodactyl node available within minutes of receiving the request, with no human involvement.