12 KiB
Stack Research
Domain: Self-hosted single-container internal tool — Windows printer deployment package generator Researched: 2026-04-10 Confidence: HIGH (core stack), MEDIUM (.intunewin reimplementation specifics)
Recommended Stack
Core Technologies
| Technology | Version | Purpose | Why Recommended |
|---|---|---|---|
| Python | 3.12 | Runtime | LTS-stable, broad library support, zipfile/cryptography stdlib covers .intunewin needs without C extensions. 3.13 is fine too; avoid 3.11 and below (FastAPI 0.130+ requires 3.10+ but 3.12 is the sweet spot for Docker image size vs feature parity). |
| FastAPI | 0.115.x (latest stable) | HTTP framework + API | De-facto standard for Python internal tools in 2025. Async-capable, Pydantic validation built-in, native file upload/download support, StreamingResponse for generated archives. Simpler than Django for this scope; more structured than Flask. |
| Uvicorn | 0.30.x | ASGI server | FastAPI's recommended server. Handles subprocess management internally — Gunicorn is not needed for a single-container internal tool with no concurrency requirements. |
| Jinja2 | 3.1.x | Server-side HTML templating | Ships with FastAPI's template support. No build step, no Node. HTMX+Jinja2 renders the full UI from the server. |
| HTMX | 2.0.x (CDN) | Dynamic UI without JavaScript SPA | Handles partial page updates (form submissions, driver list refresh, package generation progress) with zero build tooling. Ideal for CRUD-heavy internal tools. Single <script> tag. |
| Alpine.js | 3.x (CDN) | Client-side UI state | Complements HTMX: dropdowns, modals, toggle states, file-input previews. Replaces React/Vue for this scope. No npm, no bundler. |
| SQLite (stdlib) | 3.x (bundled) | Persistence: driver records, printer configs | No external database process. Python's built-in sqlite3 module is sufficient for sync operations. Single-writer model is not a concern for a single-user internal tool. Mount via Docker named volume. |
| Tailwind CSS | 4.x (CDN Play) | Utility-first CSS | For an internal tool with no public users, the CDN Play script is acceptable. It avoids a Node.js build step inside the Docker image. If CSS size ever matters, switch to the standalone Tailwind CLI binary (no Node required). |
Supporting Libraries
| Library | Version | Purpose | When to Use |
|---|---|---|---|
python-multipart |
0.0.9 | Multipart file upload parsing | Required by FastAPI for UploadFile. Always install alongside FastAPI when handling file uploads. |
pycryptodome |
3.20.x | AES-256-CBC + HMAC-SHA256 for .intunewin encryption | The .intunewin inner package is encrypted with AES-256-CBC; the HMAC-SHA256 digest is written to Detection.xml. Use pycryptodome (not the deprecated pycrypto). |
lxml or xml.etree.ElementTree (stdlib) |
stdlib | Generate Detection.xml metadata | Detection.xml is simple enough for stdlib ElementTree. Only reach for lxml if namespaces become complex. |
aiofiles |
23.x | Async file I/O | Used with FastAPI's StreamingResponse when streaming generated ZIPs to the browser without loading the full archive into memory. |
python-dotenv |
1.0.x | Environment-based configuration | Allows Docker-level config overrides (data directory, port, base URL) without rebuilding the image. |
peewee |
3.17.x | ORM for SQLite | Lightweight sync ORM — perfectly matched to SQLite's single-writer model. Avoids the async overhead of SQLModel/aiosqlite, which provides no benefit with SQLite. Use only if raw sqlite3 becomes unwieldy across multiple tables. |
Development Tools
| Tool | Purpose | Notes |
|---|---|---|
| Docker (python:3.12-slim-bookworm) | Container base image | Slim variant keeps image under 200 MB. Bookworm (Debian 12) has modern glibc. Do NOT use Alpine Linux — pycryptodome and other C-extension wheels often lack musl-compatible builds, causing silent failures. |
uvicorn --reload |
Dev server with hot reload | Run locally during development. In Docker, use CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] without --reload. |
| Docker named volume | Driver + SQLite persistence | Mount /data as a named volume. Store driver ZIPs and the SQLite file there. Never store state in the container filesystem. |
Installation
# Core runtime
pip install fastapi==0.115.* uvicorn[standard]==0.30.* jinja2==3.1.* python-multipart==0.0.9
# File generation and encryption
pip install pycryptodome==3.20.* aiofiles==23.*
# Configuration + optional ORM
pip install python-dotenv==1.0.* peewee==3.17.*
# Dev only
pip install httpx pytest pytest-asyncio
Tailwind CSS and HTMX are loaded from CDN in templates — no npm install.
Alternatives Considered
| Recommended | Alternative | When to Use Alternative |
|---|---|---|
| FastAPI + Jinja2 + HTMX | Django + django-htmx | If the project grows to need Django admin, multi-tenant auth, or complex ORM migrations. Overkill for this scope. |
| FastAPI + Jinja2 + HTMX | FastAPI + React/Vue SPA | If the UI needs real-time collaborative editing or complex client-side state (drag-and-drop ordering, offline mode). Not needed here. |
| Python (FastAPI) | Go (Gin/Chi) | If single binary with no runtime dependency is the top priority. Go produces a smaller Docker image but adds complexity for the .intunewin crypto/zip logic that Python stdlib already handles. |
| pycryptodome | cryptography (PyCA) | cryptography is fine too and arguably better maintained. Either works for AES-CBC. Prefer cryptography if you want FIPS-like assurance; pycryptodome is simpler to use for this exact pattern (CBC + HMAC). |
| SQLite (builtin / peewee) | PostgreSQL | If multi-user write concurrency is ever needed. Not the case here. |
| Tailwind CDN | Tailwind standalone CLI | Use the standalone CLI binary (no Node) if you want purged, production-sized CSS. Drop the tailwindcss binary into the Docker image at build time. Adds ~10 MB to the image but removes CDN dependency. |
| python:3.12-slim-bookworm | python:3.12-alpine | Alpine breaks C-extension wheels. Slim Debian is the correct base for any project using pycryptodome or similar. |
What NOT to Use
| Avoid | Why | Use Instead |
|---|---|---|
| IntuneWinAppUtil.exe inside Docker | It is a Windows PE binary. It will not run in a Linux container. Even Wine would add hundreds of MB and introduce instability. | Reimplement the format in Python: zipfile (stdlib) for inner ZIP with ZIP_STORED, pycryptodome for AES-256-CBC encryption, xml.etree.ElementTree for Detection.xml. The format is fully documented by reverse engineering (svrooij.io). |
| Alpine Linux base image | musl libc breaks pre-built wheels for pycryptodome and other C-extension packages, causing pip to fall back to source builds or fail silently. | python:3.12-slim-bookworm — Debian-based, glibc, pre-built wheels always work. |
| Celery / Redis / task queue | Massively overengineered for a single-user internal tool. .intunewin generation takes < 5 seconds per package. | FastAPI BackgroundTasks is sufficient for post-response cleanup; synchronous generation on the request thread is fine. |
| SQLAlchemy async (aiosqlite) | Async gives zero throughput benefit with SQLite (single-writer lock). Adds complexity for no gain. | Synchronous sqlite3 stdlib or peewee. Run database calls synchronously inside asyncio.run_in_executor if truly needed. |
| React / Vue / Next.js | Requires a Node.js build pipeline in the Docker image, a separate frontend build stage, and a JS bundle. Unnecessary complexity for an internal CRUD tool. | HTMX + Alpine.js served from Jinja2 templates. No build step. |
| PyCrypto (original) | Unmaintained since 2012. Known vulnerabilities. | pycryptodome — a maintained drop-in replacement. |
Stack Patterns by Variant
If .intunewin generation needs to stay on the request thread (simplest path):
- Generate the .intunewin file synchronously, stream it with
FileResponseorStreamingResponse, then delete the temp file withBackgroundTasks. - No async file I/O needed for packages under ~500 MB.
If driver ZIPs are very large (>500 MB) and upload times block Uvicorn:
- Use
aiofilesfor async reads during upload processing. - Uvicorn is single-threaded by default in development; add
--workers 2in production Dockerfile if needed (though unlikely for an internal tool).
If Tailwind CDN becomes a problem (slow intranet, offline use):
- Download the Tailwind standalone CLI (
tailwindcss-linux-x64) into the Docker image during build. - Run
tailwindcss -i input.css -o static/output.css --minifyas a DockerfileRUNstep. - Serve the generated CSS as a static file. Zero Node.js required.
.intunewin Reimplementation (Critical Detail)
The official IntuneWinAppUtil.exe is Windows-only. The format is well-documented through reverse engineering:
- Inner ZIP — compress source folder using
zipfile.ZipFile(..., compression=ZIP_DEFLATED)(note: unlike the outer ZIP, the inner package uses DEFLATE not STORED) - Encrypt — AES-256-CBC with a random 32-byte key and 32-byte IV; prepend HMAC-SHA256 (32 bytes) then IV (32 bytes) to the ciphertext
- Detection.xml — XML file with base64-encoded EncryptionKey, InitializationVector, Mac (HMAC), FileDigest (SHA256 of plaintext), FileDigestAlgorithm ("SHA256"), and ProfileIdentifier ("ProfileVersion1")
- Outer ZIP — ZIP_STORED containing
IntuneWinPackage/Contents/IntunePackage.intunewin(the encrypted blob) andIntuneWinPackage/Metadata/Detection.xml
Reference implementation: svrooij.io — Creating IntuneWin files with C# — the logic is language-agnostic and Python stdlib + pycryptodome covers all requirements. Confidence: MEDIUM (based on reverse-engineering documentation; validate against a real Intune upload during Phase 1).
Version Compatibility
| Package | Compatible With | Notes |
|---|---|---|
| fastapi 0.115.x | pydantic 2.x | FastAPI 0.115+ requires Pydantic v2. Do not mix with Pydantic v1. |
| pycryptodome 3.20.x | Python 3.12, 3.13 | Use pycryptodome not pycryptodomex (different package name, same code, different import path). Import as from Crypto.Cipher import AES. |
| uvicorn 0.30.x | fastapi 0.115.x | Compatible. Use uvicorn[standard] to include uvloop and httptools for better performance. |
| peewee 3.17.x | Python 3.12 | Sync only. Does not conflict with FastAPI's async model — call peewee from sync functions or run_in_executor. |
| Tailwind CDN v4 | Any browser | Play CDN is development-only per Tailwind docs. For production isolation, use standalone CLI instead. |
Sources
- FastAPI Deployment with Docker — Official Docs — Docker patterns, Uvicorn configuration
- FastAPI Templates — Official Docs — Jinja2 integration
- svrooij.io — Creating IntuneWin files with C# — .intunewin format reference (MEDIUM confidence, reverse-engineered)
- svrooij.io — Decrypting intunewin files — Encryption structure validation
- SvRooij.ContentPrep on NuGet — Cross-platform C# reference implementation, last updated 2025-10-03
- PyCryptodome docs — AES-CBC usage
- Python zipfile docs — ZIP_STORED / ZIP_DEFLATED constants
- HTMX + FastAPI patterns 2025 — HTMX suitability for internal tools
- Python 2025 Web Stack — Medium — FastAPI + HTMX ecosystem confirmation
- Tailwind Play CDN docs — CDN limitations for production
- Docker Volumes for SQLite — Named volume pattern
- WebSearch (multiple queries, 2026-04-10) — ecosystem verification
Stack research for: ImpTune — printer deployment package generator Researched: 2026-04-10