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

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

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

πŸ€” Why This Matters

βœ… Prerequisites

πŸ“¦ Your First docker-compose.yml

πŸš€ Start the Service

πŸ” Inspect the Service

πŸ“ Using Environment Files

πŸ›‘ Tear It Down

πŸ“¦ Second Compose File: CloudBeaver

πŸ“‹ Docker Run vs Docker Compose

πŸ§ͺ Exercise: Build Your Nextcloud Stack with Compose

Part 1: MariaDB

Part 2: Redis

Part 3: Nextcloud PHP-FPM

Part 4: Nginx 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. In the last post, you connected containers by building a custom bridge network and running CloudBeaver + PostgreSQL by hand: Three commands. That's not the problem. There's a better way. Docker Compose lets you define this entire stack in a single YAML file: 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. 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. Create a directory for your PostgreSQL service: Create docker-compose.yml: Four things to notice: services: is the top-level key. Each entry under services: is one container. We have one, and it's called dtpg. 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. No --network flag. The network is implicit. We're not connecting to anything else yet. One container, one service. 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. One command creates a container, a network, and a volume. Everything you need. Follow logs in real-time (like docker logs -f): Press Ctrl-C to stop following. 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: The volume survives. Your data is safe. Hardcoding passwords in YAML is bad practice. Move secrets to a .env file: Update docker-compose.yml to reference them: Now docker compose up -d reads the variables automatically. Same command, cleaner file. The container and network are gone, but the volume survives. Your data is still right where you left it: To remove the volume too: Use --volumes when you want a clean slate. Leave it off when you want data to survive across restarts. 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: Then create the CloudBeaver directory: 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: 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. 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: Create docker-compose.yml: 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: You'll see Nextcloud's file structure. Things like index.php, core/, apps/, config/. The container put everything there for you. You should see <h2>Nextcloud is coming</h2>. πŸ‘‰ 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? πŸ™Œ Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ -weight: 500;">docker network create dtstack $ -weight: 500;">docker run -d --rm --name dtpg \ --network dtstack \ -e POSTGRES_PASSWORD=-weight: 500;">docker \ -e POSTGRES_DB=testdb \ -v pgdata:/var/lib/postgresql/data \ --tmpfs /var/run/postgresql \ postgres:17 $ -weight: 500;">docker run -d --rm --name cloudbeaver \ --network dtstack \ -p 8978:8978 \ -v cbdata:/opt/cloudbeaver/workspace \ dbeaver/cloudbeaver:latest $ -weight: 500;">docker network create dtstack $ -weight: 500;">docker run -d --rm --name dtpg \ --network dtstack \ -e POSTGRES_PASSWORD=-weight: 500;">docker \ -e POSTGRES_DB=testdb \ -v pgdata:/var/lib/postgresql/data \ --tmpfs /var/run/postgresql \ postgres:17 $ -weight: 500;">docker run -d --rm --name cloudbeaver \ --network dtstack \ -p 8978:8978 \ -v cbdata:/opt/cloudbeaver/workspace \ dbeaver/cloudbeaver:latest $ -weight: 500;">docker network create dtstack $ -weight: 500;">docker run -d --rm --name dtpg \ --network dtstack \ -e POSTGRES_PASSWORD=-weight: 500;">docker \ -e POSTGRES_DB=testdb \ -v pgdata:/var/lib/postgresql/data \ --tmpfs /var/run/postgresql \ postgres:17 $ -weight: 500;">docker run -d --rm --name cloudbeaver \ --network dtstack \ -p 8978:8978 \ -v cbdata:/opt/cloudbeaver/workspace \ dbeaver/cloudbeaver:latest $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose up -d $ mkdir -p dtstack-pg && cd dtstack-pg $ mkdir -p dtstack-pg && cd dtstack-pg $ mkdir -p dtstack-pg && cd dtstack-pg services: dtpg: container_name: dtpg image: postgres:17 environment: POSTGRES_PASSWORD: -weight: 500;">docker POSTGRES_DB: testdb volumes: - pgdata:/var/lib/postgresql/data tmpfs: - /var/run/postgresql volumes: pgdata: services: dtpg: container_name: dtpg image: postgres:17 environment: POSTGRES_PASSWORD: -weight: 500;">docker POSTGRES_DB: testdb volumes: - pgdata:/var/lib/postgresql/data tmpfs: - /var/run/postgresql volumes: pgdata: services: dtpg: container_name: dtpg image: postgres:17 environment: POSTGRES_PASSWORD: -weight: 500;">docker POSTGRES_DB: testdb volumes: - pgdata:/var/lib/postgresql/data tmpfs: - /var/run/postgresql volumes: pgdata: $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose up -d [+] Running 3/3 βœ” Network dtstack-pg_default Created βœ” Volume dtstack-pg_pgdata Created βœ” Container dtpg Started [+] Running 3/3 βœ” Network dtstack-pg_default Created βœ” Volume dtstack-pg_pgdata Created βœ” Container dtpg Started [+] Running 3/3 βœ” Network dtstack-pg_default Created βœ” Volume dtstack-pg_pgdata Created βœ” Container dtpg Started $ -weight: 500;">docker compose ps NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS dtpg postgres:17 "-weight: 500;">docker-entrypoint.s…" dtpg 54 seconds ago Up 54 seconds 5432/tcp $ -weight: 500;">docker compose ps NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS dtpg postgres:17 "-weight: 500;">docker-entrypoint.s…" dtpg 54 seconds ago Up 54 seconds 5432/tcp $ -weight: 500;">docker compose ps NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS dtpg postgres:17 "-weight: 500;">docker-entrypoint.s…" dtpg 54 seconds ago Up 54 seconds 5432/tcp $ -weight: 500;">docker compose logs $ -weight: 500;">docker compose logs $ -weight: 500;">docker compose logs dtpg | PostgreSQL init process complete; ready for -weight: 500;">start up. dtpg | database system is ready to accept connections dtpg | PostgreSQL init process complete; ready for -weight: 500;">start up. dtpg | database system is ready to accept connections dtpg | PostgreSQL init process complete; ready for -weight: 500;">start up. dtpg | database system is ready to accept connections $ -weight: 500;">docker compose logs -f $ -weight: 500;">docker compose logs -f $ -weight: 500;">docker compose logs -f $ -weight: 500;">docker compose exec dtpg psql -U postgres -c "SELECT version();" $ -weight: 500;">docker compose exec dtpg psql -U postgres -c "SELECT version();" $ -weight: 500;">docker compose exec dtpg psql -U postgres -c "SELECT version();" 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) 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) 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) $ -weight: 500;">docker compose down $ -weight: 500;">docker compose down $ -weight: 500;">docker compose down [+] Running 2/2 βœ” Container dtpg Removed βœ” Network dtstack-pg_default Removed [+] Running 2/2 βœ” Container dtpg Removed βœ” Network dtstack-pg_default Removed [+] Running 2/2 βœ” Container dtpg Removed βœ” Network dtstack-pg_default Removed $ cat > .env << EOF POSTGRES_PASSWORD=-weight: 500;">docker POSTGRES_DB=testdb EOF $ cat > .env << EOF POSTGRES_PASSWORD=-weight: 500;">docker POSTGRES_DB=testdb EOF $ cat > .env << EOF POSTGRES_PASSWORD=-weight: 500;">docker POSTGRES_DB=testdb EOF 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: 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: 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: $ -weight: 500;">docker compose down $ -weight: 500;">docker compose down $ -weight: 500;">docker compose down [+] Running 2/2 βœ” Container dtpg Removed βœ” Network dtstack-pg_default Removed [+] Running 2/2 βœ” Container dtpg Removed βœ” Network dtstack-pg_default Removed [+] Running 2/2 βœ” Container dtpg Removed βœ” Network dtstack-pg_default Removed $ -weight: 500;">docker volume ls | grep dtstack $ -weight: 500;">docker volume ls | grep dtstack $ -weight: 500;">docker volume ls | grep dtstack local dtstack-pg_pgdata local dtstack-pg_pgdata local dtstack-pg_pgdata $ -weight: 500;">docker compose down --volumes $ -weight: 500;">docker compose down --volumes $ -weight: 500;">docker compose down --volumes [+] Running 1/1 βœ” Volume dtstack-pg_pgdata Removed [+] Running 1/1 βœ” Volume dtstack-pg_pgdata Removed [+] Running 1/1 βœ” Volume dtstack-pg_pgdata Removed $ mkdir -p dtstack-cb && cd dtstack-cb $ mkdir -p dtstack-cb && cd dtstack-cb $ mkdir -p dtstack-cb && cd dtstack-cb services: cloudbeaver: container_name: cloudbeaver image: dbeaver/cloudbeaver:latest ports: - "8978:8978" volumes: - cbdata:/opt/cloudbeaver/workspace volumes: cbdata: services: cloudbeaver: container_name: cloudbeaver image: dbeaver/cloudbeaver:latest ports: - "8978:8978" volumes: - cbdata:/opt/cloudbeaver/workspace volumes: cbdata: services: cloudbeaver: container_name: cloudbeaver image: dbeaver/cloudbeaver:latest ports: - "8978:8978" volumes: - cbdata:/opt/cloudbeaver/workspace volumes: cbdata: $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose up -d [+] Running 3/3 βœ” Network dtstack-cb_default Created βœ” Volume dtstack-cb_cbdata Created βœ” Container cloudbeaver Started [+] Running 3/3 βœ” Network dtstack-cb_default Created βœ” Volume dtstack-cb_cbdata Created βœ” Container cloudbeaver Started [+] Running 3/3 βœ” Network dtstack-cb_default Created βœ” Volume dtstack-cb_cbdata Created βœ” Container cloudbeaver Started $ -weight: 500;">docker compose down --volumes $ -weight: 500;">docker compose down --volumes $ -weight: 500;">docker compose down --volumes $ mkdir -p nc-db && cd nc-db $ mkdir -p nc-db && cd nc-db $ mkdir -p nc-db && cd nc-db $ cat > .env << EOF MYSQL_ROOT_PASSWORD=nextcloud MYSQL_DATABASE=nextcloud MYSQL_USER=nextcloud MYSQL_PASSWORD=nextcloud EOF $ cat > .env << EOF MYSQL_ROOT_PASSWORD=nextcloud MYSQL_DATABASE=nextcloud MYSQL_USER=nextcloud MYSQL_PASSWORD=nextcloud EOF $ cat > .env << EOF MYSQL_ROOT_PASSWORD=nextcloud MYSQL_DATABASE=nextcloud MYSQL_USER=nextcloud MYSQL_PASSWORD=nextcloud EOF 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: 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: 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: $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose exec db mariadb -u root -pnextcloud -e "SHOW DATABASES;" $ -weight: 500;">docker compose exec db mariadb -u root -pnextcloud -e "SHOW DATABASES;" $ -weight: 500;">docker compose exec db mariadb -u root -pnextcloud -e "SHOW DATABASES;" +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | nextcloud | | performance_schema | | sys | +--------------------+ +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | nextcloud | | performance_schema | | sys | +--------------------+ +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | nextcloud | | performance_schema | | sys | +--------------------+ $ -weight: 500;">docker compose down --volumes $ -weight: 500;">docker compose down --volumes $ -weight: 500;">docker compose down --volumes $ cd ~ $ mkdir -p nc-redis && cd nc-redis $ cd ~ $ mkdir -p nc-redis && cd nc-redis $ cd ~ $ mkdir -p nc-redis && cd nc-redis services: redis: container_name: nc-redis image: redis:8.6 ports: - "6379:6379" volumes: - redisdata:/data volumes: redisdata: services: redis: container_name: nc-redis image: redis:8.6 ports: - "6379:6379" volumes: - redisdata:/data volumes: redisdata: services: redis: container_name: nc-redis image: redis:8.6 ports: - "6379:6379" volumes: - redisdata:/data volumes: redisdata: $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose exec redis redis-cli PING $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose exec redis redis-cli PING $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose exec redis redis-cli PING $ -weight: 500;">docker compose down --volumes $ -weight: 500;">docker compose down --volumes $ -weight: 500;">docker compose down --volumes $ cd ~ $ mkdir -p nc-php && cd nc-php $ cd ~ $ mkdir -p nc-php && cd nc-php $ cd ~ $ mkdir -p nc-php && cd nc-php services: php: container_name: nc-php image: nextcloud:fpm ports: - "9000:9000" volumes: - ./html:/var/www/html services: php: container_name: nc-php image: nextcloud:fpm ports: - "9000:9000" volumes: - ./html:/var/www/html services: php: container_name: nc-php image: nextcloud:fpm ports: - "9000:9000" volumes: - ./html:/var/www/html $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose down $ -weight: 500;">docker compose down $ -weight: 500;">docker compose down $ cd ~ $ mkdir -p nc-nginx && cd nc-nginx $ cd ~ $ mkdir -p nc-nginx && cd nc-nginx $ cd ~ $ mkdir -p nc-nginx && cd nc-nginx services: nginx: container_name: nc-nginx image: nginx:latest ports: - "8080:80" volumes: - ./html:/usr/share/nginx/html services: nginx: container_name: nc-nginx image: nginx:latest ports: - "8080:80" volumes: - ./html:/usr/share/nginx/html services: nginx: container_name: nc-nginx image: nginx:latest ports: - "8080:80" volumes: - ./html:/usr/share/nginx/html $ mkdir -p html $ cat > html/index.html << 'EOF' <h2>Nextcloud is coming</h2> EOF $ mkdir -p html $ cat > html/index.html << 'EOF' <h2>Nextcloud is coming</h2> EOF $ mkdir -p html $ cat > html/index.html << 'EOF' <h2>Nextcloud is coming</h2> EOF $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose exec nginx -weight: 500;">curl localhost $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose exec nginx -weight: 500;">curl localhost $ -weight: 500;">docker compose up -d $ -weight: 500;">docker compose exec nginx -weight: 500;">curl localhost $ -weight: 500;">docker compose down $ -weight: 500;">docker compose down $ -weight: 500;">docker compose down - 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? - 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 -weight: 500;">docker compose version to verify. - services: is the top-level key. Each entry under services: is one container. We have one, and it's called dtpg. - container_name gives it a clean name. Instead of Compose's auto-generated dtstack-pg-dtpg-1, we get dtpg. Same as --name in -weight: 500;">docker run. - No --network flag. The network is implicit. We're not connecting to anything else yet. One container, one -weight: 500;">service. - Volumes are declared at the bottom. Named volumes are defined in the volumes: block and referenced by the -weight: 500;">service. Docker creates them on first use. - LinkedIn: Share with your network - Twitter: Tweet about it - Questions? Drop a comment below or reach out on LinkedIn