Automated Game Server Deployment Pipelines With Python and SSH

Published on

Managing a fleet of VPS nodes for game hosting manually doesn't scale. This guide shows how to build automated deployment pipelines using Python's Paramiko library and the Pterodactyl API to provision servers without human intervention.

Written by Jochem Wassenaar – CEO of Space-Node – 15+ years combined experience in game server hosting, VPS infrastructure, and 24/7 streaming solutions. Learn more

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:

  1. Open an SSH connection to the new VPS
  2. Update the system and install Docker
  3. Download and install the Wings binary
  4. Write the Wings configuration file with the correct panel address and token
  5. Register the node in Pterodactyl via the API
  6. Start and enable the Wings service
  7. 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.

Jochem Wassenaar

About the Author

Jochem Wassenaar – CEO of Space-Node – Experts in game server hosting, VPS infrastructure, and 24/7 streaming solutions with 15+ years combined experience.

Since 2023
500+ servers hosted
4.8/5 avg rating

Our team specializes in Minecraft, FiveM, Rust, and 24/7 streaming infrastructure, operating enterprise-grade AMD Ryzen 9 hardware in Netherlands datacenters. We maintain GDPR compliance and ISO 27001-aligned security standards.

Read full author bio and credentials →

Launch Your VPS Today

Get started with professional VPS hosting powered by enterprise hardware. Instant deployment and 24/7 support included.

Automated Game Server Deployment Pipelines With Python and SSH