Skip to content
Developer Template5 columns · 5 sample rows· Updated

Docker Container Ports & Volumes Matrix Markdown Table Template

A ports and volumes matrix is the map of where a containerised stack touches the machine it runs on. Ports are the holes from the host into each container; volumes are the places where data outlives the container. Anyone who has run docker compose up and hit “port is already allocated”, or lost a local database after a cleanup command, knows why this deserves a table rather than a paragraph.

Edit This Template

Edit cells, add rows, or sort — the Markdown updates live
Row
1
web-frontend
3000
3000
./src:/app/src (bind mount)
2
api-gateway
8080
80
3
postgres-db
5432
5432
pgdata:/var/lib/postgresql (named volume; postgres:18+ image path)
4
redis-cache
6379
6379
redisdata:/data
5
mailpit
8025
8025
Output
| Service        | Host Port | Container Port | Protocol | Volume / Mount Target                                                  |
| :------------- | --------: | -------------: | :------: | :--------------------------------------------------------------------- |
| `web-frontend` |    `3000` |         `3000` |   TCP    | `./src:/app/src` (bind mount)                                          |
| `api-gateway`  |    `8080` |           `80` |   TCP    | None (stateless)                                                       |
| `postgres-db`  |    `5432` |         `5432` |   TCP    | `pgdata:/var/lib/postgresql` (named volume; `postgres:18`+ image path) |
| `redis-cache`  |    `6379` |         `6379` |   TCP    | `redisdata:/data`                                                      |
| `mailpit`      |    `8025` |         `8025` |   TCP    | None (SMTP on 1025, internal only)                                     |
5 rows × 5 columns
Rendered Preview
ServiceHost PortContainer PortProtocolVolume / Mount Target
web-frontend30003000TCP./src:/app/src (bind mount)
api-gateway808080TCPNone (stateless)
postgres-db54325432TCPpgdata:/var/lib/postgresql (named volume; postgres:18+ image path)
redis-cache63796379TCPredisdata:/data
mailpit80258025TCPNone (SMTP on 1025, internal only)

When to Use This Table

It gets read at specific moments. A new developer checks which local ports the stack will grab before it collides with the Postgres they already run natively. Whoever reviews a Compose change checks that a database port was not quietly exposed to the whole network. Ops reads the volume column to decide what needs a backup and what can be thrown away. And during an incident, someone needs to know whether the API reaches the database at postgres-db:5432 or through the host.

The most useful version separates the two directions clearly. Host Port is what you type into a browser or psql on your laptop; Container Port is what the process inside listens on, and also what other containers on the same network use. A good matrix also records the bind address when it is not all interfaces, and labels each mount as a named volume, a bind mount, or nothing. A poor one copies the ports: list without saying which service actually needs to be reachable from outside. The connection strings that use these names and ports belong in a companion environment variable table.

What Goes in Each Column

ColumnAlignmentWhat to put here
Serviceleft :---The service key exactly as written in compose.yaml, in backticks. That key doubles as the DNS name other containers use on the shared network, so api-gateway here must match api-gateway in connection strings. Left-aligned and listed in the same order as the Compose file.
Host Portright ---:The left half of HOST:CONTAINER: the port on your machine. Right-aligned so numbers line up. If the mapping binds to a specific address, include it (127.0.0.1:5432). Write — for services that publish nothing and are reachable only from other containers.
Container Portright ---:The right half of the mapping: the port the process inside the container listens on. Take it from the image documentation or its EXPOSE instruction, not from guesswork. Other containers connect to this number, never to the host port, so it is the one that appears in internal URLs.
Protocolcenter :---:Docker publishes TCP unless told otherwise, so TCP is the default. Write UDP for DNS, syslog, WireGuard, or game servers, which need the /udp suffix (53:53/udp). A service needing both protocols requires two separate mappings, so give it two rows or write TCP + UDP.
Volume / Mount Targetleft :---Use the same source:target notation as Compose and label the kind. A bare name such as pgdata is a named volume managed by Docker; a path starting with ./ or / is a bind mount from the host. Add :ro when the container must not write, and write “None” plus the reason when it is stateless.

