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 livePaste cells from Excel or Google Sheets into the first header to replace the table · Tab / Enter / arrows move · Shift+F10 opens row & column actions
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
| Column | Alignment | What to put here |
|---|---|---|
| Service | left :--- | 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 Port | right ---: | 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 Port | right ---: | 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. |
| Protocol | center :---: | 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 Target | left :--- | 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
- 1Run
docker compose configand readportsandvolumesfrom its output instead of the raw file. It merges override files and substitutes variables, so you see the values that will actually be applied. - 2Bring the stack up and confirm what really got published with
docker compose psordocker port <container>. List mounts per container withdocker inspect --format '{{json .Mounts}}' <container>. - 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.
- 4Test each host port for local conflicts before publishing the table:
ss -ltnpon Linux, orlsof -iTCP -sTCP:LISTEN -n -Pon macOS. - 5For every volume, write down what destroys it.
docker compose downkeeps named volumes;docker compose down -vremoves 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:8080when 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 withdocker port <container>after the stack starts.Publishing Postgres or Redis as plain
5432:5432on 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 to127.0.0.1:5432:5432, or skip publishing when only other containers need access.Pointing one container at another through
localhostor the host port.Inside a container,
localhostis that container itself. Use the service name and the container port, such aspostgres-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:22as 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
postgresimage moved its volume to/var/lib/postgresqlfrom version 18, sopgdata:/var/lib/postgresql/datais only right for 17 and older. Pin the tag and check the image page.
Related Markdown Table Templates
Browse all 50 templatesCI/CD Pipeline Stages & Automation Gates
Document multi-stage CI/CD workflows across linting, unit testing, security scanning, container builds, and production deployments.
Environment Variables (.env) Matrix
Document required and optional environment keys, secret classifications, types, and staging vs production defaults.
Git Workflow & Branching Governance
Define branching conventions, source bases, pull request targets, required approval reviews, and CI automation gates.
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.