Docker/Podman Composer — Examples

Hands-on runs: nginx and Postgres to Compose, several commands with a network, dollar signs, Compose as a start script with build and depends_on, podman.

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

The manual explains both directions and every supported flag in detail. This page shows concrete conversions with input and the actual result. The tool converts live — paste the input and the result appears underneath a moment later. Tab and button names are given in their original spelling. Unless stated otherwise, the Compose project name field is empty.

Example 1: A simple nginx container (Run → Compose)

The classic — a short run command becomes a Compose file. Input in the Run → Compose tab:

docker run -d --name web -p 8080:80 nginx:alpine

Result in the Docker Compose YAML field:

yaml
services:
  web:
    image: nginx:alpine
    container_name: web
    ports:
      - "8080:80"

Note: the -d (detach) disappears — it's implicit in Compose. The service name web comes from --name; without --name, the tool would have derived it from the image (nginx). The port mapping is quoted because older YAML readers might otherwise interpret 8080:80 as a number.

Example 2: Volumes, ports, environment, and a health check

A realistic database container with several flag types. Input:

Terminal
docker run -d \
  --name db \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  -p 5432:5432 \
  --restart unless-stopped \
  --health-cmd "pg_isready -U postgres" \
  --health-interval 10s \
  --memory 512m \
  postgres:16

Result:

yaml
services:
  db:
    image: postgres:16
    container_name: db
    restart: unless-stopped
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=secret
    healthcheck:
      test:
        - CMD-SHELL
        - pg_isready -U postgres
      interval: 10s
    mem_limit: 512m

volumes:
  pgdata:
    external: true

Three things are worth a closer look: the --health-cmd becomes a test list in CMD-SHELL form, and --memory lands as mem_limit. Thanks to the tokenizer, the quoted health-check command stays a single argument. And the tool declares the named volume pgdata at the bottom as external: true — that way Compose uses the volume your docker run already created instead of making a new <project>_pgdata.

Example 3: A multi-flag command with combined short flags

Here combined flags and a trailing command come into play. Input:

docker run -it --rm -p 3000:3000 node:20 bash

Result:

yaml
services:
  node:
    image: node:20
    command:
      - bash
    ports:
      - "3000:3000"
    stdin_open: true
    tty: true

A yellow note appears below the output:

--rm: Compose keeps stopped containers; use "docker compose run --rm" for one-off runs.

What happens here: -it is split into -i and -t, yielding stdin_open: true and tty: true. The --rm is dropped because it has no Compose equivalent — the note tells you the alternative. The bash after the image name becomes the command, written as a list with one word per entry. Since no --name is set, the tool derives the service name from the image: node.

Example 4: The full feature set (the Example button)

The Example button in the run tab loads a small setup of three commands: a network, an nginx web server, and a Node app that share the network:

Terminal
# Network shared by both containers
docker network create mynet

docker run -d \
  --name webserver \
  -p 8080:80 \
  -v /var/www:/usr/share/nginx/html:ro \
  -v nginx-logs:/var/log/nginx \
  -e NGINX_HOST=example.com \
  --restart unless-stopped \
  --network mynet \
  --label app=webserver \
  --memory 256m \
  --health-cmd "curl -fs http://localhost/ || exit 1" \
  --health-interval 30s \
  --log-opt max-size=10m \
  nginx:1.27-alpine

docker run -d --name app --network mynet -w /app -v "$PWD/app:/app" \
  -e NODE_ENV=production -e "GREETING=Hello, world" \
  node:22-alpine node server.js

Result:

yaml
services:
  webserver:
    image: nginx:1.27-alpine
    container_name: webserver
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - /var/www:/usr/share/nginx/html:ro
      - nginx-logs:/var/log/nginx
    environment:
      - NGINX_HOST=example.com
    networks:
      - mynet
    healthcheck:
      test:
        - CMD-SHELL
        - curl -fs http://localhost/ || exit 1
      interval: 30s
    logging:
      options:
        max-size: 10m
    labels:
      - app=webserver
    mem_limit: 256m
  app:
    image: node:22-alpine
    container_name: app
    command:
      - node
      - server.js
    volumes:
      - $PWD/app:/app
    environment:
      - NODE_ENV=production
      - GREETING=Hello, world
    networks:
      - mynet
    working_dir: /app

networks:
  mynet:
    name: mynet

volumes:
  nginx-logs:
    external: true

The two run commands become two services in one file; the comment line is skipped. Because docker network create mynet is part of the input, the tool declares the network with name: mynet — so Compose uses exactly that network instead of a new <project>_mynet. The volume nginx-logs wasn't created anywhere, so it counts as external. The || in the health check survives because it sits inside quotes. $PWD stays a variable that Compose fills in at startup.

Example 5: The reverse direction — Compose → Run with multiple services

Switch to the Compose → Run tab, enter shop in the Compose project name field, and paste a Compose file with two services:

yaml
services:
  web:
    image: nginx:alpine
    container_name: web
    ports:
      - "8080:80"
    restart: unless-stopped
    networks:
      - mynet
    depends_on:
      - app
  app:
    image: node:20-alpine
    container_name: app
    restart: always
    working_dir: /app
    volumes:
      - ./app:/app
    environment:
      NODE_ENV: production
      PORT: "3000"
    networks:
      - mynet

networks:
  mynet:

With runtime docker, you get this script:

Terminal
# Networks and volumes
docker network inspect shop_mynet >/dev/null 2>&1 || docker network create shop_mynet

# app
docker run -d \
  --name app \
  --restart always \
  --workdir /app \
  --network shop_mynet \
  --network-alias app \
  -v ./app:/app \
  -e NODE_ENV=production \
  -e PORT=3000 \
  node:20-alpine

