Corey Schue Avatar
, , ,
Air-Gapped LLM Deployment: The Weight on One Side of the Wall | Implicit Deny
Infrastructure AI / MCP Series: Air-Gapped AI — Part 2 of 3

The Weight on One Side of the Wall

Air-gapped LLM deployment isn’t one pipeline — it’s two running in parallel. The MCP server and the inference layer both have to cross the wall, and they do it differently. Almost all the work happens before either one gets there.

notice

Where we left off

Last post covered the why — no frontier model, no API key, no internet, and why that’s still worth building agentic AI inside of anyway. This post is the how. Specifically, it’s about air-gapped LLM deployment in full: getting both the MCP server and the inference layer from an idea on a whiteboard to something running inside a wall that doesn’t talk to the outside world.

Most writing about this topic picks one or the other. Either it’s a tutorial on deploying an MCP server, or it’s a tutorial on running a local LLM. In an air-gapped environment, you can’t separate them — you need both, you need them to talk to each other, and you need to get them both across the boundary first. So this post covers both pipelines.

The asymmetry up front, because it’s the whole point: almost everything that requires judgment happens on the internet-facing side. Almost nothing does on the air-gapped side. That’s not a limitation I’m working around. That’s the design.

warning

Two pipelines, one connected side

Everything starts on a system with internet access. There are two distinct pipelines running in parallel — the MCP server and the inference layer — and while they follow similar patterns, they’re not the same thing and shouldn’t be treated like they are.

// pipeline 1: the MCP server

  1. Scaffold the MCP server project using kmcp.
  2. Write the actual tool logic in Python (FastMCP), dropped into the scaffold kmcp generated.
  3. Push the project into CI/CD. Code verification and vulnerability/dependency scanning run automatically on push — this is where trust gets established.
  4. Build the container image from the verified, scanned code.
  5. Push the approved image to the internet-facing registry — Harbor, in my setup.
// coming soon

Steps 1 and 2 — scaffolding the MCP server and writing tools with kmcp and FastMCP — are their own discipline and deserve more than a bullet point. I’ll be covering the full kmcp scaffold and tool creation workflow in a dedicated standalone post. This post is focused on getting what you’ve already built across the wall and running.

// pipeline 2: the inference layer

The inference layer is two separate things that often get conflated: the serving container and the model weights. They travel differently, and treating them as the same artifact is how you end up with a 70GB container image and a confused ops team.

  1. Pull the vLLM base image and verify it in CI/CD — same pipeline as the MCP server. Build, scan, push to Harbor as a standard container image.
  2. On the connected side, download model weights from HuggingFace using huggingface-cli snapshot_download. Stick to quantized formats — GGUF, AWQ, or GPTQ depending on your hardware — because full-precision weights for anything useful are enormous, and quantization is honestly what makes air-gapped LLM deployment viable at all.
  3. Package and transfer the weights. More on this in the next section, because it’s its own problem.
# Step Pipeline Location Tooling What happens
MCP Server Pipeline
1 Scaffold & build MCP connected kmcp Initialize MCP server project, scaffold structure
2 Add tools MCP connected Python (FastMCP) Write tool logic into the kmcp scaffold
3 Verify & scan MCP connected CI/CD pipeline Code verification and vulnerability scanning on push
4 Build & push image MCP connected CI/CD + Harbor Container image built from verified code, pushed to registry
Inference Layer Pipeline
5 Build vLLM image LLM connected CI/CD + Harbor vLLM serving container built, scanned, pushed — same pipeline as MCP
5a Build gateway image (optional) LLM connected CI/CD + Harbor LiteLLM, Bifrost, or similar — same pipeline, another image crossing the wall
6 Download weights LLM connected HuggingFace CLI Quantized model weights pulled via snapshot_download
7 Package weights LLM connected tar / Harbor OCI Weights packaged for transfer — raw bundle or OCI artifact
Boundary Crossing
8 Export images boundary MCP image + vLLM image exported as transferable artifacts
9 Transfer weights boundary Weight bundle or OCI artifact crosses the wall separately
Air-Gapped Side
10 Push to internal registry MCP air-gapped Harbor (mirrored) MCP image lands internally — no build, no scan, no source
11 Push vLLM image LLM air-gapped Harbor (mirrored) vLLM serving image lands internally
12 Stage weights LLM air-gapped PVC / NFS Model weights staged to persistent storage for mounting
13 Deploy & test MCP MCP air-gapped kmcp deploy + K8s MCP server runs, reconciled against MCPServer CRD
14 Deploy & test vLLM LLM air-gapped vLLM + K8s vLLM serving container runs with weights mounted; MCP connects to endpoint

Look at where the line falls. Steps one through seven — everything requiring judgment, everything that establishes trust — happen on one side of the wall. Steps eight through fourteen are mechanical. Move the artifacts. Stage the weights. Run what already passed the gate. In fact, that’s the whole air-gapped side of the job, and if it feels anticlimactic, good. It’s supposed to.

critical

The model weight problem

The MCP server and the vLLM container are just container images — they follow the same pipeline, cross the wall the same way, and land in Harbor the same way. Model weights are a completely different artifact class. They’re not code. They’re not images. They’re data, they can be enormous, and they need their own transfer strategy.

First, quantization — because if you’re not already using it, it’s the thing that makes air-gapped LLM deployment actually viable in constrained environments. Full-precision weights for a 13B parameter model can run 26GB or more. The same model in AWQ or GPTQ quantization lands somewhere between 7 and 10GB. GGUF can go lower depending on the quantization level. That difference matters a lot when you’re physically moving data across a boundary and you’re working with limited storage on the other side.

