Docker/Podman Composer — Manual

Full feature reference: both directions, every run flag and Compose key, networks, volumes, $ quoting, the start script, and the podman specifics.

Back to the overview: Docker/Podman Composer · Open the tool live: www.jpkc.com/tools/docker/

This manual describes the Docker/Podman Composer in full: what happens in each of the two conversion directions, which run flags and Compose keys are supported, how networks, volumes, and dollar signs are handled, how the generated start script is laid out, and where the limits are. The tool's interface is in English, so tab, field, and button names are given exactly as they appear on screen.

Structure: three tabs, one client-side engine

The tool has three tabs: Run → Compose, Compose → Run, and Reference. The first two are the converters; the third is a built-in lookup sheet. Both converters run entirely in the browser — the shell tokenizer, flag parser, YAML emitter, and the js-yaml parser are all JavaScript. There is no server fetch and no API; your input never leaves the browser. All four editor fields are built on the ACE editor with syntax highlighting for shell (sh) or YAML.

Conversion is live: as soon as you type or paste something, the tool recalculates after a short pause (300 ms). There's no convert button. Each input field has an Example button that loads a ready-made sample and a Clear button that empties the field.

Compose project name

Above the tabs there's an optional Compose project name field (placeholder myapp). It applies to both directions, because Compose names networks, volumes, and containers after the project — and falls back to the folder name if you don't set one:

  • Run → Compose: the name goes into the file as name: at the very top (lowercased, invalid characters become _).
  • Compose → Run: the tool needs the name to reproduce the identifiers Compose itself would assign — myapp_default for the default network, myapp_data for a volume data, myapp-web-1 for the container of service web. Priority: the field wins over name: in the file; if neither is set, the tool uses myapp and tells you so.

The tool stores the project name and the chosen runtime (docker/podman) in your browser's localStorage, so they're still set on your next visit. Your commands and Compose files are not stored.

Notes below the output

Below both output fields a list appears when there's something to report: red for errors (such as "No image found in: …" or a YAML syntax error with line and column), yellow for warnings — almost everything the tool couldn't translate, or could only translate with a caveat. The list is announced to screen readers. Read it after every conversion: it tells you what's missing from the result and why. A few details are dropped without a note, though; the sections on the two directions below name them.

Direction 1: Run → Compose

In the Run → Compose tab you paste one or more commands into the Docker / Podman Run Command field. The result shows up right below in the Docker Compose YAML field.

How the command is read

