DEV Community

David Tio
David Tio

Posted on • Originally published at blog.dtio.app

Docker Compose Explained: One File, One Container (2026)

๐Ÿณ Docker Compose Explained: One File, One Container (2026)

Quick one-liner: Replace docker run commands with a docker-compose.yml file. One command to start or tear down any container, reproducibly, every time.


๐Ÿค” Why This Matters

In the last post, you connected containers by building a custom bridge network and running CloudBeaver + PostgreSQL by hand:

$ docker network create dtstack
$ docker run -d --rm --name dtpg \
    --network dtstack \
    -e POSTGRES_PASSWORD=docker \
    -e POSTGRES_DB=testdb \
    -v pgdata:/var/lib/postgresql/data \
    --tmpfs /var/run/postgresql \
    postgres:17
$ docker run -d --rm --name cloudbeaver \
    --network dtstack \
    -p 8978:8978 \
    -v cbdata:/opt/cloudbeaver/workspace \
    dbeaver/cloudbeaver:latest
Enter fullscreen mode Exit fullscreen mode

Three commands. That's not the problem.

The problem is:

  • The second command is a 150-character wall of flags
  • One typo in --tmpfs and PostgreSQL silently starts but won't accept connections
  • Forget --network dtstack and the containers won't find each other
  • Tear it down and rebuild? Type it all again
  • What about when you have 5 containers? 10?

There's a better way.

Docker Compose lets you define this entire stack in a single YAML file:

$ docker compose up -d
Enter fullscreen mode Exit fullscreen mode

One command. Same result. Every time.

Here's how it works. Instead of typing flags every time, you write a docker-compose.yml file that captures everything. You list the image, ports, volumes, environment variables, and networks. Then you run docker compose up -d and Docker does the rest. Start it, stop it, tear it down. All with one command.

We'll start by composing each of our containers individually. One compose file for PostgreSQL. One for CloudBeaver. You'll get comfortable with the up/ps/logs/down workflow.

By the end of this post, you'll never have to stare at another never-ending line of docker run flags again.


โœ… Prerequisites

  • Ep 1-6 completed. Docker is installed and running, you know volumes, networking, and port mapping. Rootless mode recommended.
  • Docker Compose plugin. Already installed as part of Blog-01/02. Just run docker compose version to verify.

Compose v2: The old docker-compose (with hyphen) is deprecated. Modern Docker ships docker compose (space) as a plugin. If docker compose version doesn't work, go back and re-run the installation steps in Blog-01 or Blog-02. The plugin was included there.


๐Ÿ“ฆ Your First docker-compose.yml

Create a directory for your PostgreSQL service:

$ mkdir -p dtstack-pg && cd dtstack-pg
Enter fullscreen mode Exit fullscreen mode

Create docker-compose.yml:

services:
  dtpg:
    container_name: dtpg
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: docker
      POSTGRES_DB: testdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    tmpfs:
      - /var/run/postgresql

volumes:
  pgdata:
Enter fullscreen mode Exit fullscreen mode

Four things to notice:

  1. services: is the top-level key. Each entry under services: is one container. We have one, and it's called dtpg.

  2. container_name gives it a clean name. Instead of Compose's auto-generated dtstack-pg-dtpg-1, we get dtpg. Same as --name in docker run.

  3. No --network flag. The network is implicit. We're not connecting to anything else yet. One container, one service.

  4. Volumes are declared at the bottom. Named volumes are defined in the volumes: block and referenced by the service. Docker creates them on first use.


๐Ÿš€ Start the Service

$ docker compose up -d
Enter fullscreen mode Exit fullscreen mode
[+] Running 3/3
 โœ” Network dtstack-pg_default  Created
 โœ” Volume dtstack-pg_pgdata    Created
 โœ” Container dtpg              Started
Enter fullscreen mode Exit fullscreen mode

One command creates a container, a network, and a volume. Everything you need.

Verify it's up:

$ docker compose ps
NAME   IMAGE         COMMAND                  SERVICE   CREATED         STATUS         PORTS
dtpg   postgres:17   "docker-entrypoint.sโ€ฆ"   dtpg      54 seconds ago  Up 54 seconds  5432/tcp
Enter fullscreen mode Exit fullscreen mode

๐Ÿ” Inspect the Service

