← Back to Case Studies Network Engineering

Zero Touch Provisioning Server

Docker Compose stack for automated Cisco Catalyst 9200/9300/9500 provisioning — DHCP, HTTP config delivery, FastAPI config generator with Jinja2 templates, and a Next.js dashboard. Factory-reset to fully configured in under 10 minutes, no engineer on-site.

Docker FastAPI Jinja2 Next.js dnsmasq SQLite Python

Problem

Deploying a new Cisco Catalyst switch at a remote site typically requires a network engineer — either on-site or walking someone through console cable commands over the phone. For organizations running multi-site deployments, every switch replacement or new deployment means scheduling a truck roll or a long remote session. A factory-reset switch sitting in a box is useless until someone configures it, and that someone usually has a CCIE and a $200/hr billing rate.

The provisioning process itself is straightforward: assign a hostname, configure VLANs, set up uplinks, apply QoS, enable management access. It’s the same playbook every time with per-site variables swapped in. That’s exactly the kind of work that shouldn’t require a human.

Solution

Built a Docker Compose stack that implements Cisco’s Zero Touch Provisioning protocol end-to-end. A factory-reset Catalyst 9200, 9300, or 9500 plugs into the network, gets a DHCP lease pointing it to the ZTP server, downloads a Python bootstrap script, phones home with its serial number, receives a fully rendered configuration, and applies it — all without human intervention.

The stack has four services: dnsmasq for DHCP with ZTP options, nginx for HTTP config delivery, a FastAPI config generator that renders Jinja2 templates against a per-device inventory, and a Next.js dashboard for managing inventory and monitoring provisioning status.

Architecture

Factory-Reset Switch (IOS-XE Guest Shell)

    ├── DHCP Request ──────────→ dnsmasq (Option 67: ztp.py)

    ├── HTTP GET /ztp.py ──────→ nginx (bootstrap script)

    └── POST /api/provision ───→ FastAPI Config Generator
            │                         │
        Serial Number            Jinja2 Templates
        Platform Type            Per-client inventory (SQLite)
        IOS-XE Version           Rendered config returned

                                 Next.js Dashboard

                                 Provisioning status
                                 Device inventory
                                 Template management

Each client gets a modular directory: their own Jinja2 templates, device inventory, and variable sets. Adding a new site means adding a row to the inventory with the switch serial number and site-specific variables — hostname, management IP, uplink config, VLAN assignments. The ZTP script on the switch handles the rest.

Key Decisions

On-switch Python bootstrap via Guest Shell. The ZTP Python script runs inside IOS-XE Guest Shell, which provides a Linux environment on the switch itself. The script phones home with the switch’s serial number, platform, and IOS-XE version, then applies the returned config and triggers a reload if needed. This keeps all intelligence server-side — the on-switch script is intentionally thin.

Per-client modular architecture. Rather than a monolithic template set, each client gets isolated templates, inventory, and variables. This prevents cross-client contamination and lets the same ZTP server handle multiple organizations with completely different network designs. A managed service provider can run one stack for all their clients.

Raspberry Pi field appliance prototype. For sites without existing infrastructure (greenfield deployments, construction trailers, temporary venues), prototyped a Raspberry Pi 4 running the full stack. PoE-powered via a Catalyst switch port, WiFi management interface for initial setup. Ship the Pi with the switches — plug everything in, walk away.

Docker Compose over bare metal. The entire stack runs as docker compose up. No manual service configuration, no dependency conflicts, no “works on my machine.” The same compose file runs on a rack server, a NUC in the server room, or the Raspberry Pi prototype.

Results

  • Factory-reset Catalyst switch to fully configured in under 10 minutes
  • Supports Catalyst 9200, 9300, and 9500 platforms
  • Per-client template isolation with Jinja2 rendering
  • Real-time provisioning dashboard showing device status and history
  • Serial number-based device identification — no manual input required
  • Raspberry Pi field appliance prototype for greenfield sites
  • Full Docker Compose stack: one command to deploy

How This Scales

  • Multi-vendor expansion — Extend beyond Cisco IOS-XE to support Arista ZTP and Juniper phone-home protocols using the same template/inventory architecture.
  • SaaS model — Hosted ZTP service where clients upload templates and register serial numbers through the dashboard. Switches phone home to a cloud endpoint instead of an on-prem server.
  • Firmware management — Add IOS-XE image hosting and INSTALL mode upgrades as part of the ZTP flow. Switch gets the right firmware and config in one pass.
  • Integration with NetBox — Pull device inventory and IP assignments directly from NetBox instead of maintaining a separate SQLite database.

Tech Stack

  • Config Engine: FastAPI, Jinja2, SQLite (device inventory)
  • ZTP Protocol: dnsmasq (DHCP Option 67), nginx (HTTP delivery)
  • On-Switch: Python (IOS-XE Guest Shell bootstrap script)
  • Dashboard: Next.js
  • Deployment: Docker Compose
  • Prototype: Raspberry Pi 4 (PoE-powered, WiFi management)

Need something similar?

I've built this before. Let's talk about adapting it for your needs.