Sign in

    podcomp

    A Compose Spec implementation that runs on Podman, written in MoonBit

    podman
    compose
    docker-compose
    containers
    cli
    moonbit
    Download zip
    Author
    Version
    0.1.2
    License
    Apache-2.0
    Last updated
    16 hours ago
    Downloads
    3

    #podcomp

    A Compose Spec implementation that runs on Podman, written in MoonBit. It reads the same compose.yaml / compose.yml / docker-compose.yml files Docker Compose reads, merges and interpolates them, and drives podman directly.

    One native binary. No daemon, no Python runtime, no background state.

    $ podcomp up -d $ podcomp ps $ podcomp logs -f web $ podcomp down

    #Install

    # Installer script (Linux x86-64/arm64, macOS arm64) curl -LsSf https://raw.githubusercontent.com/tomasky/podcomp/main/install.sh | bash # npm npm install -g @tomasko/podcomp # From source git clone https://github.com/tomasky/podcomp && cd podcomp moon build --target native --release

    #What works

    podcomp is a port of the reference podman-compose behaviour, so the two can be used interchangeably for the common cases:

    • File discovery — compose.yaml, compose.yml, docker-compose.yml, or -f (repeatable, - reads standard input). Multiple files are merged with Compose's rules, and << merge keys are expanded.
    • Interpolation — .env files (via --env-file), the process environment, and the full ${VAR}, ${VAR:-default}, ${VAR-default}, ${VAR:?err}, ${VAR:+alt} and $$ operator set.
    • Profiles — services are selected with --profile and COMPOSE_PROFILES.
    • The service model — image, build, command, entrypoint, environment, env_file, ports, expose, volumes, tmpfs, networks, depends_on, healthcheck, labels, secrets, ulimits, deploy, resource limits, restart, user, workdir, hostname, dns, devices, sysctls, cap_add/cap_drop, and more.
    • Pods — the whole project runs in one pod (--infra=false), which is what makes service-name DNS work between containers. x-podman.in_pod and --in-pod control it.
    • Labels — the same io.podman.compose.* / com.docker.compose.* labels the reference writes, so an existing project is picked up unchanged.

    #Commands

    up create and start the stack down stop and remove the stack ps list containers logs show container logs start start services stop stop services restart restart services kill kill services rm remove stopped containers pull pull service images build build service images images list service images exec run a command in a running container run run a one-off command in a new container port print the host port for a published port pause pause services unpause unpause services config print the merged compose file version show the version

    Global options include -f/--file, -p/--project-name, --profile, --env-file, --podman-path, --podman-args, --dry-run, --verbose and --in-pod. Run podcomp --help for the full list.

    --dry-run prints the podman invocations instead of running them, which is the quickest way to see what podcomp would do.

    #Example

    services: web: image: docker.io/library/nginx:alpine ports: - "8080:80" volumes: - ./html:/usr/share/nginx/html:ro depends_on: - db db: image: docker.io/library/postgres:16 environment: POSTGRES_PASSWORD: secret volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready"] interval: 10s retries: 5 volumes: pgdata: {}

    $ podcomp --dry-run up podman pod exists pod_demo podman pod create --name=pod_demo --infra=false --share= podman network exists demo_default podman network create --label io.podman.compose.project=demo ... demo_default podman create --name=demo_db_1 --pod=pod_demo ... docker.io/library/postgres:16 podman create --name=demo_web_1 --pod=pod_demo --requires=demo_db_1 ... nginx:alpine podman start -a demo_db_1 podman start -a demo_web_1

    #Building

    Requires the MoonBit toolchain.

    moon check --target native --deny-warn moon test --target native moon build --target native --release # _build/native/release/build/cmd/main/main.exe

    Releases are cut by pushing a v* tag; .github/workflows/release.yml builds static musl binaries for Linux and a native binary for macOS, attaches them to the GitHub Release, and publishes the npm launcher.

    #License

    Apache-2.0

    PodCompError

    pub(all) suberror PodCompError {
    PodCompError(String)
    }

    The single error type the library raises. Every user-facing failure — a malformed compose file, a missing env var, a podman command that exits non-zero — arrives here as a message the CLI prints verbatim.

    CommandArgs

    pub struct CommandArgs {
    services : Array[String]
    detach : Bool
    no_deps : Bool
    no_build : Bool
    build : Bool
    force_recreate : Bool
    no_recreate : Bool
    no_start : Bool
    volumes : Bool
    remove_orphans : Bool
    rmi : String?
    timeout : Int?
    follow : Bool
    tail : String?
    since : String?
    until : String?
    timestamps : Bool
    names : Bool
    no_color : Bool
    no_log_prefix : Bool
    quiet : Bool
    format : String?
    short : Bool
    signal : String?
    all : Bool
    index : Int
    protocol : String
    private_port : Int
    no_cache : Bool
    pull : String?
    build_arg : Array[String]
    quiet_pull : Bool
    wait : Bool
    wait_timeout : Int?
    name : String?
    entrypoint : String?
    env : Array[String]
    user : String?
    workdir : String?
    rm : Bool
    publish : Array[String]
    service_ports : Bool
    volume : Array[String]
    no_tty : Bool
    privileged : Bool
    force_local : Bool
    services_flag : Bool
    no_normalize : Bool
    pull_always : Bool
    abort_on_container_exit : Bool
    abort_on_container_failure : Bool
    exit_code_from : String?
    no_attach : Array[String]
    scale : String?
    renew_anon_volumes : Bool
    no_hosts : Bool
    cnt_command : Array[String]
    }

    Compose

    pub struct Compose {
    project_name : String
    dirname : String
    files : Array[String]
    environ : Map[String, String]
    services : Map[String,
    Yaml
    ]
    deps : Map[String, Array[String]]
    container_names_by_service : Map[String, Array[String]]
    containers : Array[
    Yaml
    ]
    container_by_name : Map[String,
    Yaml
    ]
    networks : Map[String,
    Yaml
    ]
    vols : Map[String,
    Yaml
    ]
    declared_secrets : Map[String,
    Yaml
    ]
    default_net : String?
    pods : Array[String]
    merged_yaml :
    Yaml

    x_podman : Map[String,
    Yaml
    ]
    podman_version : String?
    pod_args : Array[String]
    env_file_cache : Map[String, Map[String, String]]
    }

    A loaded project, ready for a command to run against.

    GlobalOptions

    pub struct GlobalOptions {
    files : Array[String]
    project_name : String?
    profiles : Array[String]
    env_files : Array[String]
    podman_path : String
    dry_run : Bool
    verbose : Bool
    no_ansi : Bool
    no_cleanup : Bool
    in_pod : String?
    pod_args : String?
    podman_args : Array[String]
    podman_cmd_args : Map[String, Array[String]]
    }

    Mount

    type Mount

    A parsed volume/mount description.

    Podman

    pub struct Podman {
    path : String
    dry_run : Bool
    verbose : Bool
    global_args : Array[String]
    cmd_args : Map[String, Array[String]]
    }

    A configured podman invocation.

    abspath

    fn abspath(path : String) -> String

    An absolute version of path relative to the current directory.

    apply_merge_keys

    Resolve YAML merge keys (<<: *anchor) in place, bottom-up.

    The parser keeps << as an ordinary key, so it is expanded here: a single mapping, or a sequence of mappings, contributes the keys the node does not already define. The << key is removed afterwards.

    ascii_lower

    fn ascii_lower(s : String) -> String

    Lower-case ASCII only. MoonBit's core has no String::to_lowercase, and env-var names and boolean literals are ASCII anyway.

    assert_networks_and_volumes

    async fn assert_networks_and_volumes(compose : Compose, podman : Podman, containers : Array[
    Yaml
    ]) -> Unit

    Ensure every network and named volume the given containers need exists.

    The probes are deduplicated by name: a project with N containers on one network, or N services sharing one volume, pays one podman network exists / podman volume inspect instead of one per container.

    cmd_build

    async fn cmd_build(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp build

    cmd_config

    fn cmd_config(compose : Compose, args : CommandArgs) -> Unit

    podcomp config

    cmd_down

    async fn cmd_down(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp down

    cmd_exec

    async fn cmd_exec(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp exec

    cmd_images

    async fn cmd_images(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp images

    cmd_kill

    async fn cmd_kill(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp kill

    cmd_logs

    async fn cmd_logs(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp logs

    cmd_pause_unpause

    async fn cmd_pause_unpause(compose : Compose, podman : Podman, args : CommandArgs, action : String) -> Int

    podcomp pause / podcomp unpause

    cmd_port

    async fn cmd_port(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp port

    cmd_ps

    async fn cmd_ps(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp ps

    cmd_pull

    async fn cmd_pull(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp pull

    cmd_restart

    async fn cmd_restart(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp restart

    cmd_rm

    async fn cmd_rm(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp rm

    cmd_run

    async fn cmd_run(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp run

    cmd_start

    async fn cmd_start(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp start

    cmd_stop

    async fn cmd_stop(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp stop

    cmd_up

    async fn cmd_up(compose : Compose, podman : Podman, args : CommandArgs) -> Int

    podcomp up

    cmd_version

    async fn cmd_version(podman : Podman, args : CommandArgs) -> Unit

    podcomp version

    compose_env_file

    fn compose_env_file(compose : Compose, path : String) -> Map[String, String]

    Read and parse an env_file, caching by path so a file shared by many services is read and parsed once.

    compose_labels

    fn compose_labels(compose : Compose) -> Array[String]

    The project label prefix, matching what the reference implementation writes so podman ps --filter label=... finds containers either tool created.

    config_hash

    fn config_hash(compose : Compose, service :
    Yaml
    ) -> String

    The config_hash of a service, used to decide whether a container is stale.

    container_mounts

    fn container_mounts(compose : Compose, cnt :
    Yaml
    ) -> Array[Mount]

    Every mount of a container, parsed once so the args builder and the volume-creation pass share the same work.

    container_networks

    fn container_networks(compose : Compose, cnt :
    Yaml
    ) -> Map[String,
    Yaml
    ]

    The networks a container joins: the service's networks, or the project's default network.

    container_to_args

    fn container_to_args(compose : Compose, cnt :
    Yaml
    , detached : Bool, no_deps : Bool) -> Array[String] raise

    The argument list for podman create/run, following the reference implementation's ordering closely enough that the output is comparable.

    current_dir

    fn current_dir() -> String

    The process working directory, or . when it cannot be read.

    default_compose_files

    let default_compose_files : Array[String]

    The default compose file names, in the order Docker Compose probes them.

    dotenv_to_dict

    fn dotenv_to_dict(path : String) -> Map[String, String]

    Read and parse a .env file, returning an empty map when it does not exist.

    error_text

    fn error_text(error : Error) -> String

    Render any MoonBit error as a one-line message. A PodCompError already carries a human sentence, so it is unwrapped rather than printed with its constructor name attached.

    fail

    fn fail(message : String) -> PodCompError

    Raise a PodCompError with a formatted message.

    format_name

    fn format_name(compose : Compose, parts : Array[String]) -> String

    <project>_<parts...>.

    home_dir

    fn home_dir() -> String

    The user's home directory, or an empty string.

    interp_str

    fn interp_str(value : String, env : Map[String, String]) -> String raise

    Substitute every variable reference in one string.

    is_git_url

    fn is_git_url(path : String) -> Bool

    Whether a build context is a remote git URL rather than a local directory.

    join_name_parts

    fn join_name_parts(compose : Compose, parts : Array[String]) -> String

    Join name parts with the configured separator.

    load_compose

    async fn load_compose(g : GlobalOptions) -> Compose

    Load a project. This reads files (and possibly standard input), so it is async; everything else about the model is synchronous.

    main_cli

    async fn main_cli() -> Unit

    The entry point the executable calls.

    mount_arg

    fn mount_arg(compose : Compose, cnt :
    Yaml
    , mount : Mount) -> Array[String]

    Translate one parsed mount into podman arguments.

    mount_target_of

    fn mount_target_of(item :
    Yaml
    ) -> String?

    The container path of a short or long mount description.

    network_args

    fn network_args(compose : Compose, cnt :
    Yaml
    ) -> Array[String]

    --network arguments for a container.

    network_name

    fn network_name(compose : Compose, net : String) -> String

    The podman name of a top-level network, honouring name and external.

    new_command_args

    fn new_command_args() -> CommandArgs

    new_compose

    fn new_compose() -> Compose

    new_global_options

    fn new_global_options() -> GlobalOptions

    new_podman

    fn new_podman(g : GlobalOptions) -> Podman

    normalize_depends

    Bring depends_on to its mapping form so two files can merge it.

    parse_dotenv

    fn parse_dotenv(text : String) -> Map[String, String]

    Parse .env text. Later definitions override earlier ones; expansion sees the accumulated map.

    parse_short_mount

    fn parse_short_mount(spec : String, basedir : String) -> Mount

    Parse the short mount form. A bare path is an anonymous volume; a path-like source is a bind mount; anything else is a named volume.

    path_abspath

    fn path_abspath(base : String, path : String) -> String

    Resolve path against base when it is relative, then normalize.

    path_basename

    fn path_basename(path : String) -> String

    The final component of a path.

    path_dirname

    fn path_dirname(path : String) -> String

    The directory part of a path.

    path_expand_home

    fn path_expand_home(path : String) -> String

    Expand a leading ~ into the user's home directory.

    path_join

    fn path_join(a : String, b : String) -> String

    Join two path fragments with a single separator.

    path_normalize

    fn path_normalize(path : String) -> String

    Collapse . and .. segments, preserving a leading /.

    podman_argv

    fn podman_argv(podman : Podman, cmd : String, args : Array[String]) -> Array[String]

    The arguments after the podman binary: global options, the subcommand, then the command's configured options and the caller's own.

    podman_available

    async fn podman_available(podman : Podman) -> Bool

    Whether podman is on the path and runnable.

    podman_output

    async fn podman_output(podman : Podman, cmd : String, args : Array[String]) -> String

    Run podman and capture its standard output. Raises when the exit code is non-zero, with the captured standard error as the message.

    podman_run

    async fn podman_run(podman : Podman, cmd : String, args : Array[String]) -> Int

    Run podman, inheriting the terminal. Returns the exit code.

    podman_try_output

    async fn podman_try_output(podman : Podman, cmd : String, args : Array[String]) -> String?

    Run podman and capture its standard output, returning None when it fails. Used for existence probes (podman inspect, podman network exists).

    podman_version

    async fn podman_version(podman : Podman) -> String?

    The podman version string, or None when podman is unavailable.
    fn print_help() -> Unit

    The podcomp --help text.

    rec_merge

    fn rec_merge(target : Map[String,
    Yaml
    ], source : Map[String,
    Yaml
    ]) -> Unit raise

    Merge source into target in place.

    rec_subs

    fn rec_subs(v :
    Yaml
    , env : Map[String, String]) ->
    Yaml
    raise

    Interpolate every string in a value tree.

    Within a service mapping, the service's own environment entries are added to the substitution scope first, so command: "$${PORT}" can see a PORT the service sets. Variables already in env win, matching the reference implementation.

    resolve_pod_args

    fn resolve_pod_args(compose : Compose, cli_pod_args : String?) -> Array[String]

    Extra arguments passed to podman pod create.

    resolve_pod_name

    fn resolve_pod_name(compose : Compose, cli_in_pod : String?) -> String?

    The pod name, honouring --in-pod, x-podman.in_pod, and the default of one pod per project.

    resolve_volume_name

    fn resolve_volume_name(compose : Compose, cnt :
    Yaml
    , source : String, target : String) -> String

    The podman name of a named volume: name, the external name, or the project-prefixed default.

    shell_split

    fn shell_split(command : String) -> Array[String]

    Split a string the way a shell would, honouring single and double quotes.

    short_mount_target

    fn short_mount_target(spec : String) -> String?

    The container path in a SOURCE:TARGET[:OPTIONS] string.

    simple_hash

    fn simple_hash(s : String) -> String

    A stable, short hash used for the config-hash label. Not cryptographic — it only has to change when the service configuration changes.

    str_to_seconds

    fn str_to_seconds(text : String) -> Int?

    Convert "90s", "1m30s", "500ms" or a bare number to seconds.

    try_int_str

    fn try_int_str(s : String) -> Int?

    Parse an integer, or None when the text is not one.

    version

    let version : String

    Kept in sync with moon.mod, npmjs/package.json and the release tag. ci.yml fails when this drifts from moon.mod.

    version_at_least

    fn version_at_least(have : String, want : String) -> Bool

    Whether a dotted version string is at least want ("5.6.0").

    y_arr

    The array behind a Yaml::Array, if any.

    y_bool

    fn y_bool(v :
    Yaml
    ) -> Bool?

    A boolean, accepting the spellings Compose and podman accept.

    y_bool_v

    fn y_bool_v(b : Bool) ->
    Yaml

    y_clone

    Deep copy. Merge mutates its target, so every value copied in must be owned.

    y_dict

    The norm_as_dict of the reference implementation: ["k=v", "k2"] becomes {k: v, k2: null}. Bare keys keep a Yaml::Null value so the original spelling survives a round-trip.

    y_dump

    fn y_dump(v :
    Yaml
    ) -> String

    Render a value as YAML text, without the leading --- document marker.

    y_env_list

    fn y_env_list(v :
    Yaml
    ) -> Array[String]

    Turn a mapping into a list of k=v (or bare k) strings. This is the norm_as_list(norm_as_dict(x)) shape container_to_args wants.

    y_get

    Look a key up in a mapping value. None for a non-mapping or a missing key.

    y_int

    fn y_int(v :
    Yaml
    ) -> Int?

    An integer, accepting ints, floats and numeric strings.

    y_int_v

    fn y_int_v(i : Int) ->
    Yaml

    y_list

    The norm_as_list of the reference implementation: a scalar becomes a one-element list, a map becomes ["k=v", "k2"], null becomes empty.

    y_list_str

    fn y_list_str(v :
    Yaml
    ) -> Array[String]

    y_list, keeping only the scalars.

    y_map

    The map behind a Yaml::Map, if any.

    y_obj

    Build a mapping value from key/value pairs.

    y_str

    fn y_str(v :
    Yaml
    ) -> String?

    A scalar rendered as text. None for collections and nulls.

    y_str_or

    fn y_str_or(v :
    Yaml
    , fallback : String) -> String

    A scalar rendered as text, or fallback.

    y_str_v

    fn y_str_v(s : String) ->
    Yaml