Packube v0.1.1 builds self-contained Kubernetes packages online and installs them in air-gapped environments.
The package is the unit of ownership and transfer. A target selects package
versions, while a bundle is only target.yaml plus a pool of unchanged package
archives. There is no package lock, bundle manifest, or application digest
chain.
The complete design, file contracts, command flows, validation policy, and code architecture are documented in docs/system-design-v0.1.1.md.
- A package is a self-contained
.tar.gzand can be inspected, planned, and installed independently. target buildremains the main online workflow. It reuses existing package archives, builds missing packages, and creates a bundle directory.- Platforms are configured once in
packube.yamland apply to every package in the workspace. package scanscans every package by default. An optional package name limits it to one source. It adds missing image declarations without removing manual declarations.- Package and target builds never scan for images; they package only references
already declared in
spec.images. - Runtime packages have no Packube content digests. Operators may extract,
edit, and recreate them with standard
tarcommands. - Archive path safety, schema validity, dependencies, image platforms, cluster access, and deployment failures remain hard errors.
- K3s and K9s download checksums remain strict because they protect upstream executable distribution, not editable application content.
Packube requires Go 1.26.5 or newer to build this repository.
make build
./build/packube versionPackage authoring additionally uses Helm when a package contains Helm deploy units and Skopeo when it contains images.
Image packaging resolves normal fully qualified references from their registry and falls back to the local Docker daemon when the registry source is unavailable. This allows locally built Compose images to be packaged without a separate export script.
Create a workspace and package source:
packube init shop --platform linux/amd64 --platform linux/arm64
cd shop
packube package new api --version 1.4.0Edit packages/api/package.yaml and its referenced files, then discover and
build images:
packube package scan
packube package scan api --check
packube package build apiDefine a target in targets/production.yaml, inspect its exact resolution, and
build the bundle directory:
packube target inspect production
packube target build production
packube target build production --rebuild api
packube target build production --rebuild-allRun package scan after changing manifests, charts, or values. Both
package build and target build use only the image references already
declared in spec.images.
The generated layout is intentionally simple:
dist/
├── packages/
│ ├── api-1.4.0.tar.gz
│ └── database-16.2.0.tar.gz
└── bundles/
└── production/
├── target.yaml
└── packages/
├── api-1.4.0.tar.gz
└── database-16.2.0.tar.gz
Transfer the directory directly or create an outer tar with the system tool:
tar -czf production.tar.gz -C dist/bundles/production .Inspect, plan, and install offline:
packube bundle inspect production.tar.gz
packube bundle plan production.tar.gz --context airgap
packube bundle install production.tar.gz --context airgapbundle plan performs cluster and tool preflight checks without importing
images or changing Kubernetes resources. bundle install repeats those checks,
installs the complete dependency closure in dependency-first order, and stops
at the first failure without rollback.
After another K3s node joins, import the same selected application images into that node's local containerd store without applying manifests or running Helm:
sudo packube bundle install production.tar.gz --import-onlyRun this command locally on every new node. It chooses the archives matching that host's OS and architecture and does not require Kubernetes API access.
packube.yaml owns the complete platform set:
apiVersion: packube/v1alpha1
kind: Project
metadata:
name: shop
spec:
platforms:
- os: linux
architecture: amd64
- os: linux
architecture: arm64A package source owns dependencies, declared images, deploy units, and readiness rules:
apiVersion: packube/v1alpha1
kind: PackageSource
metadata:
name: api
version: 1.4.0
spec:
dependencies:
- name: database
version: ">=16.0.0 <17.0.0"
images:
- registry.example.com/shop/api:1.4.0
deploy:
- manifests:
- manifests/*.yaml
- helm:
chart:
name: api
repository: https://charts.example.com
version: 1.4.0
release: api
namespace: api
values:
- values/production.yaml
readiness:
- resource:
apiVersion: apps/v1
kind: Deployment
namespace: api
name: api
condition:
type: Available
status: "True"A target selects root package constraints. Transitive dependencies come from the selected runtime package or package source:
apiVersion: packube/v1alpha1
kind: Target
metadata:
name: production
spec:
packages:
- name: api
version: ">=1.4.0 <2.0.0"Packube selects the highest compatible version in the current source/artifact pool. It selects one version per name and rejects missing packages, conflicts, cycles, and duplicate identities. Because v0.1.1 writes no lock file, use exact constraints when resolution must not change as new packages are added.
JSON schemas and complete examples are in schemas.
Package commands require a package tar, not an extracted directory:
packube package inspect api-1.4.0.tar.gz
packube package plan api-1.4.0.tar.gz --pool ./packages
packube package install api-1.4.0.tar.gz --pool ./packagesThe default dependency policy finds each direct dependency package in the
sibling directory or --pool, selects its highest compatible version, and
checks its declared readiness against the live cluster without reinstalling it.
Dependencies therefore need meaningful readiness rules.
Use --recursive to resolve and install the complete dependency closure, or
--ignore-dependencies to install only the requested package with warnings.
These flags are mutually exclusive.
Use standard tar commands; Packube has no pack, unpack, or patch command:
mkdir api-1.4.0
tar -xzf api-1.4.0.tar.gz -C api-1.4.0
# Edit manifests, values, or package.yaml.
tar -czf api-1.4.0-custom.tar.gz -C api-1.4.0 .
packube package inspect api-1.4.0-custom.tar.gzRetarring does not require checksum regeneration. Do not edit an embedded OCI archive by hand; rebuild or replace that archive instead. If a customized package keeps the same name and version, replace the original archive in the pool because two files with the same internal identity are rejected.
There is no full-bundle rebuild requirement. Copy only the changed package and, when necessary, the changed target:
cp api-1.4.1.tar.gz production/packages/
cp targets/production.yaml production/target.yaml
packube bundle plan productionA package pool may contain multiple versions. Unused versions produce warnings.
There is no bundle uninstall. Packube does not record enough ownership state to safely remove shared namespaces, CRDs, Helm releases, YAML objects, or images. Use Helm, kubectl, or a cluster reset for explicit teardown and refresh flows.
# Online
packube k3s prepare
packube self-copy
# Offline first control-plane node
sudo packube k3s init k3s-cluster-<version>-<arch>.tar.gz \
--vip 192.168.1.100
# Offline additional control-plane node
sudo packube k3s join k3s-cluster-<version>-<arch>.tar.gz \
--vip 192.168.1.100 \
--server-ip 192.168.1.10
packube monitor k9s --readonly
packube cluster doctorK3s installation detects the default interface and host IPv4 address unless they are supplied explicitly. The prepared K3s bundle includes K9s and a shared cluster token; treat it as private material.
packube
├── init
├── package
│ ├── new
│ ├── scan
│ ├── build
│ ├── inspect
│ ├── plan
│ └── install
├── target
│ ├── inspect
│ └── build
├── bundle
│ ├── inspect
│ ├── plan
│ └── install
├── cluster
│ └── doctor
├── k3s
│ ├── prepare
│ ├── init
│ └── join
├── monitor
│ └── k9s
├── self-copy
└── version
Package and bundle installation currently supports exactly one schedulable node
when the installation contains images, because installation imports into the
local image store. Packages without images may be installed on a multi-node
cluster. Use bundle install --import-only locally after each additional node
joins; that mode only imports images and never redeploys workloads.