Before any flag is evaluated, a tokenizer splits the text following POSIX shell rules. It handles:

  • Line continuations with a backslash (\ at end of line, Windows line endings included) — long multi-line commands are joined into one command.
  • Single quotes ('…') as literal text, double quotes ("…") in which only \$, \`, \", \\, and the newline are escaped, and ANSI-C quoting $'…' with \n, \t, \r, and friends.
  • Quotes in the middle of a word: -e MSG="a b" yields the single argument MSG=a b.
  • Comments (# at the start of a word, up to the end of the line) and command separators: a newline, ;, &&, and || each end a command.
  • Pipes and redirections (|, &, <, >): the rest of the line is ignored, with a note.
  • Command substitution ($(…), backticks) is kept as text, with a note — it only runs in a real shell.

Both spellings --flag value and --flag=value are supported. Combined short flags are split up even when the last one takes a value: -it, -dp 80:80, and -p8080:80 all work. -- ends the flags; the next word is the image. Boolean flags with =false (such as --init=false) are not set. The first argument that isn't a flag is the image; everything after it becomes the command.

Unknown flags are dropped, but not silently: the tool reports "Unknown flag … — dropped" and warns you if the following word may have been read as the image as a result.

Which commands are recognized

The input field may contain several commands — each run becomes its own service in one Compose file. Recognized are:

  • The programs docker, podman, and nerdctl (with or without a path); a leading sudo and variable assignments like FOO=1 docker run … are skipped. Global options before the subcommand (--context, -H …) are ignored with a note.
  • run, create, container run, and container create — one service each.
  • network create and volume create — they become top-level definitions (see Networks and volumes).
  • All other subcommands (docker ps, docker compose …) and non-Docker commands are skipped with a note.

The service name comes from --name if set (lowercased, invalid characters become _); without --name it's derived from the image (last path segment without tag or digest, e.g. nginx:1.27-alpine → nginx). If a name occurs twice, the tool appends _2, _3, and so on.

Supported value flags (flag → Compose key)

These flags take a value and map to a Compose key. Flags given multiple times (ports, volumes, env …) end up as a YAML list or map.

  • Identity: --name → container_name; --hostname/-h → hostname; --domainname → domainname; --platform → platform; --mac-address → mac_address
  • Image & pull: --pull → pull_policy; --entrypoint → entrypoint (as a one-element list); --runtime → runtime; --isolation → isolation
  • Ports & expose: -p/--publish → ports; --expose → expose
  • Volumes: -v/--volume → volumes (short syntax); --mount → volumes in long syntax (type, source, target, read_only, bind.propagation, volume.nocopy, volume.subpath, tmpfs.size/mode …; with type=bind the tool also adds bind.create_host_path: false, because --mount doesn't create missing host directories); --tmpfs → tmpfs; --volumes-from → volumes_from
  • Environment: -e/--env → environment; --env-file → env_file
  • Network: --network/--net → networks or network_mode; --network-alias/--net-alias → aliases; --ip/--ip6 → ipv4_address/ipv6_address; --link-local-ip → link_local_ips; --dns → dns; --dns-search → dns_search; --dns-opt/--dns-option → dns_opt; --add-host → extra_hosts; --link → links
  • Lifecycle & signals: --restart → restart; --stop-signal → stop_signal; --stop-timeout → stop_grace_period (30 becomes 30s)
  • Runtime: -u/--user → user; -w/--workdir → working_dir; --ipc → ipc; --pid → pid; --uts → uts; --userns → userns_mode; --cgroupns → cgroup; --cgroup-parent → cgroup_parent
  • Labels & annotations: -l/--label → labels; --label-file → label_file; --annotation → annotations
  • Memory: -m/--memory → mem_limit; --memory-swap → memswap_limit; --memory-reservation → mem_reservation; --memory-swappiness → mem_swappiness; --shm-size → shm_size
  • CPU: --cpus → cpus; -c/--cpu-shares → cpu_shares; --cpuset-cpus → cpuset; --cpu-period, --cpu-quota, --cpu-rt-period, --cpu-rt-runtime, --cpu-count, --cpu-percent → the keys of the same name with underscores
  • Other limits: --pids-limit → pids_limit; --oom-score-adj → oom_score_adj; --ulimit → ulimits (nofile=1024:2048 becomes soft/hard); --sysctl → sysctls; --storage-opt → storage_opt
  • Block I/O: --blkio-weight, --blkio-weight-device, --device-read-bps, --device-read-iops, --device-write-bps, --device-write-iops → blkio_config
  • Devices & security: --device → devices; --device-cgroup-rule → device_cgroup_rules; --gpus → deploy.resources.reservations.devices (driver nvidia, capability gpu, count or device_ids; in the form --gpus "device=…,capabilities=…" the tool only reads device, count, capabilities, and driver, other keys are dropped without a note); --cap-add → cap_add; --cap-drop → cap_drop; --security-opt → security_opt; --group-add → group_add
  • Logging: --log-driver → logging.driver; --log-opt → logging.options (as key: value)
  • Health check: --health-cmd → healthcheck.test (as [CMD-SHELL, …]); --health-interval, --health-timeout, --health-retries, --health-start-period, --health-start-interval → the matching healthcheck.* fields

Supported boolean flags (no value)

  • -t/--tty → tty: true
  • -i/--interactive → stdin_open: true
  • --privileged → privileged: true
  • --read-only → read_only: true
  • --init → init: true
  • --oom-kill-disable → oom_kill_disable: true
  • --no-healthcheck → healthcheck.disable: true
  • -d/--detach → dropped (implicit in Compose)

Flags without a Compose equivalent

Some run options simply have no place in Compose. The tool drops them and tells you which ones in the notes list:

  • --rm — Compose keeps stopped containers; for one-off runs the note points you to docker compose run --rm.
  • -P/--publish-all — Compose doesn't know it; list the ports under ports instead.
  • No Compose key ("has no Compose equivalent — dropped"): --cidfile, --cpuset-mems, --io-maxbandwidth, --io-maxiops, --kernel-memory, --use-api-socket, --volume-driver, plus the Podman options --pod, --systemd, --secret, and --tz.
  • Skipped silently, because they only affect how the container is started from a terminal: -d, -a/--attach, --detach-keys, --sig-proxy, -q/--quiet, --disable-content-trust, --help, and the Podman options --replace and --http-proxy.

Networks and volumes

A docker run uses networks and named volumes that exist outside of Compose. To keep the generated file valid — and to stop Compose from creating new, project-prefixed objects instead — the tool declares them at the top level:

  • --network NAME → networks: [NAME] on the service and NAME: {external: true} under the top-level networks:.
  • -v NAME:/path and --mount type=volume,src=NAME,… → NAME: {external: true} under the top-level volumes:. Bind mounts (paths starting with /, ./, ~, $, or a Windows drive letter) are left alone.
  • If the input also contains docker network create NAME or docker volume create NAME, the tool writes a real definition with name: NAME (the exact name Docker created it under) instead of external: true. For networks it carries over --driver, --opt, --label, --subnet, --gateway, --ip-range, --ipam-driver, --internal, --attachable, and --ipv6; for volumes --driver, --opt, --label, and --name. --scope, --aux-address, and --ipv4 are read along with their value but neither carried over nor reported; other unknown options get the note "network create: … is not mapped." A created network or volume that no run uses also ends up as a definition in the file.
  • --network host, none, bridge, default, and container:NAME become network_mode.
  • Network options (--network-alias, --ip, --ip6, --link-local-ip) belong to the first named network; the tool then writes networks: in map form with aliases, ipv4_address, and so on. The long form --network name=backend,alias=jobs,ip=… is read as well; unknown keys in it are skipped without a note. Without a named network, the tool drops these options with a note.

One difference remains by design: docker run without --network lands in the default bridge network, whereas Compose creates its own network <project>_default.

The YAML output format

If a project name is set, the output starts with name:, followed by the services: block and then the top-level networks: and volumes: blocks. Within a service, the common keys follow a fixed order: image, container_name, restart, entrypoint, command, ports, expose, volumes, tmpfs, environment, env_file, networks, network_mode, hostname, user, working_dir, healthcheck, logging; all other keys follow in the order of the flags. command is written as a list, one word per entry. No version: line is generated (modern Compose spec, where the field is obsolete).

Values are put in double quotes only when a YAML reader would otherwise interpret them differently. The tool checks this with a round trip through js-yaml, plus the YAML 1.1 pitfalls: port mappings like "8080:80" (older readers parse 22:22 as a base-60 number), pure numbers meant as strings like "3600", and the old booleans yes, no, on, off, y, n.

The result field carries an "editable" badge and really is editable: right inside the tool you can add what a run command can't tell the converter — depends_on or a build: block, for example. Copy copies the YAML; compose.yaml downloads it as the file compose.yaml (a locally generated blob). If you change the input afterwards, the live conversion overwrites your edits in the output field.

Direction 2: Compose → Run

In the Compose → Run tab you paste a Compose file into the Docker Compose YAML field. Below it, the Generated Run Commands field shows a ready-made shell script. This output field is read-only; Copy copies its contents, run.sh downloads it as the file run.sh — with #!/bin/sh and set -e prepended, so the script stops at the first error.

Choosing docker or podman

In the footer of the input field there's a Runtime radio switch with docker (default) and podman. It sets the command word for every generated line — run, build, network create, network connect, and volume create. The flags themselves are identical, because Podman reproduces the docker run options in a largely compatible way. GPUs are the exception: for Docker the tool writes --gpus all or --gpus '"device=0,1"', for Podman --device nvidia.com/gpu=all (CDI notation).

Structure of the generated script

The script is ordered so that it runs from top to bottom:

  1. # Networks and volumes — networks come first, because docker run doesn't create them. Each line is idempotent: docker network inspect NAME >/dev/null 2>&1 || docker network create … NAME. The tool carries over driver, driver_opts, internal, attachable, enable_ipv6, ipam (driver, subnet, gateway, ip_range), and labels. External networks only appear as a comment ("must already exist"). Volumes with driver, driver_opts, or labels are created with docker volume create; plain volumes are created by docker run itself.
  2. One block per service, starting with the comment line # <service>, preceded by a docker build if the service has build:, then docker run -d with one flag per line (backslash continuations) and the image plus command on the last line.
  3. docker network connect for every additional network of a service, right after its run.

The services appear in depends_on order: dependencies first. If there's a cycle, the tool warns you.

What is read and generated

The YAML parser (js-yaml) accepts a complete Compose file with services: as well as a bare services map, as long as every entry has image or build. Service, network, and volume names must follow the Compose rules (letters, digits, _, ., -, starting with a letter or digit) — otherwise the tool reports an error, and a service with an invalid name is left out.

Every command starts with <runtime> run -d and --name: taken from container_name, otherwise — as in Compose — <project>-<service>-1. After that, the Compose keys are translated back into run flags:

  • Image & build: image; build (string or object) → docker build -t <image> [-f dockerfile] [--target …] [--build-arg …] <context> before the run — without image, the image is called <project>-<service>. If both image and build are missing, the tool reports an error.
  • Restart: restart, or failing that deploy.restart_policy (none → no, on-failure with max_attempts → on-failure:N, anything else always).
  • Simple values: hostname, domainname, platform, user (→ --user), working_dir (→ --workdir), mac_address, ipc, pid, uts, userns_mode (→ --userns), cgroup (→ --cgroupns), cgroup_parent, isolation, runtime, shm_size, stop_signal, pull_policy (→ --pull, if_not_present → missing, build is dropped)
  • Resources: mem_limit (→ --memory), mem_reservation, memswap_limit (→ --memory-swap), mem_swappiness, cpus, cpu_shares, cpu_period, cpu_quota, cpu_rt_period, cpu_rt_runtime, cpuset (→ --cpuset-cpus), cpu_count, cpu_percent, oom_score_adj, pids_limit, oom_kill_disable, blkio_config, sysctls, storage_opt, ulimits; plus deploy.resources.limits (memory, cpus, pids) and deploy.resources.reservations.memory when the service-level keys are missing; GPU reservations and gpus → --gpus or Podman CDI
  • Network: network_mode (service:NAME → container:<container name>); otherwise networks (list or map) — the first network via --network with --network-alias <service> plus your own aliases, --ip, --ip6, --link-local-ip, every further one via network connect --alias …. Without networks, the container joins the network <project>_default, just as with Compose. Also links/external_links (→ --link with container names), extra_hosts (list, host=ip included, or map → --add-host), dns, dns_search, dns_opt (→ --dns-option)
  • Ports: ports as a short string or long object with host_ip (IPv6 in square brackets), published, target, protocol; expose
  • Volumes: short syntax (named volumes get the project prefix, <project>_<name>); long syntax → --mount type=…,source=…,target=… with readonly, bind-propagation, volume-nocopy, volume-subpath, consistency — a bind mount with an absolute or relative path that Compose would be allowed to create becomes -v instead, because only -v creates missing host directories (with bind.propagation set, it stays --mount); type: tmpfs → --tmpfs target:size=…,mode=…; tmpfs; volumes_from (service → container name). Host paths starting with ~/ are written as "$HOME"/…, because the shell only expands ~ unquoted at the start of a word. That works for secrets/configs, and in the long syntax only when the mount becomes -v: if a bind mount stays --mount (with bind.create_host_path: false or bind.propagation), the ~ ends up unexpanded in the script. In the short syntax the tool currently doubles the path (~/data:/data → -v "$HOME"/data/data:/data). In both cases, write $HOME/… or an absolute path instead.
  • Secrets & configs: with file: → a read-only bind mount (-v file:target:ro), secrets to /run/secrets/<target>, configs to /<target> (an absolute target is used as is); without target, the target is the name of the secret or config
  • Environment: env_file (string, list, or object with path/required), environment (list or map → -e KEY=VALUE)
  • Labels & security: labels, label_file, annotations, cap_add, cap_drop, security_opt, group_add, device_cgroup_rules, devices (string or object), privileged, read_only, init, stdin_open (→ -i), tty (→ -t)
  • Logging: logging.driver → --log-driver, logging.options → one --log-opt key=value each
  • Health check: test as [CMD-SHELL, …] or a string → --health-cmd; [CMD, …] → the words shell-quoted one by one, with a note that docker run only knows the shell form and the image needs /bin/sh; disable: true or NONE → --no-healthcheck; plus interval, timeout, start_period, start_interval, retries
  • Stopping: stop_grace_period → --stop-timeout in whole seconds, rounded up (1m30s → 90)
  • Entrypoint & command: --entrypoint takes a single program only. So entrypoint: [sh, -c] becomes --entrypoint sh, and the -c moves in front of the command. Strings are split into words following shell rules, as Compose does.

For the app service from the Example button (project shop, build: ./app, deploy.resources.limits, network back with alias api, stop_grace_period: 1m30s) the tool produces this block:

Terminal
# app
docker build -t shop-app ./app
docker run -d \
  --name shop-app-1 \
  --workdir /app \
  --memory 512M \
  --cpus 0.5 \
  --network shop_back \
  --network-alias app \
  --network-alias api \
  -e NODE_ENV=production \
  -e 'PRICE_TAG=costs $5' \
  --stop-timeout 90 \
  shop-app node server.js

What is NOT translated on Compose import

Anything without a docker run equivalent is missing from the script. The tool reports most of it in the notes list:

  • depends_on conditions such as service_healthy — the script only keeps the order; it doesn't wait for a healthy container.
  • profiles — the command always runs.
  • deploy.replicas and scale greater than 1 — docker run starts exactly one container.
  • deploy.resources.reservations.cpus and device reservations without the gpu capability.
  • Secrets/configs from environment:, with content:, or external (Swarm) — they can't be mounted as a file.
  • env_file with required: false is still passed as --env-file; docker run aborts if the file is missing.
  • Relative paths (./html) — they resolve against the current directory; run the script next to the Compose file.
  • All other service keys show up as "<key>" is not converted. — x-… extension fields are skipped silently.

Sub-keys the tool doesn't read under a known key, on the other hand, are dropped without a note: the check for unknown keys only covers the top level of a service. From build the tool reads only context, dockerfile, target, and args — so build.cache_from or build.ssh vanish silently. From deploy it only evaluates restart_policy, replicas, and resources; deploy.placement, for example, goes missing without a report. For a service's networks it carries over only aliases, ipv4_address, ipv6_address, and link_local_ips (the latter only for the first network; for further networks it is dropped); a network's priority, mac_address, or driver_opts are lost. With files like that, compare input and output yourself.

Dollar signs and variables

The shell and Compose treat $ differently: in the shell $VAR expands and '$VAR' doesn't. In Compose, $VAR and ${VAR} are interpolated from the environment or the .env file, and $$ stands for a literal $. The tool translates in both directions so that a variable stays a variable and a literal stays a literal:

  • Run → Compose: -e "HOME_DIR=$HOME" becomes HOME_DIR=$HOME — Compose fills in the value at startup. A literal dollar sign, as in -e 'PRICE=$5', becomes PRICE=$$5.
  • Compose → Run: literal parts go into single quotes, and $$ turns back into $. ${NAME}, $NAME, and the forms with a default value (${NAME:-value}, ${NAME-value}, ${NAME:?…}, ${NAME:+…}) become a shell expansion in double quotes, e.g. -e NGINX_HOST="${HOST:-example.com}".

For security reasons, the tool only lets simple default values through to expansion (letters, digits, _ . , : / @ % + = -, and spaces). Everything else — quotes, $(…), backticks, backslashes — stays literal text in single quotes. That way a crafted Compose file can't execute code through the generated script. Keep in mind: Compose also reads variables from .env, the script doesn't — export them in your shell first.

The Reference tab

The third tab is a built-in lookup sheet and needs no input:

  • Flag Reference: docker run → Compose — a table grouped by topic (Container Identity, Image & Command, Ports & Volumes, Environment, Network, Lifecycle, Runtime Flags, Security, Resources, Logging, Healthcheck, Labels) with the run flag, the Compose key, and notes, for instance on --mount, --network host, --gpus, and deploy.resources.
  • Compose-only Features — depends_on (order yes, conditions no), build (becomes docker build), profiles/replicas/scale (ignored with a note), secrets/configs (file variants as a mount), and interpolation (${VAR:-default} stays a shell variable).
  • Restart Policies — no (default, never restart), always, unless-stopped, on-failure, and on-failure:3 (max. 3 retries).
  • Tips — line continuation with \, combined flags, several run commands as several services, checking with docker compose config, and quoting ports.

Limits — in brief

  • One run = one service. Several commands produce several services in one file; options without a Compose equivalent (e.g. --rm, -P, --cidfile) and unknown flags are dropped, mostly with a note — the details dropped without one are listed under "Direction 1: Run → Compose".
  • Some Compose-only constructs get lost when you convert from Compose to run: depends_on conditions, profiles, replicas/scale, secrets without a file, and unsupported keys.
  • No docker/podman in the background. The tool doesn't run or check anything against a real engine — it's a pure text-to-text translator. Validate the generated file with docker compose config before you use it.

For the introduction and target audiences, see the overview page; for concrete walkthroughs, the examples. You can try everything directly in the tool.