View logs:

$ docker compose logs
Enter fullscreen mode Exit fullscreen mode
dtpg | PostgreSQL init process complete; ready for start up.
dtpg | database system is ready to accept connections
Enter fullscreen mode Exit fullscreen mode

Follow logs in real-time (like docker logs -f):

$ docker compose logs -f
Enter fullscreen mode Exit fullscreen mode

Press Ctrl-C to stop following.

Connect and verify:

$ docker compose exec dtpg psql -U postgres -c "SELECT version();"
Enter fullscreen mode Exit fullscreen mode
                                                      version
--------------------------------------------------------------------------------------------------------------------
 PostgreSQL 17.9 (Debian 17.9-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
(1 row)
Enter fullscreen mode Exit fullscreen mode

PostgreSQL is running. We used dtpg to target the container, and Compose knows exactly which one to hit.

Let's bring it down before we make changes:

$ docker compose down
Enter fullscreen mode Exit fullscreen mode
[+] Running 2/2
 โœ” Container dtpg              Removed
 โœ” Network dtstack-pg_default  Removed
Enter fullscreen mode Exit fullscreen mode

The volume survives. Your data is safe.


๐Ÿ“ Using Environment Files

Hardcoding passwords in YAML is bad practice. Move secrets to a .env file:

$ cat > .env << EOF
POSTGRES_PASSWORD=docker
POSTGRES_DB=testdb
EOF
Enter fullscreen mode Exit fullscreen mode

Update docker-compose.yml to reference them:

services:
  dtpg:
    container_name: dtpg
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - pgdata:/var/lib/postgresql/data
    tmpfs:
      - /var/run/postgresql

volumes:
  pgdata:
Enter fullscreen mode Exit fullscreen mode

Now docker compose up -d reads the variables automatically. Same command, cleaner file.


๐Ÿ›‘ Tear It Down

$ docker compose down
Enter fullscreen mode Exit fullscreen mode
[+] Running 2/2
 โœ” Container dtpg              Removed
 โœ” Network dtstack-pg_default   Removed
Enter fullscreen mode Exit fullscreen mode

The container and network are gone, but the volume survives. Your data is still right where you left it:

$ docker volume ls | grep dtstack
Enter fullscreen mode Exit fullscreen mode
local  dtstack-pg_pgdata
Enter fullscreen mode Exit fullscreen mode

To remove the volume too:

$ docker compose down --volumes
Enter fullscreen mode Exit fullscreen mode
[+] Running 1/1
 โœ” Volume dtstack-pg_pgdata  Removed
Enter fullscreen mode Exit fullscreen mode

Use --volumes when you want a clean slate. Leave it off when you want data to survive across restarts.


๐Ÿ“ฆ Second Compose File: CloudBeaver

Now let's do the same for CloudBeaver. It gets its own directory and its own compose file.

First, go back to your home directory:

$ cd ~
Enter fullscreen mode Exit fullscreen mode

Then create the CloudBeaver directory:

$ mkdir -p dtstack-cb && cd dtstack-cb
Enter fullscreen mode Exit fullscreen mode
services:
  cloudbeaver:
    container_name: cloudbeaver
    image: dbeaver/cloudbeaver:latest
    ports:
      - "8978:8978"
    volumes:
      - cbdata:/opt/cloudbeaver/workspace

volumes:
  cbdata:
Enter fullscreen mode Exit fullscreen mode

Start it:

$ docker compose up -d
Enter fullscreen mode Exit fullscreen mode
[+] Running 3/3
 โœ” Network dtstack-cb_default  Created
 โœ” Volume dtstack-cb_cbdata    Created
 โœ” Container cloudbeaver       Started
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:8978. CloudBeaver loads. โœ…

But there's no PostgreSQL on this network. CloudBeaver and PG live in separate compose projects. Different directories, different networks. They can't talk to each other yet.

Dรฉjร  vu. We solved this exact problem in the last post with custom bridge networks. Same concept, but this time we're doing it through Compose. We'll get there next post.

For now, let's clean up:

$ docker compose down --volumes
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“‹ Docker Run vs Docker Compose

Task docker run docker compose
Start docker run -d --name x --network n ... docker compose up -d
List docker ps docker compose ps
Logs docker logs x docker compose logs
Exec docker exec -it x sh docker compose exec x sh
Stop docker stop x docker compose down
Network docker network create Automatic

The docker compose commands are scoped to your project. docker compose ps only shows your stack's containers. It won't list everything running on your machine.


๐Ÿงช Exercise: Build Your Nextcloud Stack with Compose

Nextcloud is a self-hosted productivity platform. It functions just like Google Docs, but it runs on your own server. It needs four services: a database, a cache, a web server, and a PHP backend. You'll create four compose files, one per service, each in its own directory.

First, go back to your home directory:

$ cd ~
Enter fullscreen mode Exit fullscreen mode

Part 1: MariaDB

$ mkdir -p nc-db && cd nc-db
Enter fullscreen mode Exit fullscreen mode

Create .env:

$ cat > .env << EOF
MYSQL_ROOT_PASSWORD=nextcloud
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud
MYSQL_PASSWORD=nextcloud
EOF
Enter fullscreen mode Exit fullscreen mode

Create docker-compose.yml:

services:
  db:
    container_name: nc-db
    image: mariadb:11
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - dbdata:/var/lib/mysql

volumes:
  dbdata:
Enter fullscreen mode Exit fullscreen mode
$ docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Verify:

$ docker compose exec db mariadb -u root -pnextcloud -e "SHOW DATABASES;"
Enter fullscreen mode Exit fullscreen mode
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| nextcloud          |
| performance_schema |
| sys                |
+--------------------+
Enter fullscreen mode Exit fullscreen mode
$ docker compose down --volumes
Enter fullscreen mode Exit fullscreen mode

Part 2: Redis

$ cd ~
$ mkdir -p nc-redis && cd nc-redis
Enter fullscreen mode Exit fullscreen mode
services:
  redis:
    container_name: nc-redis
    image: redis:8.6
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data

volumes:
  redisdata:
Enter fullscreen mode Exit fullscreen mode
$ docker compose up -d
$ docker compose exec redis redis-cli PING
Enter fullscreen mode Exit fullscreen mode

You should get PONG.

$ docker compose down --volumes
Enter fullscreen mode Exit fullscreen mode

Part 3: Nextcloud PHP-FPM

$ cd ~
$ mkdir -p nc-php && cd nc-php
Enter fullscreen mode Exit fullscreen mode
services:
  php:
    container_name: nc-php
    image: nextcloud:fpm
    ports:
      - "9000:9000"
    volumes:
      - ./html:/var/www/html
Enter fullscreen mode Exit fullscreen mode
$ docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Nextcloud's PHP-FPM image comes with Nextcloud pre-installed. On first start, it runs its setup scripts and copies the app files into the bind-mounted html/ directory. You can see it populate:

$ ls html/
Enter fullscreen mode Exit fullscreen mode

You'll see Nextcloud's file structure. Things like index.php, core/, apps/, config/. The container put everything there for you.

$ docker compose down
Enter fullscreen mode Exit fullscreen mode

Part 4: Nginx

$ cd ~
$ mkdir -p nc-nginx && cd nc-nginx
Enter fullscreen mode Exit fullscreen mode
services:
  nginx:
    container_name: nc-nginx
    image: nginx:latest
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html
Enter fullscreen mode Exit fullscreen mode
$ mkdir -p html
$ cat > html/index.html << 'EOF'
<h2>Nextcloud is coming</h2>
EOF
Enter fullscreen mode Exit fullscreen mode
$ docker compose up -d
$ docker compose exec nginx curl localhost
Enter fullscreen mode Exit fullscreen mode

You should see <h2>Nextcloud is coming</h2>.

$ docker compose down
Enter fullscreen mode Exit fullscreen mode

๐Ÿ‘‰ Coming up: This isn't a full Nextcloud deployment yet, but you now have all the containers you need to get it running. Next post, we'll glue them all up and get it working. See you then.

๐Ÿ“š Want More? This guide covers the basics from Chapter 11: Using Docker Compose in my book, "Levelling Up with Docker". That's 14 chapters of practical, hands-on Docker guides.

> Note: The book has more content than this blog series. Some topics are only available in the book.

๐Ÿ“š Grab the book: "Levelling Up with Docker" on Amazon


Found this helpful? ๐Ÿ™Œ

Top comments (0)