Back to Insights
PythonFastAPISeleniumReactDockerAutomationCase Study

Automating CS2 Game Data with Selenium, Multi-VM Architecture & Cookie Management

2026-07-068 min read

The Client & The Problem

A client named Karol came to me with a tricky automation project. He needed to:

  1. Scrape XP and level data from the Steam web application
  2. Auto-launch CS2 and navigate to the main menu
  3. Open CS2 into Deathmatch mode automatically
  4. Scrape data from the Steam web app alongside the game client

The challenge wasn't just the automation — it was managing Steam session cookies, coordinating across 5 different machines, and keeping everything secure.

Why Selenium Over Playwright

I chose Selenium over Playwright for this project. The main reason: security. Steam's authentication is sensitive — login credentials, session cookies, and game access all needed to be handled carefully. Selenium's more mature ecosystem gave me better control over the browser profile, cookie storage, and session persistence.

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
import pickle
import os

COOKIE_FILE = "steam_cookies.pkl"

def create_driver():
    opts = Options()
    opts.add_argument("--disable-blink-features=AutomationControlled")
    opts.add_argument("--no-sandbox")
    opts.add_argument("--disable-dev-shm-usage")
    opts.add_experimental_option("excludeSwitches", ["enable-automation"])
    opts.add_experimental_option("useAutomationExtension", False)
    return webdriver.Chrome(options=opts)

The biggest headache? Steam cookies take a long time to expire — but when they do, you can't do anything until you re-authenticate.

Here's how I handled it:

def load_or_login(driver, username, password):
    if os.path.exists(COOKIE_FILE):
        driver.get("https://steamcommunity.com")
        with open(COOKIE_FILE, "rb") as f:
            cookies = pickle.load(f)
        for cookie in cookies:
            driver.add_cookie(cookie)
        driver.refresh()
        # Check if login actually stuck
        if "login" in driver.current_url:
            return full_login(driver, username, password)
        return True  # Cookies were valid
    else:
        return full_login(driver, username, password)

def full_login(driver, username, password):
    driver.get("https://steamcommunity.com/login/home")
    # Fill credentials
    driver.find_element(By.ID, "steamAccountName").send_keys(username)
    driver.find_element(By.ID, "steamPassword").send_keys(password)
    driver.find_element(By.ID, "loginBtn").click()
    # Wait for redirect
    WebDriverWait(driver, 30).until(
        EC.url_changes("https://steamcommunity.com/login/home")
    )
    # Save cookies for next time
    with open(COOKIE_FILE, "wb") as f:
        pickle.dump(driver.get_cookies(), f)
    return True

The system checks cookies at the start of every task:

  • If valid → skip login, open Steam directly
  • If expired → full login, store fresh cookies, overwrite the old file

Expired cookies are overwritten immediately — no stale files hanging around.

Multi-Machine Architecture

This was the most complex part. The setup spanned 5 machines:

MachineOSRole
PCWindowsCS2 game + Steam client
VM 1LinuxBackend API
VM 2LinuxFrontend dashboard
VM 3LinuxDatabase
VM 4LinuxWorker tasks

The Windows PC was Karol's main machine where Steam and CS2 were installed. The backend on VM1 was built with Python + FastAPI — it handled all the logic: sending commands to launch the game, checking cookie expiry, and serving data to the dashboard. The frontend on VM2 was built with React + Vite for fast development and hot reloads, styled with Tailwind CSS for a clean, responsive UI. VM3 ran the database, and VM4 handled auxiliary scraping tasks.

Connecting Everything

SSH keys handled authentication between machines. Docker containerized each service so deployment was consistent:

# docker-compose.yml (simplified)
services:
  backend:
    build: ./backend # FastAPI + Python
    ports:
      - "8000:8000"
    environment:
      - DB_HOST=db
      - SSH_KEY_PATH=/keys/id_rsa
    volumes:
      - ./keys:/keys
      - ./cookies:/cookies

  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    depends_on:
      - backend

  db:
    image: postgres:15
    volumes:
      - pgdata:/var/lib/postgresql/data

Remote Game Control

The backend connected to the Windows PC via SSH to execute game commands:

import paramiko

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect("windows-pc-ip", username="karol", key_filename="/keys/id_rsa")

# Launch CS2
stdin, stdout, stderr = ssh.exec_command(
    'start steam://rungameid/730'
)

# Wait for game window, then send keystrokes for Deathmatch
# This was done via a helper script on the Windows machine

The Client Dashboard

Karol could manage everything from a web dashboard built with React + Vite and Tailwind CSS (chosen specifically for fast iteration — Vite's instant hot reload made UI tweaks painless):

  • View task status (running, completed, failed)
  • Monitor cookie expiry dates
  • See scraped XP/level data in real-time
  • Manually trigger tasks or schedule them
  • View logs from all 5 machines in one place

What I Learned

  1. Cookie management is the silent killer — Steam sessions last long but fail unpredictably. Always check before assuming.
  2. Selenium is still the right choice for auth-heavy automation — Playwright is great, but Selenium's cookie and profile handling is more battle-tested.
  3. Multi-VM debugging is painful without centralized logging — Set up log aggregation early, not after the first crash.
  4. SSH + Docker is a powerful combo — lightweight, secure, and works across Windows and Linux seamlessly.
  5. Windows game automation from Linux is possible — but requires careful SSH command construction and sometimes a helper agent on the Windows side.

This was one of those projects where the simple-sounding requirement ("automate CS2") turned into a distributed systems challenge. But getting it working — seeing the dashboard show live XP data scraped automatically from a game running on another machine — was worth every debugging session.