# Docker Compose Builder

> Build a docker-compose.yml from a form and get it checked for common mistakes: privileged containers, database ports published to the world, orphan volumes. Nothing is executed.

Canonical: https://host-tools.com/tools/docker-compose-builder/

---

## What this tool does

It builds a `docker-compose.yml` from a form — services, images, builds, ports, volumes,
environment, networks, dependencies, healthchecks — and then reviews the result for problems that
actually break deployments or open doors. You can also paste an existing file and validate it.

It does not run Docker. There is no daemon, no socket, no CLI: the YAML is generated and inspected
as text.

## services, ports and expose are three different things

A **service** is one container definition. Its `image` or `build` says where it comes from. Then:

- `ports` **publishes** a container port on the host, so anything that can reach the host can reach
  the service. `8080:80` publishes container port 80 as host port 8080.
- `expose` only documents that the port exists; it does not publish anything. Other services on the
  same network can reach it anyway, because they talk to the container, not to the host.
- No `ports` and no `expose`: still reachable from other services on the same network by its
  service name.

That distinction is why `expose` is the right answer for a database that only your app needs.

## The database port mistake

```yaml
db:
  image: postgres:16
  ports:
    - 5432:5432   # anyone who can reach the host can now try to log in
```

Developers do this to connect from a GUI client, and it works — which is exactly the problem. On a
server with a public IP, PostgreSQL is now exposed to the internet. If you need host access, bind it
to loopback: `127.0.0.1:5432:5432`. The builder flags the open form for PostgreSQL, MySQL, MongoDB,
Redis, Elasticsearch and friends.

## Volumes and networks

`volumes` at the root level declares **named** volumes. A service uses one by naming it:
`dbdata:/var/lib/postgresql/data`. A volume that is declared and never used is dead configuration —
a sign of a copy-paste that was never finished. Same for networks: declare them, then attach the
services, or the isolation you think you have does not exist.

## depends_on does not wait for readiness

`depends_on` controls **start order**, not readiness. A container that is "started" may still be
initialising its database. If the dependency has a `healthcheck`, Compose can wait for it with
`condition: service_healthy`; that is the difference between "the container exists" and "the service
answers". The builder points this out when the dependency has a healthcheck.

## healthcheck

```yaml
healthcheck:
  test: ["CMD", "pg_isready", "-U", "postgres"]
  interval: 10s
  timeout: 5s
  retries: 5
```

A healthcheck without a `test` is useless: Docker has nothing to run. The form asks for the test
first and only then offers the timings.

## Scope

The builder covers `services` with the fields listed in the form, plus root-level `volumes` and
`networks`. It does not generate `deploy`, `secrets`, `configs`, `profiles` or multi-file overrides,
and it says so rather than producing something that looks complete. Pair it with the
[.env validator](/tools/env-validator/) before wiring `env_file` into a service.
