Note
This project is in early development and not ready for production use.
The goal of Caisson is to provide an update workflow for predefined Docker services on air-gapped or offline devices. In 0.1.0, operators should only need to point the CLI at a local package file and apply the update. The backend handles validation, image import, service restart, health checks, rollback, and local audit history.
A caisson is a controlled chamber used to move work safely through difficult or isolated environments. That is the idea behind this project: a controlled path for getting updates onto an edge device and applying them safely.
Caisson is focused on:
- accept an offline update package
- work through a CLI-first operator flow in
0.1.0 - validate it
- load the Docker image
- update a predefined service
- run health checks
- roll back automatically if the update fails
- keep a local history of what happened
- support predefined services that use either direct Docker control or developer-provided Docker Compose definitions
Caisson is not:
- a fleet manager
- a registry
- a cloud update system
- a general Docker dashboard
Hauler describes itself as an “Airgap Swiss Army Knife” for fetching, storing, packaging, and distributing artifacts across disconnected environments.
Mender supports standalone deployments for devices without network connectivity, including updates triggered manually or through external storage, and it also has Docker Compose update support.
Portainer is a broader container management UI for Docker and other environments, positioned as a toolset for building and managing containers.
Caisson exists because I wanted something smaller and more opinionated than those options: a local updater focused specifically on offline Docker service updates on one device, with a simple operator experience. It is not always that the human closest to the end device has the technical knowlege to safely update its services.
The first release is focused on getting the baseline workflow working:
- offline package intake
- validation
- Docker image import
- service update
- health checks
- rollback
- local audit/history
Note
Current roadmap:
0.1.0: cli for offline updater0.2.0: minimal GUI on top of the same application logic0.3.0: self-updater support0.4.0: encryption and signatures
cargo run -- service list --services path/to/services.toml --state-dir .caisson-stateThat command shows the predefined services from services.toml together with the locally known image state and the last recorded update result.
Validate a package without changing anything:
cargo run -- package validate path/to/update.edgepkg --services path/to/services.toml --state-dir .caisson-stateThat command:
- stages the package locally
- validates the
.edgepkgtar structure - parses
manifest.toml - checks target service compatibility against
services.toml - persists validation records and audit events under the chosen state directory
Load a package:
cargo run -- package load path/to/update.edgepkg --services path/to/services.toml --state-dir .caisson-stateThat command validates the package first, asks for confirmation, imports the staged image.tar into Docker, applies the update to the target service, runs health checks, and rolls back automatically if the update does not stay healthy.
If you want the same flow without the confirmation prompt:
cargo run -- package load path/to/update.edgepkg --yes --services path/to/services.toml --state-dir .caisson-stateTo inspect local update history after a run:
cargo run -- history list --state-dir .caisson-state
cargo run -- history show <update-id> --state-dir .caisson-stateIf you want to remove leftover local package artifacts created under the updater state directory:
cargo run -- package cleanup --state-dir .caisson-stateThat command currently cleans only the local package workspace. It does not remove validation records, audit history, candidate releases, or service state.
Quick way to build a local package for manual testing is:
tmpdir=$(mktemp -d)
docker pull alpine:3.19
docker pull alpine:3.20
docker tag alpine:3.19 example/frontend:current
docker tag alpine:3.20 example/frontend:1.2.3
docker save example/frontend:1.2.3 -o "$tmpdir/image.tar"
cp tests/fixtures/manifests/valid-frontend.toml "$tmpdir/manifest.toml"
tar -cf "$tmpdir/frontend.edgepkg" -C "$tmpdir" manifest.toml image.tarIf you want to test the full package load path, start the managed service first so there is something to replace:
docker rm -f frontend 2>/dev/null || true
docker run -d --name frontend example/frontend:current sh -c 'sleep infinity'Then run:
cargo run -- package load "$tmpdir/frontend.edgepkg" \
--services tests/fixtures/services.valid.toml \
--state-dir "$tmpdir/state"hello-world is still fine for the earlier image-import smoke test, but it exits immediately, so it is not a good fit for the full apply + health check.
For a one-shot local smoke test, run:
./scripts/docker-test.shTo build just the lightweight demo package used in local docs and demos:
./scripts/make-demo-edgepkg.shTest fixtures live under tests/fixtures/.