How to Fill It In

  1. 1Run docker compose config and read ports and volumes from its output instead of the raw file. It merges override files and substitutes variables, so you see the values that will actually be applied.
  2. 2Bring the stack up and confirm what really got published with docker compose ps or docker port <container>. List mounts per container with docker inspect --format '{{json .Mounts}}' <container>.
  3. 3Check every container port against the image’s documentation. For official images, the Docker Hub page lists the listen port and the data directory to mount; pin the image tag so the path you document stays correct.
  4. 4Test each host port for local conflicts before publishing the table: ss -ltnp on Linux, or lsof -iTCP -sTCP:LISTEN -n -P on macOS.
  5. 5For every volume, write down what destroys it. docker compose down keeps named volumes; docker compose down -v removes them, and a bind mount survives both because it lives in your project directory.

Ready-to-Paste Variations

Loopback-only bindings for a dev stack

Datastores bound to 127.0.0.1 are reachable from your laptop but not from other machines on the network. The Mapping column can be pasted straight into ports:.

| Service | Mapping | Reach it at | Notes |
| :--- | :--- | :--- | :--- |
| `web` | `"3000:3000"` | `http://localhost:3000` | Dev server must listen on `0.0.0.0` inside the container |
| `db` | `"127.0.0.1:5432:5432"` | `localhost:5432` | Local tools only; other containers use `db:5432` |
| `cache` | `"127.0.0.1:6379:6379"` | `localhost:6379` | No auth configured, so never publish on all interfaces |
| `mail` | `"8025:8025"` | `http://localhost:8025` | Mailpit web UI; SMTP on `1025` stays internal |

Volume inventory

For runbooks and backup plans. One row per mount, focused on what is stored and how it can be lost.

| Mount | Kind | Used by | Holds | Removed by |
| :--- | :--- | :--- | :--- | :--- |
| `pgdata` | Named volume | `db` | All database files | `docker compose down -v` |
| `redisdata` | Named volume | `cache` | RDB snapshots | `docker compose down -v` |
| `./src` | Bind mount | `web` | Source code for hot reload | Nothing; it is your working tree |
| `./nginx.conf` | Bind mount (`:ro`) | `proxy` | Reverse proxy config | Nothing; tracked in Git |

Internal vs external access

For microservice diagrams. Shows that most services never need a published port at all.

| Service | From the host | From other containers | Published |
| :--- | :--- | :--- | :---: |
| `gateway` | `http://localhost:8080` | `http://gateway:80` | Yes |
| `orders` | — | `http://orders:8000` | No |
| `billing` | — | `http://billing:8000` | No |
| `queue` | — | `amqp://queue:5672` | No |

Common Mistakes

  • Writing the mapping backwards, as 80:8080 when the app listens on 8080.

    Docker’s order is always HOST:CONTAINER. Read it as “my machine’s port, then the container’s port”, and confirm with docker port <container> after the stack starts.

  • Publishing Postgres or Redis as plain 5432:5432 on a laptop that joins shared Wi-Fi.

    A plain mapping listens on every interface, and on Linux Docker’s own firewall rules are applied ahead of tools like ufw. Bind to 127.0.0.1:5432:5432, or skip publishing when only other containers need access.

  • Pointing one container at another through localhost or the host port.

    Inside a container, localhost is that container itself. Use the service name and the container port, such as postgres-db:5432, which works whether or not the port is published.

  • Leaving short-syntax ports unquoted in YAML.

    YAML 1.1 parsers can read a value like 22:22 as a base-60 number. Quote every mapping ("8080:80"), as the Compose documentation recommends.

  • Copying a database mount path from an old tutorial.

    Data directories change between image versions. The official postgres image moved its volume to /var/lib/postgresql from version 18, so pgdata:/var/lib/postgresql/data is only right for 17 and older. Pin the tag and check the image page.

Browse all 50 templates
FAQ

Docker Container Ports & Volumes Matrix: Frequently Asked Questions

What is the difference between ports and expose in Compose?

ports publishes a container port on the host so you can reach it from outside Docker. expose publishes nothing; it only records which ports the service listens on. Containers on the same network can reach each other’s ports either way.

Should a database use a named volume or a bind mount?

Use a named volume. Docker manages its ownership and location, and on Docker Desktop it avoids the slower file-sharing layer that bind mounts go through on macOS and Windows. Keep bind mounts for things you edit on the host, like source code and config files.

Why does the published port refuse connections?

Usually the process inside binds only to 127.0.0.1, which is unreachable through Docker’s port forwarding. Make it listen on 0.0.0.0; for example, the Vite dev server needs --host. Also confirm the container port in the mapping matches the port the process actually uses.

Why did my named volume start with files in it?

When an empty named volume is first mounted over a directory that already contains files in the image, Docker copies those files into the volume. Bind mounts never do this: they hide whatever the image had at that path.