Quantization isn’t a compromise you make because you can’t afford better hardware. In air-gapped environments, it’s an operational requirement.

For the transfer itself, two approaches depending on the model and your setup:

// raw file transfer

The simpler path. Use huggingface-cli snapshot_download on the connected side to pull the full model repository — weights, tokenizer, config, the works — then tar the directory and move it as a file bundle. On the air-gapped side, extract to a persistent volume or NFS share and mount it into your vLLM container at runtime. This works for any model, requires no special tooling on the receiving end, and keeps the weights completely separate from the serving container.

// OCI artifact via Harbor

Harbor 2.x supports OCI artifacts natively, which means you can push model weights as OCI layers and pull them the same way you’d pull a container image. This approach integrates the weights into your existing registry workflow and gives you versioning and audit history essentially for free. The tradeoff is setup complexity — you need tooling like oras to push and pull non-image OCI artifacts, and it’s more to explain to whoever’s managing the internal registry on the air-gapped side. For shops that are already heavily Harbor-centric, it’s worth the investment. For everyone else, the tar bundle is usually fine.

// practical note

Whichever transfer approach you use, hash the weights on both sides of the wall and verify before you deploy. A corrupted transfer doesn’t always fail loudly — sometimes it just produces bad model output, which is much harder to diagnose than a clean checksum mismatch.


also

Why a narrow crossing is the feature

Here’s the part worth sitting with, because it’s easy to look at a 14-step table and think the air-gapped side is getting shortchanged. In fact, it got the simple end, and simple is the goal. The heavier the connected-side pipeline, the thinner and more auditable the crossing becomes. That’s not a coincidence — that’s the whole reason to build it this way.

CONNECTED AIR-GAPPED MCP scaffold scan build harbor LLM vllm img weights snapshot package tar / OCI harbor the wall images weights registry harbor weights PVC / NFS deploy MCP + vLLM

fig. 1 — two pipelines converge at the wall; two slim artifacts cross; one deployment on the other side

The air-gapped cluster doesn’t trust these artifacts because they look fine. It trusts them because they arrived with proof they already passed the gate.

Name what actually crosses the wall and the point makes itself: two container images, a weight bundle or OCI artifact, and maybe a manifest or two. Not the source. Not the build tools. Not a dependency cache. Just the finished, already-approved results of everything that happened on the connected side. Less crossing the wall isn’t a compromise you make for security. It’s the security.


error

What air-gapped LLM deployment actually looks like

Once both artifacts are inside the wall, the job is straightforward by design. The container images go into the internal Harbor instance — same tool, separate from anything internet-facing, never touched the outside world. The weights go to persistent storage, either a PVC on Kubernetes or an NFS share, depending on your setup.

From there, two deployment patterns for vLLM depending on model size and what makes operational sense:

// weights mounted at runtime

The more common pattern for anything non-trivial. The vLLM container image stays lean — it’s just the serving layer. The weights live on a PVC or NFS share and get mounted in at deploy time. This keeps the container image manageable, lets you swap or update weights without rebuilding and re-crossing the image, and works well with Kubernetes volume claims. After all, it’s the same image regardless of which model you’re serving — only the mount changes.

// weights baked into the image

Simpler to reason about and sometimes the right call for smaller quantized models where the weight size isn’t prohibitive. One image, no external mounts to manage, no volume claim to get wrong. The tradeoff is image size and the fact that updating the model means building and re-crossing a new image. For a handful of tools running one specific model, that’s often an acceptable cost. For a more dynamic environment, it gets unwieldy fast.

Once vLLM is running, kmcp deploy and the Kubernetes controller bring up the MCP server, reconciling the image against the MCPServer CRD. Depending on your setup, you may also want a gateway layer sitting between MCP and the vLLM endpoint — something like LiteLLM or Bifrost (there are others) to handle routing, load balancing across multiple models, or to normalize the API surface if you’re swapping serving backends. If you’re running one model and one serving container, a gateway is probably more overhead than it’s worth upfront. If you’re juggling multiple models or need rate limiting and observability at the API layer, it’s worth the extra container crossing the wall. Either way, the gateway follows the same pipeline as everything else — build, scan, push, export, import, deploy.

From the MCP server’s perspective, the local inference layer is just an API endpoint — OpenAI-compatible, whether vLLM is serving directly or a gateway is in front of it. The wall doesn’t change how they communicate. It just changes where the thing they’re talking to lives.

// day 2

Signing and attestation for both the container images and the weight artifacts is out of scope here on purpose — tools like cosign cover the image side, and the OCI artifact path via Harbor gives you a natural hook for weight attestation as well. Worth its own post once I’m actually running it rather than a paragraph tacked onto this one.

In fact, most of the public conversation about LLM deployment assumes you can keep iterating — pull a new model version, restart the container, done. We don’t get that. Going back to update anything means doing the whole pipeline again. So the work doesn’t get lighter because you’re doing air-gapped LLM deployment. It just moves to where it can actually be checked. That’s the design. Heavy in the open, simple behind the wall.

Next in this series: Operationalizing

What “production” actually means once both pipelines are running — secrets without an external vault, RBAC scoped to the MCPServer CRD, update cadence when you can’t just pull from a live repo, and observability inside a boundary that doesn’t talk back.

ID

Leave a Reply

Your email address will not be published. Required fields are marked *

Latest Posts