Nine hands-on, self-contained tutorials to take you from your first container to production-grade deployments.
CLI binary: ctst
Composition files: .ctst
Rust SDK crate: containust-sdk
- Hello World
- Web Server with Port Exposure
- Full Stack Application (API + PostgreSQL + Redis)
- Custom Images from Local Sources
- Reusable Templates with FROM
- Secrets Management
- Health Checks and Restart Policies
- Offline / Air-Gapped Deployment
- Using the Rust SDK
- How to write a minimal
.ctstcomposition file- How to run a single container with
ctst run- What happens behind the scenes when Containust launches a container
- Containust installed (
ctst --versionprints a version string) - An Alpine root filesystem available at
/opt/images/alpine(or anyfile://path)
Linux:
# No additional dependencies required.
# Requires Linux kernel 5.10+ with user namespaces enabled.
curl -sSL https://github.com/containust/containust/releases/latest/download/ctst-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv ctst /usr/local/bin/macOS:
# Install QEMU for the VM backend (required on macOS).
brew install qemu
# Download and install the ctst binary.
curl -sSL https://github.com/containust/containust/releases/latest/download/ctst-aarch64-apple-darwin.tar.gz | tar xz
sudo mv ctst /usr/local/bin/
# The VM backend boots automatically on the first container operation.
# To pre-boot for faster startup:
ctst vm startWindows:
# Install QEMU for the VM backend (required on Windows).
winget install QEMU.QEMU
# Download ctst from GitHub Releases and add to PATH.
# The VM backend boots automatically on the first container operation.From source (all platforms):
git clone https://github.com/RemiPelloux/Containust.git
cd Containust
cargo install --path crates/containust-cli1. Create the composition file
Create a file called hello.ctst in your working directory:
// hello.ctst — The simplest Containust composition.
COMPONENT hello {
image = "file:///opt/images/alpine"
command = ["/bin/echo", "Hello from Containust!"]
}
This defines a single component named hello that runs the echo command inside an Alpine container.
2. Preview the deployment plan
Before running, inspect what Containust will do:
ctst plan hello.ctstExpected output:
Plan: 1 component(s) to deploy
+ hello
image: file:///opt/images/alpine
command: /bin/echo "Hello from Containust!"
readonly: true (default)
network: bridge (default)
No connections declared.
Deployment order: [hello]
3. Run the container
ctst run hello.ctstExpected output:
[INFO] Parsing hello.ctst...
[INFO] Validating composition graph...
[INFO] Loading image: file:///opt/images/alpine
[INFO] SHA-256: a3f2b8c...d94e1 ✓
[INFO] Creating namespaces for 'hello' (pid, mount, net, uts, ipc)
[INFO] Setting up cgroups v2 for 'hello'
[INFO] Mounting read-only rootfs via OverlayFS
[INFO] Spawning process: /bin/echo "Hello from Containust!"
Hello from Containust!
[INFO] Process exited with code 0
[INFO] Cleaning up namespaces and cgroups for 'hello'
[INFO] Done.
4. Verify cleanup
ctst psExpected output:
No running containers.
- Parse & validate — The
.ctstfile was parsed and statically analyzed for errors. - Image load — The Alpine rootfs was loaded from disk and verified with SHA-256.
- Namespace creation — Linux namespaces (PID, mount, network, UTS, IPC) were created to isolate the container.
- Cgroup setup — A cgroups v2 hierarchy was configured for resource limits.
- OverlayFS mount — The rootfs was mounted read-only via OverlayFS.
- Process spawn — The
echocommand ran inside the isolated environment. - Cleanup — All namespaces, cgroups, and mounts were torn down. No daemon lingers.
You ran your first container with Containust — a single echo command in a fully isolated Linux namespace, with no daemon process involved. The entire lifecycle was handled by a single ctst run invocation.
Cross-platform note: On macOS and Windows, the same workflow applies. Containust automatically boots a lightweight QEMU VM on the first operation and forwards all container commands to the Linux native backend inside the VM. The user experience is identical across all platforms.
- How to mount a host directory as a volume
- How to expose container ports to the host
- How to serve static files with nginx under Containust
- Containust installed
- An nginx root filesystem at
/opt/images/nginx curlinstalled on the host
1. Create the project directory
mkdir -p webserver/html2. Create a static HTML page
Create webserver/html/index.html:
<!DOCTYPE html>
<html lang="en">
<head><title>Containust Web</title></head>
<body>
<h1>Served by Containust</h1>
<p>This page is running inside an nginx container with zero daemon overhead.</p>
</body>
</html>3. Write the composition file
Create webserver/nginx.ctst:
// nginx.ctst — Static web server with volume mount and port exposure.
COMPONENT web {
image = "file:///opt/images/nginx"
port = 80
memory = "128MiB"
volume = "/absolute/path/to/webserver/html:/usr/share/nginx/html"
readonly = true
restart = "always"
healthcheck = {
command = ["curl", "-f", "http://localhost:80/"]
interval = "15s"
timeout = "3s"
retries = 3
}
}
EXPOSE 8080:80
Replace /absolute/path/to/webserver/html with the actual absolute path to your html directory.
4. Run the web server
ctst run webserver/nginx.ctstExpected output:
[INFO] Parsing webserver/nginx.ctst...
[INFO] Validating composition graph...
[INFO] Loading image: file:///opt/images/nginx
[INFO] SHA-256: b7c4e1a...82f3d ✓
[INFO] Creating namespaces for 'web'
[INFO] Mounting volume: /absolute/path/to/webserver/html -> /usr/share/nginx/html
[INFO] Mounting read-only rootfs via OverlayFS
[INFO] Exposing port 8080 -> 80
[INFO] Starting 'web'...
[INFO] Healthcheck: starting (grace period)
[INFO] Healthcheck: healthy ✓
[INFO] Container 'web' is running.
5. Verify with curl
curl http://localhost:8080Expected output:
<!DOCTYPE html>
<html lang="en">
<head><title>Containust Web</title></head>
<body>
<h1>Served by Containust</h1>
<p>This page is running inside an nginx container with zero daemon overhead.</p>
</body>
</html>6. Check container status
ctst psExpected output:
NAME IMAGE STATUS HEALTH PORTS MEMORY
web file:///opt/images/nginx running healthy 8080->80 42/128 MiB
7. Stop the server
ctst stop webserver/nginx.ctstYou served a static website through nginx running inside a Containust container. The host directory was mounted as a read-only volume, and the container's port 80 was exposed to the host on port 8080 — all without a daemon.
- How to define a multi-component stack (API + PostgreSQL + Redis)
- How
CONNECTcontrols startup order and auto-injects environment variables- How to plan, run, inspect, and stop a full composition
- Containust installed
- Root filesystems for your API, PostgreSQL, and Redis at
/opt/images/ - A secret environment variable for the database password
1. Set up the database secret
export CONTAINUST_SECRET_DB_PASS="super_s3cure_p@ssword"2. Write the composition file
Create stack.ctst:
// stack.ctst — Full stack: API + PostgreSQL + Redis
COMPONENT api {
image = "file:///opt/images/myapp-api"
port = 8080
memory = "256MiB"
cpu = "1024"
env = {
RUST_LOG = "info"
DATABASE_URL = "postgres://${db.host}:${db.port}/app"
REDIS_URL = "redis://${cache.host}:${cache.port}/0"
}
command = ["./api-server", "--bind", "0.0.0.0:8080"]
readonly = true
restart = "on-failure"
healthcheck = {
command = ["curl", "-f", "http://localhost:8080/healthz"]
interval = "10s"
timeout = "3s"
retries = 5
start_period = "15s"
}
}
COMPONENT db {
image = "file:///opt/images/postgres-16"
port = 5432
memory = "512MiB"
volume = "/data/postgres:/var/lib/postgresql/data"
env = {
POSTGRES_DB = "app"
POSTGRES_USER = "app_user"
POSTGRES_PASSWORD = "${secret.db_pass}"
}
readonly = false
healthcheck = {
command = ["pg_isready", "-U", "app_user"]
interval = "5s"
timeout = "2s"
retries = 10
}
}
COMPONENT cache {
image = "tar:///opt/images/redis-7.tar"
port = 6379
memory = "128MiB"
readonly = true
command = ["redis-server", "--maxmemory", "100mb"]
healthcheck = {
command = ["redis-cli", "ping"]
interval = "5s"
timeout = "2s"
retries = 5
}
}
CONNECT api -> db
CONNECT api -> cache
EXPOSE 8080
3. Preview the deployment plan
ctst plan stack.ctstExpected output:
Plan: 3 component(s) to deploy
+ db
image: file:///opt/images/postgres-16
port: 5432
memory: 512 MiB
+ cache
image: tar:///opt/images/redis-7.tar
port: 6379
memory: 128 MiB
+ api (depends on: db, cache)
image: file:///opt/images/myapp-api
port: 8080
memory: 256 MiB
auto-injected env:
DB_HOST = <db.host>
DB_PORT = 5432
DB_CONNECTION_STRING = postgres://<db.host>:5432
CACHE_HOST = <cache.host>
CACHE_PORT = 6379
CACHE_CONNECTION_STRING = redis://<cache.host>:6379
Deployment order: [db, cache] -> [api]
Host port exposure: 8080 -> api:8080
4. Deploy the stack
ctst run stack.ctstExpected output:
[INFO] Starting deployment of 3 components...
[INFO] Phase 1/2: Starting independent components [db, cache]
[INFO] db: namespaces created, image loaded, starting process...
[INFO] cache: namespaces created, image loaded, starting process...
[INFO] db: healthcheck healthy ✓
[INFO] cache: healthcheck healthy ✓
[INFO] Phase 2/2: Starting dependent components [api]
[INFO] api: injecting DB_HOST, DB_PORT, DB_CONNECTION_STRING
[INFO] api: injecting CACHE_HOST, CACHE_PORT, CACHE_CONNECTION_STRING
[INFO] api: namespaces created, image loaded, starting process...
[INFO] api: healthcheck healthy ✓
[INFO] All 3 components running. Exposed: 8080 -> api:8080
5. Verify the running stack
ctst psExpected output:
NAME IMAGE STATUS HEALTH PORTS MEMORY
db file:///opt/images/postgres-16 running healthy 5432 120/512 MiB
cache tar:///opt/images/redis-7.tar running healthy 6379 18/128 MiB
api file:///opt/images/myapp-api running healthy 8080->8080 64/256 MiB
6. Execute a command inside a running container
ctst exec db -- psql -U app_user -d app -c "SELECT version();"Expected output:
version
-----------------------------------------------------------
PostgreSQL 16.2 on x86_64-pc-linux-gnu, compiled by gcc
(1 row)
7. Stop everything
ctst stop stack.ctstExpected output:
[INFO] Stopping api...
[INFO] Stopping cache...
[INFO] Stopping db...
[INFO] Cleaning up namespaces and cgroups...
[INFO] All components stopped.
You deployed a three-tier application with automatic dependency ordering. CONNECT ensured PostgreSQL and Redis were healthy before the API started, and it auto-injected connection information as environment variables. The entire stack ran without any daemon.
- How to create images from local directories using
file://- How to create images from tar archives using
tar://- How SHA-256 verification works during the build process
- Containust installed
- Basic Linux filesystem utilities (
mkdir,tar,chmod)
1. Create a minimal root filesystem
mkdir -p myimage/rootfs/{bin,lib,etc}
# Copy a statically linked binary (example: busybox)
cp /usr/bin/busybox myimage/rootfs/bin/
chmod +x myimage/rootfs/bin/busybox
# Create symlinks for common utilities
ln -s busybox myimage/rootfs/bin/sh
ln -s busybox myimage/rootfs/bin/echo
ln -s busybox myimage/rootfs/bin/ls
# Add a minimal /etc/passwd
echo "root:x:0:0:root:/root:/bin/sh" > myimage/rootfs/etc/passwd2. Write a composition using file://
Create myimage/from-dir.ctst:
// from-dir.ctst — Image sourced from a local directory.
COMPONENT app {
image = "file:///absolute/path/to/myimage/rootfs"
command = ["/bin/echo", "Built from a local directory!"]
}
Replace the path with the absolute path to your rootfs directory.
3. Build and verify the directory-based image
ctst build myimage/from-dir.ctstExpected output:
[INFO] Parsing myimage/from-dir.ctst...
[INFO] Building image for 'app' from file:///absolute/path/to/myimage/rootfs
[INFO] Computing SHA-256 of directory tree...
[INFO] SHA-256: e4a1c7f...b82d3 ✓
[INFO] Analyzing binary dependencies (distroless)...
[INFO] /bin/busybox: statically linked, no shared libraries needed
[INFO] Image layer created: 1.2 MiB
[INFO] Build complete. 1 image(s) ready.
4. Run the directory-based image
ctst run myimage/from-dir.ctstExpected output:
Built from a local directory!
5. Create a tar archive from the rootfs
cd myimage/rootfs
tar cf /opt/images/myimage.tar .
cd ../..6. Write a composition using tar://
Create myimage/from-tar.ctst:
// from-tar.ctst — Image sourced from a tar archive.
COMPONENT app {
image = "tar:///opt/images/myimage.tar"
command = ["/bin/echo", "Built from a tar archive!"]
}
7. Build and verify the tar-based image
ctst build myimage/from-tar.ctstExpected output:
[INFO] Parsing myimage/from-tar.ctst...
[INFO] Building image for 'app' from tar:///opt/images/myimage.tar
[INFO] Verifying archive SHA-256...
[INFO] SHA-256: 7d2f9a1...c45e8 ✓
[INFO] Extracting tar archive...
[INFO] Image layer created: 1.2 MiB
[INFO] Build complete. 1 image(s) ready.
8. List cached images
ctst imagesExpected output:
HASH SOURCE SIZE CREATED
e4a1c7f...b82d3 file:///absolute/path/to/rootfs 1.2 MiB 2 minutes ago
7d2f9a1...c45e8 tar:///opt/images/myimage.tar 1.2 MiB 30 seconds ago
You created container images from both a local directory (file://) and a tar archive (tar://). Containust verified content integrity with SHA-256 hashing and performed automatic distroless analysis on your binaries. Both protocols work fully offline.
- How to create reusable component templates
- How
IMPORTandFROMenable template inheritance- How child components override and merge parent properties
- Containust installed
- Root filesystems for PostgreSQL and Redis at
/opt/images/
1. Create the templates directory
mkdir -p templates2. Create a PostgreSQL template
Create templates/postgres.ctst:
// templates/postgres.ctst — Reusable PostgreSQL template.
COMPONENT postgres {
image = "file:///opt/images/postgres-16"
port = 5432
memory = "256MiB"
cpu = "512"
readonly = false
restart = "on-failure"
env = {
POSTGRES_USER = "postgres"
PGDATA = "/var/lib/postgresql/data"
}
healthcheck = {
command = ["pg_isready", "-U", "postgres"]
interval = "5s"
timeout = "2s"
retries = 10
}
}
3. Create a Redis template
Create templates/redis.ctst:
// templates/redis.ctst — Reusable Redis template.
COMPONENT redis {
image = "tar:///opt/images/redis-7.tar"
port = 6379
memory = "128MiB"
readonly = true
restart = "on-failure"
command = ["redis-server", "--maxmemory", "100mb", "--appendonly", "yes"]
healthcheck = {
command = ["redis-cli", "ping"]
interval = "5s"
timeout = "2s"
retries = 5
}
}
4. Create a composition that uses the templates
Create app.ctst:
// app.ctst — Application using reusable templates.
IMPORT "templates/postgres.ctst" AS pg
IMPORT "templates/redis.ctst" AS redis_tmpl
COMPONENT api {
image = "file:///opt/images/myapp-api"
port = 8080
memory = "256MiB"
env = {
DATABASE_URL = "postgres://${db.host}:${db.port}/myapp"
REDIS_URL = "redis://${cache.host}:${cache.port}/0"
}
command = ["./api-server"]
}
// Inherit everything from the postgres template, override specifics.
COMPONENT db FROM pg.postgres {
memory = "512MiB"
volume = "/data/app-db:/var/lib/postgresql/data"
env = {
POSTGRES_DB = "myapp"
POSTGRES_PASSWORD = "${secret.db_pass}"
}
}
// Inherit from the redis template, increase memory.
COMPONENT cache FROM redis_tmpl.redis {
memory = "256MiB"
}
CONNECT api -> db
CONNECT api -> cache
EXPOSE 8080
5. Preview template inheritance
ctst plan app.ctstExpected output:
Plan: 3 component(s) to deploy
+ db (FROM pg.postgres)
image: file:///opt/images/postgres-16 (inherited)
port: 5432 (inherited)
memory: 512 MiB (overridden: was 256 MiB)
readonly: false (inherited)
restart: on-failure (inherited)
env:
POSTGRES_USER = "postgres" (inherited)
PGDATA = "/var/lib/..." (inherited)
POSTGRES_DB = "myapp" (added)
POSTGRES_PASSWORD = <secret:db_pass> (added)
volume: /data/app-db:/var/lib/... (added)
+ cache (FROM redis_tmpl.redis)
image: tar:///opt/images/redis-7.tar (inherited)
port: 6379 (inherited)
memory: 256 MiB (overridden: was 128 MiB)
command: redis-server --maxmemory 100mb... (inherited)
+ api (depends on: db, cache)
image: file:///opt/images/myapp-api
port: 8080
memory: 256 MiB
Deployment order: [db, cache] -> [api]
Notice how env maps are merged: the child's POSTGRES_DB and POSTGRES_PASSWORD are added alongside the parent's POSTGRES_USER and PGDATA.
6. Deploy
export CONTAINUST_SECRET_DB_PASS="template_demo_pass"
ctst run app.ctstYou created reusable PostgreSQL and Redis templates that encapsulate best-practice defaults. The main composition imported them and selectively overrode properties. Template inheritance reduces duplication and enforces organizational standards across projects.
- How to inject secrets using
${secret.name}interpolation- The two secret resolution sources: environment variables and secret files
- How to verify that secrets are never stored in the state file
- How to rotate secrets without rebuilding images
- Containust installed
- A PostgreSQL root filesystem at
/opt/images/postgres-16
1. Set secrets via environment variables
The naming convention is CONTAINUST_SECRET_<NAME> (uppercased):
export CONTAINUST_SECRET_DB_PASS="initial_p@ssword_123"
export CONTAINUST_SECRET_API_KEY="sk_live_abc123xyz789"2. Write a composition that references secrets
Create secrets-demo.ctst:
// secrets-demo.ctst — Demonstrating secret injection.
COMPONENT db {
image = "file:///opt/images/postgres-16"
port = 5432
memory = "256MiB"
readonly = false
env = {
POSTGRES_DB = "secure_app"
POSTGRES_PASSWORD = "${secret.db_pass}"
}
healthcheck = {
command = ["pg_isready", "-U", "postgres"]
interval = "5s"
timeout = "2s"
retries = 10
}
}
COMPONENT api {
image = "file:///opt/images/myapp-api"
port = 8080
env = {
DATABASE_URL = "postgres://${db.host}:${db.port}/secure_app"
STRIPE_API_KEY = "${secret.api_key}"
}
command = ["./api-server"]
}
CONNECT api -> db
EXPOSE 8080
3. Deploy with secrets
ctst run secrets-demo.ctstExpected output:
[INFO] Resolving secret 'db_pass'... found in environment ✓
[INFO] Resolving secret 'api_key'... found in environment ✓
[INFO] Starting db...
[INFO] Starting api...
[INFO] All components running.
4. Verify secrets are NOT in the state file
ctst ps --state-fileExpected output shows the state file path. Inspect it:
cat $(ctst ps --state-file-path)Expected: the JSON state file contains component metadata but no secret values:
{
"components": {
"db": {
"status": "running",
"image": "file:///opt/images/postgres-16",
"pid": 12345,
"ports": [5432]
},
"api": {
"status": "running",
"image": "file:///opt/images/myapp-api",
"pid": 12346,
"ports": [8080]
}
}
}No POSTGRES_PASSWORD or STRIPE_API_KEY values appear anywhere in the file.
5. Alternative: use secret files
For environments where environment variables are not ideal, use secret files:
sudo mkdir -p /run/containust/secrets
echo "file_based_p@ssword" | sudo tee /run/containust/secrets/db_pass > /dev/null
sudo chmod 0400 /run/containust/secrets/db_passThe same ${secret.db_pass} reference in the .ctst file resolves from the file automatically. Environment variables take priority if both exist.
6. Rotate a secret
Secret rotation requires no image rebuild — just update the source and restart:
export CONTAINUST_SECRET_DB_PASS="rotated_n3w_p@ssword"
ctst stop secrets-demo.ctst
ctst run secrets-demo.ctstThe containers start with the new secret value injected at process creation time.
Secrets in Containust are resolved at deploy time from environment variables or files, injected directly into container processes, and never persisted to the state file or logs. Rotation is a simple stop-and-restart cycle with no image changes required.
- How to configure health monitoring with the
healthcheckproperty- How restart policies (
never,on-failure,always) interact with health state- How to observe automatic restarts and health transitions
- Containust installed
- Root filesystems for nginx and a test application at
/opt/images/
1. Write a composition with health checks and restart policies
Create health-demo.ctst:
// health-demo.ctst — Health checks and automatic restarts.
COMPONENT web {
image = "file:///opt/images/nginx"
port = 80
memory = "128MiB"
restart = "always"
healthcheck = {
command = ["curl", "-f", "http://localhost:80/"]
interval = "10s"
timeout = "3s"
retries = 3
start_period = "5s"
}
}
COMPONENT worker {
image = "file:///opt/images/worker"
memory = "64MiB"
restart = "on-failure"
command = ["./process-jobs", "--max-retries", "3"]
healthcheck = {
command = ["pgrep", "-f", "process-jobs"]
interval = "15s"
timeout = "2s"
retries = 2
}
}
EXPOSE 8080:80
2. Deploy and observe health state transitions
ctst run health-demo.ctstExpected output:
[INFO] Starting web...
[INFO] web: health state -> starting (grace period: 5s)
[INFO] web: health state -> healthy ✓
[INFO] Starting worker...
[INFO] worker: health state -> starting
[INFO] worker: health state -> healthy ✓
[INFO] All components running.
3. Monitor health status
ctst psExpected output:
NAME STATUS HEALTH RESTART RESTARTS UPTIME
web running healthy always 0 2m 15s
worker running healthy on-failure 0 2m 14s
4. Simulate a failure
Send a signal to crash the worker process:
ctst exec worker -- kill -9 15. Watch the automatic restart
ctst psExpected output (shortly after the failure):
NAME STATUS HEALTH RESTART RESTARTS UPTIME
web running healthy always 0 5m 30s
worker restarting unhealthy on-failure 1 3s
After a few seconds:
ctst psNAME STATUS HEALTH RESTART RESTARTS UPTIME
web running healthy always 0 5m 45s
worker running healthy on-failure 1 15s
The worker was automatically restarted because its restart policy is on-failure and the process exited with a non-zero code.
6. Understand the health lifecycle
| Phase | Duration | Behavior |
|---|---|---|
starting |
start_period (5s for web) |
Failures do not count toward retries |
healthy |
Ongoing | Last check passed |
unhealthy |
After retries consecutive failures |
Triggers restart if policy allows |
Health checks run inside the container at defined intervals. When combined with restart policies, Containust automatically recovers from failures. The on-failure policy restarts only on non-zero exits or unhealthy state, while always restarts unconditionally. Health state is visible in ctst ps output.
- How to prepare all images as local tar archives for disconnected environments
- How to deploy with
--offlineto guarantee zero network activity- The complete workflow for air-gapped / classified deployments
- Containust installed
- Access to container images (to pre-cache them before going offline)
tarutility
1. Prepare the offline image cache
On a machine with network access, collect all required images:
mkdir -p /opt/offline-images
# Assuming you have rootfs directories or can extract them from registries.
# Create tar archives for each component.
tar cf /opt/offline-images/myapp-v2.1.tar -C /opt/images/myapp-api .
tar cf /opt/offline-images/postgres-16.tar -C /opt/images/postgres-16 .
tar cf /opt/offline-images/redis-7.tar -C /opt/images/redis-7 .2. Verify archive integrity
Generate SHA-256 checksums for each archive:
sha256sum /opt/offline-images/*.tarExpected output:
a1b2c3d4e5...f6g7h8 /opt/offline-images/myapp-v2.1.tar
i9j0k1l2m3...n4o5p6 /opt/offline-images/postgres-16.tar
q7r8s9t0u1...v2w3x4 /opt/offline-images/redis-7.tar
Save these checksums for verification after transfer to the air-gapped environment.
3. Transfer to the air-gapped machine
# Copy via USB drive, secure transfer, or other approved method.
# On the air-gapped machine, verify checksums:
sha256sum -c checksums.txt4. Write the offline composition
Create airgap.ctst on the air-gapped machine:
// airgap.ctst — Fully offline deployment using local tar archives.
// Deploy with: ctst run --offline airgap.ctst
COMPONENT app {
image = "tar:///opt/offline-images/myapp-v2.1.tar"
port = 8080
memory = "256MiB"
env = {
DATABASE_URL = "postgres://${db.host}:${db.port}/secure_app"
REDIS_URL = "redis://${cache.host}:${cache.port}/0"
}
command = ["./server"]
network = "isolated"
}
COMPONENT db {
image = "tar:///opt/offline-images/postgres-16.tar"
port = 5432
memory = "512MiB"
volume = "/secure-data/pg:/var/lib/postgresql/data"
readonly = false
env = {
POSTGRES_DB = "secure_app"
POSTGRES_PASSWORD = "${secret.db_pass}"
}
network = "isolated"
}
COMPONENT cache {
image = "tar:///opt/offline-images/redis-7.tar"
port = 6379
memory = "128MiB"
readonly = true
command = ["redis-server", "--maxmemory", "100mb"]
network = "isolated"
}
CONNECT app -> db
CONNECT app -> cache
EXPOSE 8080
5. Deploy in offline mode
export CONTAINUST_SECRET_DB_PASS="airgap_s3cure_pass"
ctst run --offline airgap.ctstExpected output:
[INFO] Offline mode: all network egress blocked
[INFO] Parsing airgap.ctst...
[INFO] Validating composition graph...
[INFO] Loading image: tar:///opt/offline-images/postgres-16.tar
[INFO] SHA-256: i9j0k1l2m3...n4o5p6 ✓
[INFO] Loading image: tar:///opt/offline-images/redis-7.tar
[INFO] SHA-256: q7r8s9t0u1...v2w3x4 ✓
[INFO] Loading image: tar:///opt/offline-images/myapp-v2.1.tar
[INFO] SHA-256: a1b2c3d4e5...f6g7h8 ✓
[INFO] Starting deployment...
[INFO] db: started ✓
[INFO] cache: started ✓
[INFO] app: started ✓
[INFO] All 3 components running (offline mode).
6. Verify no network activity
The --offline flag ensures:
- No DNS lookups are attempted
- No outbound TCP/UDP connections are established
- Any
https://image sources in the.ctstfile produce a compile error (not a runtime error) - The
"isolated"network mode adds an additional layer of per-container network blocking
7. Confirm with the plan command
ctst plan --offline airgap.ctstIf any component used an https:// source, you would see:
error[E0014]: remote source forbidden in offline mode
--> airgap.ctst:3:14
|
3 | image = "https://registry.example.com/app:latest"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ remote sources are
| blocked when --offline is set
|
= help: use a local source (file:// or tar://) instead
The offline workflow is: pre-cache images as tar archives, transfer to the disconnected machine, write a .ctst using only tar:// sources, and deploy with --offline. Containust guarantees zero network activity, making it suitable for classified and air-gapped environments.
- How to embed Containust in a Rust application using
containust-sdk- How to create containers programmatically with
ContainerBuilder- How to load
.ctstfiles withGraphResolver- How to monitor container events with
EventListener
- Rust 1.85+ installed
- Linux: kernel 5.10+ (native backend)
- macOS/Windows: QEMU installed (VM backend)
- Basic familiarity with Cargo and Rust projects
1. Create a new Rust project
cargo init containust-demo
cd containust-demo2. Add the containust-sdk dependency
cargo add containust-sdk
cargo add anyhow
cargo add tracing tracing-subscriberYour Cargo.toml dependencies section will look like:
[dependencies]
containust-sdk = "0.1"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = "0.3"3. Write a simple container launcher
Replace src/main.rs with:
use anyhow::Result;
use containust_sdk::builder::ContainerBuilder;
use tracing_subscriber;
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let container = ContainerBuilder::new("hello-sdk")
.image("file:///opt/images/alpine")
.command(vec![
"/bin/echo".into(),
"Hello from the Rust SDK!".into(),
])
.memory_limit(64 * 1024 * 1024) // 64 MiB
.cpu_shares(512)
.readonly(true)
.build()?;
let exit_code = container.run()?;
println!("Container exited with code: {exit_code}");
Ok(())
}4. Build and run
cargo build --release
sudo ./target/release/containust-demoExpected output:
Hello from the Rust SDK!
Container exited with code: 0
5. Load a .ctst file with GraphResolver
Create a file demo.ctst in the project root:
COMPONENT web {
image = "file:///opt/images/nginx"
port = 80
memory = "128MiB"
}
COMPONENT api {
image = "file:///opt/images/myapp-api"
port = 8080
memory = "256MiB"
}
CONNECT api -> web
Now update src/main.rs to load and deploy a .ctst file programmatically:
use anyhow::Result;
use containust_sdk::graph::GraphResolver;
use tracing_subscriber;
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let graph = GraphResolver::from_file("demo.ctst")?;
println!("Components found: {}", graph.component_count());
println!("Deployment order:");
for (phase, components) in graph.deployment_phases() {
println!(
" Phase {}: [{}]",
phase,
components.join(", ")
);
}
graph.deploy()?;
println!("All components deployed successfully.");
graph.stop_all()?;
println!("All components stopped.");
Ok(())
}Expected output:
Components found: 2
Deployment order:
Phase 1: [web]
Phase 2: [api]
All components deployed successfully.
All components stopped.
6. Monitor events with EventListener
Add event-driven monitoring to your application:
use anyhow::Result;
use containust_sdk::builder::ContainerBuilder;
use containust_sdk::events::{EventListener, ContainerEvent};
use std::sync::Arc;
use tracing_subscriber;
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let listener = Arc::new(EventListener::new());
let listener_clone = Arc::clone(&listener);
std::thread::spawn(move || {
for event in listener_clone.subscribe() {
match event {
ContainerEvent::Started { name, pid } => {
println!("[EVENT] {name} started (PID: {pid})");
}
ContainerEvent::HealthChanged { name, status } => {
println!("[EVENT] {name} health -> {status}");
}
ContainerEvent::Stopped { name, exit_code } => {
println!("[EVENT] {name} stopped (exit: {exit_code})");
}
ContainerEvent::Restarted { name, attempt } => {
println!("[EVENT] {name} restarting (attempt #{attempt})");
}
}
}
});
let container = ContainerBuilder::new("monitored-app")
.image("file:///opt/images/alpine")
.command(vec!["/bin/sh".into(), "-c".into(), "sleep 5 && echo done".into()])
.event_listener(Arc::clone(&listener))
.build()?;
container.run()?;
Ok(())
}Expected output:
[EVENT] monitored-app started (PID: 54321)
[EVENT] monitored-app health -> healthy
done
[EVENT] monitored-app stopped (exit: 0)
7. Integrate with an existing application
The SDK is designed to be embedded in larger Rust applications. Common integration patterns:
// Pattern 1: On-demand container creation in a web server handler
async fn handle_build_request(payload: BuildRequest) -> Result<Response> {
let container = ContainerBuilder::new(&payload.job_id)
.image(&payload.image)
.command(payload.command.clone())
.memory_limit(payload.memory_mib * 1024 * 1024)
.env("BUILD_ID", &payload.job_id)
.build()?;
let exit_code = container.run()?;
Ok(Response::new(exit_code))
}
// Pattern 2: Loading infrastructure from .ctst files at startup
fn init_infrastructure(config_path: &str) -> Result<GraphResolver> {
let graph = GraphResolver::from_file(config_path)?;
graph.deploy()?;
Ok(graph)
}The containust-sdk crate lets you embed container management directly in Rust applications. ContainerBuilder provides a fluent API for single containers, GraphResolver loads and deploys full .ctst compositions, and EventListener enables reactive monitoring. Because there is no daemon, the SDK makes direct Linux syscalls — your application has full control over the container lifecycle.
Now that you have completed all nine tutorials, explore further:
- Language Reference — Complete
.ctstsyntax and semantics - CLI Reference — All
ctstsubcommands and flags - SDK Guide — Full Rust SDK API documentation
- Architecture — Internal design and crate structure
Built with Rust. Designed for sovereignty.