Skip to content

virt.heropods #

HeroPods

HeroPods is a lightweight container management system built on crun (OCI runtime), providing Docker-like functionality with bridge networking, automatic IP allocation, and image management via Podman.

Requirements

Platform: Linux only

HeroPods requires Linux-specific tools and will not work on macOS or Windows:

  • crun (OCI runtime)
  • ip (iproute2 package)
  • iptables (for NAT)
  • nsenter (for network namespace management)
  • podman (optional, for image management)

On macOS/Windows, please use Docker or Podman directly instead of HeroPods.

Quick Start

Basic Usage

import incubaid.herolib.virt.heropods

// Initialize HeroPods
mut hp := heropods.new(
    reset:      false
    use_podman: true
)!

// Create a container (definition only, not yet created in backend)
mut container := hp.container_new(
    name:              'my_alpine'
    image:             .custom
    custom_image_name: 'alpine_3_20'
    docker_url:        'docker.io/library/alpine:3.20'
)!

// Start the container (creates and starts it)
// Use keep_alive for containers with short-lived entrypoints
container.start(keep_alive: true)!

// Execute commands
result := container.exec(cmd: 'ls -la /')!
println(result)

// Stop and delete
container.stop()!
container.delete()!

Custom Network Configuration

Configure bridge name, subnet, gateway, and DNS servers:

import incubaid.herolib.virt.heropods

// Initialize with custom network settings
mut hp := heropods.new(
    reset:       false
    use_podman:  true
    bridge_name: 'mybr0'
    subnet:      '192.168.100.0/24'
    gateway_ip:  '192.168.100.1'
    dns_servers: ['1.1.1.1', '1.0.0.1']
)!

// Containers will use the custom network configuration
mut container := hp.container_new(
    name:              'custom_net_container'
    image:             .alpine_3_20
)!

container.start(keep_alive: true)!

Using HeroScript

!!heropods.configure
    name:'my_heropods'
    reset:false
    use_podman:true

!!heropods.container_new
    name:'my_container'
    image:'custom'
    custom_image_name:'alpine_3_20'
    docker_url:'docker.io/library/alpine:3.20'

!!heropods.container_start
    name:'my_container'
    keep_alive:true

!!heropods.container_exec
    name:'my_container'
    cmd:'echo "Hello from HeroPods!"'
    stdout:true

!!heropods.container_stop
    name:'my_container'

!!heropods.container_delete
    name:'my_container'

Mycelium IPv6 Overlay Network

HeroPods supports Mycelium for end-to-end encrypted IPv6 connectivity:

!!heropods.configure
    name:'mycelium_demo'
    reset:false
    use_podman:true

!!heropods.enable_mycelium
    heropods:'mycelium_demo'
    version:'v0.5.6'
    ipv6_range:'400::/7'
    key_path:'~/hero/cfg/priv_key.bin'
    peers:'tcp://185.69.166.8:9651,quic://[2a02:1802:5e:0:ec4:7aff:fe51:e36b]:9651'

!!heropods.container_new
    name:'ipv6_container'
    image:'alpine_3_20'

!!heropods.container_start
    name:'ipv6_container'
    keep_alive:true

// Container now has both IPv4 and IPv6 (Mycelium) connectivity

See MYCELIUM.md for detailed Mycelium configuration.

Keep-Alive Feature

