Skip to content

Podman Compose

So far, every example has involved a single container. You run one image, do some work, and stop it. But some workflows require multiple containers running at the same time.

Suppose you want an RStudio Server container for R analysis and a JupyterLab container for Python work. You could start each one manually with separate podman run commands. You would need to remember the port mappings, volume mounts, and other flags for each container. Stopping them means running podman stop twice.

This gets tedious fast. It is also error prone. A single compose file solves this by defining all your containers in one place.

Other common multi-container setups include:

  • A web application plus a PostgreSQL database for storing results
  • An analysis container plus a file browser for viewing outputs
  • A pipeline runner plus a monitoring dashboard

podman-compose is a command line tool that reads a docker-compose.yml file and manages multiple containers at once. It starts them together, stops them together, and handles the networking between them.

The compose file format is the same one Docker Compose uses. If you find a docker-compose.yml in a project on GitHub, podman-compose can run it without changes. This is a major advantage. You get access to the entire ecosystem of existing compose files.

On Ubuntu or Debian:

Terminal window
$ sudo apt install podman-compose

Alternatively, install it with pip:

Terminal window
$ pip install podman-compose

Verify the installation:

Terminal window
$ podman-compose --version

A compose file is a YAML file named docker-compose.yml. It defines services. Each service becomes a container.

Here is a compose file for a bioinformatics development environment with RStudio and JupyterLab:

docker-compose.yml
services:
rstudio:
image: rocker/rstudio:4.4
ports:
- "8787:8787"
volumes:
- ./data:/home/rstudio/data:ro
- ./results:/home/rstudio/results
- ./scripts:/home/rstudio/scripts
environment:
- PASSWORD=changeme
jupyter:
image: jupyter/scipy-notebook:latest
ports:
- "8888:8888"
volumes:
- ./data:/home/jovyan/data:ro
- ./results:/home/jovyan/results
- ./notebooks:/home/jovyan/notebooks

Let’s walk through this file.

services: is the top level key. Everything below it defines the containers you want to run.

rstudio: and jupyter: are service names. You choose these names. They identify each container.

image: specifies which container image to use. This is the same image you would pass to podman run.

ports: maps ports from the host to the container. The format is "host_port:container_port". RStudio listens on 8787 and Jupyter on 8888.

volumes: mounts directories from your host into the container. The syntax matches the -v flag from podman run. Both services share the same data/ and results/ directories.

environment: sets environment variables inside the container. RStudio Server requires a password.

Notice that both containers mount the same data/ and results/ directories. This means your R scripts and Python notebooks can read the same input data and write to the same results folder.

Start all services defined in the compose file:

Terminal window
$ podman-compose up

This runs in the foreground. You will see logs from both containers in your terminal.

To start in the background, add the -d flag:

Terminal window
$ podman-compose up -d

The containers start and you get your terminal back. This is the mode you will use most often.

See which services are running:

Terminal window
$ podman-compose ps

This shows each container’s name, status, and port mappings.

See the output from all services:

Terminal window
$ podman-compose logs

Follow logs in real time:

Terminal window
$ podman-compose logs -f

View logs for a specific service:

Terminal window
$ podman-compose logs jupyter

Stop and remove all containers:

Terminal window
$ podman-compose down

This stops every container defined in the compose file and removes them. Your data in mounted volumes is preserved.

Sometimes you need a custom image that does not exist on a registry. Instead of building it separately, you can tell compose to build it for you.

Suppose you have a Dockerfile for a custom bioinformatics image:

# biotools/Dockerfile
FROM ubuntu:24.04
RUN apt-get update && \
apt-get install -y samtools bcftools bedtools && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /data

Reference it in your compose file with build: instead of image::

docker-compose.yml
services:
biotools:
build: ./biotools
volumes:
- ./data:/data:ro
- ./results:/results
jupyter:
image: jupyter/scipy-notebook:latest
ports:
- "8888:8888"
volumes:
- ./data:/home/jovyan/data:ro
- ./results:/home/jovyan/results
- ./notebooks:/home/jovyan/notebooks

The build: ./biotools line tells compose to look for a Dockerfile in the biotools/ directory and build the image before starting the container.

When you run podman-compose up, compose builds the custom image first. Then it starts both services. If the Dockerfile has not changed, compose reuses the cached image on subsequent runs.

You can force a rebuild with:

Terminal window
$ podman-compose up --build

Here is a complete bioinformatics project that uses compose to run an analysis container alongside a results viewer.

variant_project/
├── docker-compose.yml
├── analysis/
│ └── Dockerfile
├── data/
│ ├── sample.bam
│ └── reference.fa
├── results/
├── scripts/
│ └── call_variants.sh
└── notebooks/
# analysis/Dockerfile
FROM ubuntu:24.04
RUN apt-get update && \
apt-get install -y \
samtools \
bcftools \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
CMD ["bash"]
docker-compose.yml
services:
analysis:
build: ./analysis
volumes:
- ./data:/workspace/data:ro
- ./results:/workspace/results
- ./scripts:/workspace/scripts:ro
stdin_open: true
tty: true
viewer:
image: jupyter/scipy-notebook:latest
ports:
- "8888:8888"
volumes:
- ./results:/home/jovyan/results:ro
- ./notebooks:/home/jovyan/notebooks

The analysis service builds a custom image with samtools and bcftools. It mounts your data as read only and writes output to the results directory. The stdin_open and tty options keep the container running so you can attach to it.

The viewer service runs JupyterLab. It mounts the results directory as read only. You can open notebooks to visualize and explore the variant calls.

Start both services:

Terminal window
$ cd variant_project
$ podman-compose up -d

Run a command inside the analysis container:

Terminal window
$ podman-compose exec analysis bash /workspace/scripts/call_variants.sh

Open JupyterLab in your browser at http://localhost:8888 to view the results.

When you are finished, tear everything down:

Terminal window
$ podman-compose down

All containers stop and are removed. Your data, results, and notebooks remain on disk because they live in mounted volumes.

Task Command
Start all services podman-compose up
Start in background podman-compose up -d
Stop and remove all services podman-compose down
List running services podman-compose ps
View logs podman-compose logs
Follow logs in real time podman-compose logs -f
View logs for one service podman-compose logs <service>
Run a command in a service podman-compose exec <service> <command>
Rebuild images and start podman-compose up --build
Force rebuild without cache podman-compose build --no-cache

With Linux fundamentals and containers covered, the next sections teach R and Python programming.