# web
docker run -d \
  --name web \
  --restart unless-stopped \
  --network shop_mynet \
  --network-alias web \
  -p 8080:80 \
  nginx:alpine

Several things stand out. First, the script creates the network — docker run wouldn't — and only if it doesn't exist yet. As with Compose, it's called shop_mynet. Because web depends on app via depends_on, app starts first. Each container gets its service name as --network-alias, so web reaches the app as app, just like under Compose. The environment object is unpacked into individual -e KEY=VALUE flags. A yellow note reminds you that the relative path ./app depends on the current directory — run the script next to the Compose file. run.sh downloads the whole thing as a ready-to-run script (with #!/bin/sh and set -e) — start it with sh run.sh, or run chmod +x run.sh first.

Example 6: Generating commands for Podman

The same Compose input, but with podman selected in the Runtime switch. The network and the web service then become:

Terminal
# Networks and volumes
podman network inspect shop_mynet >/dev/null 2>&1 || podman network create shop_mynet

# web
podman run -d \
  --name web \
  --restart unless-stopped \
  --network shop_mynet \
  --network-alias web \
  -p 8080:80 \
  nginx:alpine

The difference is the command word podman instead of docker, including for the network commands — the flags stay the same, because Podman reproduces the docker run options compatibly. Only GPU reservations are written differently for Podman (--device nvidia.com/gpu=all). Handy when you want to test an existing Compose file by hand on a Podman host. The tool remembers your runtime choice for your next visit.

Example 7: Dollar signs and variables

A $ means something different in the shell than in Compose. Input in the Run → Compose tab:

docker run -d --name shop -e 'PRICE=$5' -e "HOME_DIR=$HOME" -v "$PWD/data:/data" \
  --network host myorg/shop:1.2

Result:

yaml
services:
  shop:
    image: myorg/shop:1.2
    container_name: shop
    volumes:
      - $PWD/data:/data
    environment:
      - PRICE=$$5
      - HOME_DIR=$HOME
    network_mode: host

'$5' is in single quotes, so it's a literal dollar sign — in Compose that becomes $$5. $HOME and $PWD are variables and stay that way; Compose fills them in at startup. --network host isn't a network Compose could manage, so it becomes network_mode: host.

The reverse direction shows how the tool makes such values shell-safe again. Project name shop, input in the Compose → Run tab:

yaml
services:
  shop:
    image: myorg/shop:1.2
    environment:
      - PRICE=$$5
      - HOME_DIR=$HOME
      - GREETING=${GREETING:-Hello}
    volumes:
      - $PWD/data:/data
    network_mode: host

Result:

Terminal
# shop
docker run -d \
  --name shop-shop-1 \
  --network host \
  -v "$PWD"/data:/data \
  -e 'PRICE=$5' \
  -e HOME_DIR="$HOME" \
  -e GREETING="${GREETING:-Hello}" \
  myorg/shop:1.2

$$5 turns back into a literal $5 in single quotes, while the variables end up in double quotes and expand in the shell — default value included. Without container_name, the container is called shop-shop-1, just as Compose would name it.

Example 8: A Compose file with build, depends_on, and two networks

The Example button in the Compose → Run tab loads the shop project with everything that makes the reverse direction interesting — name: shop is in the file, so the project field can stay empty:

yaml
name: shop

services:
  webserver:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
      - nginx-logs:/var/log/nginx
    environment:
      NGINX_HOST: ${HOST:-example.com}
    networks:
      - front
      - back
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost/ || exit 1"]
      interval: 30s
      retries: 3
    logging:
      driver: json-file
      options:
        max-size: 10m
    depends_on:
      app:
        condition: service_healthy

  app:
    build: ./app
    working_dir: /app
    command: node server.js
    environment:
      NODE_ENV: production
      PRICE_TAG: "costs $$5"
    networks:
      back:
        aliases: [api]
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "0.5"
    stop_grace_period: 1m30s

networks:
  front:
  back:
    internal: true

volumes:
  nginx-logs:

Result (runtime docker):

Terminal
# Networks and volumes
docker network inspect shop_back >/dev/null 2>&1 || docker network create --internal shop_back
docker network inspect shop_front >/dev/null 2>&1 || docker network create shop_front

# 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

# webserver
docker run -d \
  --name shop-webserver-1 \
  --restart unless-stopped \
  --network shop_front \
  --network-alias webserver \
  -p 8080:80 \
  -v ./html:/usr/share/nginx/html:ro \
  -v shop_nginx-logs:/var/log/nginx \
  -e NGINX_HOST="${HOST:-example.com}" \
  --log-driver json-file \
  --log-opt max-size=10m \
  --health-cmd 'wget -qO- http://localhost/ || exit 1' \
  --health-interval 30s \
  --health-retries 3 \
  nginx:1.27-alpine
docker network connect --alias webserver shop_back shop-webserver-1

The script shows nearly every trick of the reverse direction: build: ./app becomes a docker build -t shop-app in front of the run. The internal network back is created with --internal. deploy.resources.limits turns into --memory and --cpus, 1m30s into --stop-timeout 90, and command: node server.js follows the image. $$5 becomes a literal $5 in single quotes, ${HOST:-example.com} a shell variable with a default value. The webserver joins the first network via --network and the second via docker network connect; the named volume is called shop_nginx-logs, just as with Compose.

Two yellow notes come with it: the relative path ./html depends on the current directory, and the script doesn't check the service_healthy condition — it only starts the containers in the right order.


Going deeper: the manual for every flag, the tips & tricks for the limits and knacks. You can try everything directly in the tool.