The keep_alive parameter keeps containers running after their entrypoint exits successfully. This is useful for:

  • Short-lived entrypoints: Containers whose entrypoint performs initialization then exits (e.g., Alpine's /bin/sh)
  • Interactive containers: Containers you want to exec into after startup
  • Service containers: Containers that need to stay alive for background tasks

How it works:1. Container starts with its original ENTRYPOINT and CMD (OCI-compliant)2. HeroPods waits for the entrypoint to complete3. If entrypoint exits with code 0 (success), a keep-alive process is injected4. If entrypoint fails (non-zero exit), container stops and error is returned

Example:

// Alpine's default CMD is /bin/sh which exits immediately
mut container := hp.container_new(
    name:              'my_alpine'
    image:             .custom
    custom_image_name: 'alpine_3_20'
    docker_url:        'docker.io/library/alpine:3.20'
)!

// Without keep_alive: container would exit immediately
// With keep_alive: container stays running for exec commands
container.start(keep_alive: true)!

// Now you can exec into the container
result := container.exec(cmd: 'echo "Hello!"')!

Note: If you see a warning about "bare shell CMD", use keep_alive: true when starting the container.

Features

  • Container Lifecycle: create, start, stop, delete, exec
  • Keep-Alive Support: Keep containers running after entrypoint exits
  • IPv4 Bridge Networking: Automatic IP allocation with NAT
  • IPv6 Mycelium Overlay: End-to-end encrypted peer-to-peer networking
  • Image Management: Pull Docker images via Podman or use built-in images
  • Resource Monitoring: CPU and memory usage tracking
  • Thread-Safe: Concurrent container operations supported
  • Configurable: Custom network settings, DNS, resource limits

Examples

See examples/virt/heropods/ for complete working examples:

HeroScript Examples

  • simple_container.heroscript - Basic container lifecycle management
  • ipv4_connection.heroscript - IPv4 networking and internet connectivity
  • container_mycelium.heroscript - Mycelium IPv6 overlay networking

V Language Examples

  • heropods.vsh - Complete API demonstration
  • runcommands.vsh - Simple command execution

Each example is fully documented and can be run independently. See examples/virt/heropods/README.md for details.

Documentation

Constants #

const version = '0.0.0'

fn delete #

fn delete(args ArgsGet) !

Delete a HeroPods instance from Redis (does not affect memory cache)

fn exists #

fn exists(args ArgsGet) !bool

Check if a HeroPods instance exists in Redis

fn get #

fn get(args ArgsGet) !&HeroPods

Get a HeroPods instance by name If fromdb is true, loads from Redis; otherwise returns from memory cache

fn heroscript_loads #

fn heroscript_loads(heroscript string) !HeroPods

///////////NORMALLY NO NEED TO TOUCH

fn list #

fn list(args ArgsList) ![]&HeroPods

List all HeroPods instances If fromdb is true, loads from Redis and resets memory cache If fromdb is false, returns from memory cache

fn list_available_docker_images #

fn list_available_docker_images() []string

List available Docker images that can be downloaded

Returns a curated list of commonly used Docker images

fn new #

fn new(args ArgsGet) !&HeroPods

fn play #

fn play(mut plbook PlayBook) !

fn set #

fn set(o HeroPods) !

Register a HeroPods instance (saves to both memory and Redis)

fn switch #

fn switch(name string)

Switch the default HeroPods instance

Thread Safety Note: String assignment is atomic on most platforms, so no explicit locking is needed. If strict thread safety is required in the future, this could be wrapped in a lock.

enum ContainerImageType #

enum ContainerImageType {
	alpine_3_20  // Alpine Linux 3.20
	ubuntu_24_04 // Ubuntu 24.04 LTS
	ubuntu_25_04 // Ubuntu 25.04
	custom       // Custom image downloaded via podman
}

ContainerImageType defines the available container base images

enum ContainerStatus #

enum ContainerStatus {
	running // Container is running
	stopped // Container is stopped or doesn't exist
	paused  // Container is paused
	unknown // Unknown status (error case)
}

ContainerStatus represents the current state of a container

struct ArgsGet #

@[params]
struct ArgsGet {
pub mut:
	name       string = 'default' // name of the heropods
	fromdb     bool // will load from filesystem
	create     bool // default will not create if not exist
	reset      bool // will reset the heropods
	use_podman bool = true // will use podman for image management
	// Network configuration
	bridge_name string   = 'heropods0'
	subnet      string   = '10.10.0.0/24'
	gateway_ip  string   = '10.10.0.1'
	dns_servers []string = ['8.8.8.8', '8.8.4.4']
	// Mycelium IPv6 overlay network configuration
	enable_mycelium     bool     // Enable Mycelium IPv6 overlay network
	mycelium_version    string   // Mycelium version to install (default: 'v0.5.6')
	mycelium_ipv6_range string   // Mycelium IPv6 address range (default: '400::/7')
	mycelium_peers      []string // Mycelium peer addresses (default: use public nodes)
	mycelium_key_path   string = '~/hero/cfg/priv_key.bin' // Path to Mycelium private key
}

///////FACTORY

struct ArgsList #

@[params]
struct ArgsList {
pub mut:
	fromdb bool // will load from filesystem
}

struct Container #

@[heap]
struct Container {
pub mut:
	name        string            // Unique container name
	node        ?&builder.Node    // Builder node for executing commands inside container
	tmux_pane   ?&tmux.Pane       // Optional tmux pane for interactive access
	crun_config ?&crun.CrunConfig // OCI runtime configuration
	factory     &HeroPods         // Reference to parent HeroPods instance
}

Container represents a running or stopped OCI container managed by crun

Thread Safety: Container operations that interact with network configuration (start, stop, delete) are thread-safe because they delegate to HeroPods.network_* methods which use the network_mutex for protection.

fn (Container) start #

fn (mut self Container) start(args ContainerStartArgs) !

Start the container

This method handles the complete container startup lifecycle:1. Creates the container in crun if it doesn't exist2. Handles leftover state cleanup if creation fails3. Starts the container process4. Sets up networking (thread-safe via network_mutex)5. If keep_alive=true, waits for entrypoint to exit and injects keep-alive process

Parameters:- args.keep_alive: If true, the container will be kept alive after its entrypoint exits successfully.The entrypoint runs first, and if it exits with code 0, a keep-alive process (tail -f /dev/null) is injected to prevent the container from stopping. If the entrypoint fails (non-zero exit), the container is allowed to stop. Default: false

Thread Safety: Network setup is thread-safe via HeroPods.network_setup_container()

fn (Container) stop #

fn (mut self Container) stop() !

Stop the container gracefully (SIGTERM) or forcefully (SIGKILL)

This method:1. Sends SIGTERM for graceful shutdown2. Waits up to sigterm_timeout_ms for graceful stop3. Sends SIGKILL if still running after timeout4. Cleans up network resources (thread-safe)

Thread Safety: Network cleanup is thread-safe via HeroPods.network_cleanup_container()

fn (Container) delete #

fn (mut self Container) delete() !

Delete the container

This method:1. Checks if container exists in crun2. Stops the container (which cleans up network)3. Deletes the container from crun4. Removes from factory's container cache

Thread Safety: Network cleanup is thread-safe via stop() -> cleanup_network()

fn (Container) exec #

fn (mut self Container) exec(cmd_ osal.Command) !string

Execute command inside the container

fn (Container) status #

fn (self Container) status() !ContainerStatus

fn (Container) pid #

fn (self Container) pid() !int

Get the PID of the container's init process

fn (Container) cpu_usage #

fn (self Container) cpu_usage() !f64

Get CPU usage in percentage

fn (Container) mem_usage #

fn (self Container) mem_usage() !f64

Get memory usage in MB

fn (Container) tmux_pane #

fn (mut self Container) tmux_pane(args TmuxPaneArgs) !&tmux.Pane

fn (Container) node #

fn (mut self Container) node() !&builder.Node

fn (Container) config #

fn (self Container) config() !&crun.CrunConfig

Get the crun configuration for this container

fn (Container) set_memory_limit #

fn (mut self Container) set_memory_limit(limit_mb u64) !&Container

Container configuration customization methods

fn (Container) set_cpu_limits #

fn (mut self Container) set_cpu_limits(period u64, quota i64, shares u64) !&Container

fn (Container) add_mount #

fn (mut self Container) add_mount(source string, destination string, mount_type crun.MountType, options []crun.MountOption) !&Container

fn (Container) add_capability #

fn (mut self Container) add_capability(cap crun.Capability) !&Container

fn (Container) remove_capability #

fn (mut self Container) remove_capability(cap crun.Capability) !&Container

fn (Container) add_env #

fn (mut self Container) add_env(key string, value string) !&Container

fn (Container) set_working_dir #

fn (mut self Container) set_working_dir(dir string) !&Container

fn (Container) save_config #

fn (self Container) save_config() !

Save the current configuration to disk

struct ContainerImage #

@[heap]
struct ContainerImage {
pub mut:
	image_name  string @[required] // Image name (located in ${self.factory.base_dir}/images//rootfs)
	docker_url  string // Optional Docker registry URL
	rootfs_path string // Path to the extracted rootfs
	size_mb     f64    // Size in MB
	created_at  string // Creation timestamp
	factory     &HeroPods @[skip; str: skip] // Reference to parent HeroPods instance
}

ContainerImage represents a container base image with its rootfs

Thread Safety: Image operations are filesystem-based and don't interact with network_config, so no special thread safety considerations are needed.

fn (ContainerImage) export #

fn (mut self ContainerImage) export(args ImageExportArgs) !

Export image to .tgz file

Creates a compressed tarball of the image rootfs

fn (ContainerImage) delete #

fn (mut self ContainerImage) delete() !

Delete image

Removes the image directory and removes from factory cache

fn (ContainerImage) info #

fn (self ContainerImage) info() map[string]string

Get image info as map

Returns image metadata as a string map for display/serialization

struct ContainerImageArgs #

@[params]
struct ContainerImageArgs {
pub mut:
	image_name string @[required] // Unique image name (located in ${self.factory.base_dir}/images//rootfs)
	docker_url string // Docker image URL like "alpine:3.20" or "ubuntu:24.04"
	reset      bool   // Reset if image already exists
}

ContainerImageArgs defines parameters for creating/managing container images

struct ContainerNewArgs #

@[params]
struct ContainerNewArgs {
pub:
	name              string @[required] // Unique container name
	image             ContainerImageType = .alpine_3_20 // Base image type
	custom_image_name string // Used when image = .custom
	docker_url        string // Docker image URL for new images
	reset             bool   // Reset if container already exists
}

ContainerNewArgs defines parameters for creating a new container

struct ContainerStartArgs #

@[params]
struct ContainerStartArgs {
pub:
	keep_alive bool // If true, keep container alive after entrypoint exits successfully
}

ContainerStartArgs defines parameters for starting a container

struct CrunConfigArgs #

@[params]
struct CrunConfigArgs {
pub:
	container_name string @[required] // Container name
	rootfs_path    string @[required] // Path to container rootfs
}

CrunConfigArgs defines parameters for creating crun configuration

struct HeroPods #

@[heap]
struct HeroPods {
pub mut:
	tmux_session   string                      // tmux session name
	containers     map[string]&Container       // name -> container mapping
	images         map[string]&ContainerImage  // name -> image mapping
	crun_configs   map[string]&crun.CrunConfig // name -> crun config mapping
	base_dir       string                      // base directory for all container data
	reset          bool                        // will reset the heropods
	use_podman     bool = true // will use podman for image management
	name           string // name of the heropods
	network_config NetworkConfig @[skip; str: skip] // network configuration (automatically initialized, not serialized)
	network_mutex  sync.Mutex    @[skip; str: skip] // protects network_config for thread-safe concurrent access
	// Mycelium IPv6 overlay network configuration (flattened fields)
	mycelium_enabled        bool     // Whether Mycelium is enabled
	mycelium_version        string   // Mycelium version to install (e.g., 'v0.5.6')
	mycelium_ipv6_range     string   // Mycelium IPv6 address range (e.g., '400::/7')
	mycelium_peers          []string // Mycelium peer addresses
	mycelium_key_path       string   // Path to Mycelium private key
	mycelium_ip6            string   // Host's Mycelium IPv6 address (cached)
	mycelium_interface_name string   // Mycelium TUN interface name (e.g., "mycelium0")
	logger                  logger.Logger @[skip; str: skip] // logger instance for debugging (not serialized)
}

HeroPods factory for managing containers

Thread Safety: The network_config field is protected by network_mutex for thread-safe concurrent access. We use a separate mutex instead of marking network_config as shared because V's compile-time reflection (used by paramsparser) cannot handle shared fields.

fn (HeroPods) container_new #

fn (mut self HeroPods) container_new(args ContainerNewArgs) !&Container

Create a new container

This method:1. Validates the container name2. Determines the image to use (built-in or custom)3. Creates crun configuration4. Configures DNS in rootfs

Note: The actual container creation in crun happens when start() is called.This method only prepares the configuration and rootfs.

Thread Safety: This method doesn't interact with network_config, so no mutex is needed. Network setup happens later in container.start().

fn (HeroPods) get #

fn (mut self HeroPods) get(args ContainerNewArgs) !&Container

fn (HeroPods) image_get #

fn (mut self HeroPods) image_get(name string) !&ContainerImage

Get image by name

fn (HeroPods) image_import #

fn (mut self HeroPods) image_import(args ImageImportArgs) !&ContainerImage

Import image from .tgz file

Extracts a compressed tarball into the images directory and creates image metadata

fn (HeroPods) image_new #

fn (mut self HeroPods) image_new(args ContainerImageArgs) !&ContainerImage

Create a new image or get existing image

This method:1. Normalizes the image name2. Returns existing image if found (unless reset=true)3. Downloads image from Docker registry if docker_url provided4. Creates image metadata and stores in cache

Thread Safety: Image operations are filesystem-based and don't interact with network_config.

fn (HeroPods) images_list #

fn (mut self HeroPods) images_list() ![]&ContainerImage

List all available images

Scans the images directory and returns all found images with metadata

fn (HeroPods) list #

fn (self HeroPods) list() ![]Container

List all containers currently managed by crun

fn (HeroPods) mycelium_inspect #

fn (mut self HeroPods) mycelium_inspect() !mycelium.MyceliumInspectResult

Inspect Mycelium status and return information

Returns the public key and IPv6 address of the Mycelium node

struct ImageExportArgs #

@[params]
struct ImageExportArgs {
pub mut:
	dest_path      string @[required] // Destination .tgz file path
	compress_level int = 6 // Compression level 1-9
}

ImageExportArgs defines parameters for exporting an image

struct ImageImportArgs #

@[params]
struct ImageImportArgs {
pub mut:
	source_path string @[required] // Source .tgz file path
	reset       bool // Overwrite if exists
}

ImageImportArgs defines parameters for importing an image

struct TmuxPaneArgs #

struct TmuxPaneArgs {
pub mut:
	window_name string
	pane_nr     int
	pane_name   string            // optional
	cmd         string            // optional, will execute this cmd
	reset       bool              // if true will reset everything and restart a cmd
	env         map[string]string // optional, will set these env vars in the pane
}