·

Docker Compose Tutorial: Managing Multi-Container Apps Made Easy

Learn Docker Compose from scratch - This tutorial explains how to manage multi-container applications with a single YAML file and why Docker Compose is essential for selfhosting.
Docker Compose Tutorial: Managing Multi-Container Apps Made Easy

Docker Compose Tutorial for Beginners

In our last article, we looked at how Docker works under the hood. We learned what images are, how the daemon operates, and how to start a single container.

But let's be honest: Modern web applications are rarely solo acts.

A typical app often consists of a frontend, a backend service, a database like PostgreSQL, and maybe a caching layer like Redis. If you try to juggle all of this with individual docker run commands in the terminal, you'll quickly lose track. You'd have to manually create networks, manage IP addresses, and pay attention to startup order.

This is exactly where Docker Compose comes into play.

What is Docker Compose?

Docker Compose is a tool for multi-container applications. If the Dockerfile is the recipe for a single component (like a cake), then Docker Compose is the menu for the entire 3-course meal.

With a single file, the docker-compose.yml, you describe your complete infrastructure. The genius part: You can spin up your entire environment with a single command.

Why is this especially important for selfhosting?

If you want to host your own services on a VPS, you need a solution that manages multiple containers simultaneously. Without Docker Compose, things get messy fast. With Docker Compose, you have everything under control. Whether you're running Nextcloud, GitLab, or a WordPress instance.

The Anatomy of docker-compose.yml

Docker Compose uses YAML. This is a format that's readable for both humans and machines. Let's look at the most important building blocks.

Services: The Individual Containers

Under services you define the containers that should run. Each service gets a name (like web or db), which you can later use for internal communication.

Networks: Simple Networking

Container networking used to be complicated. With Docker Compose, it's almost magical. Services in the same network can reach each other via their service name. You don't have to hardcode IP addresses anymore. Your backend simply calls db:5432, and Docker routes it to the database container.

Volumes: Persistent Data

Containers are ephemeral. When you delete a database container, the data is gone. Unless you use volumes. In the Compose file, you define where data should be persistently stored on your host system.

Here's a practical example for a setup with a web app and database:

version: '3.8'

services:
  webapp:
    build: ./app
    ports:
      - '8080:80'
    depends_on:
      - database
    environment:
      - DB_HOST=database
      - DB_USER=${DB_USER}
      - DB_PASS=${DB_PASSWORD}
    restart: unless-stopped

  database:
    image: postgres:15-alpine
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    restart: unless-stopped

volumes:
  db_data:

Note: The variables like ${DB_USER} are environment variables. More on that in a moment.

Essential Docker Compose Commands

Docker Compose makes your workflow extremely efficient. Instead of long shell scripts, you only need a handful of commands:

Start containers:

docker-compose up -d

This command reads your configuration, pulls the necessary images, creates networks, and starts all containers in the background (-d for detached).

Check status:

docker-compose ps

Shows you immediately which services are running, which ports are occupied, and if a container has crashed.

View logs:

docker-compose logs -f

Essential for debugging. This lets you see the log outputs of all services bundled in one stream.

Stop containers:

docker-compose down

Stops the containers and cleans up the networks. Clean and tidy.

Rebuild containers:

docker-compose up -d --build

If you've made changes to your code, this command rebuilds the images and starts the containers.

Environment Variables and Security

In the code example above, we used ${DB_PASSWORD}. Hardcoded passwords in a yml file are a security risk – especially if you push the code to GitHub.

Docker Compose supports .env files automatically. Create a file named .env in the same folder and add the variables:

DB_USER=admin
DB_PASSWORD=supersecret

Important for security:

  • Add .env to your .gitignore
  • Use different passwords on your production server than locally
  • Never use default passwords like "admin123"

Docker Compose for Selfhosting on Your VPS

Why is Docker Compose perfect for selfhosting?

1. Easy Setup on a VPS You log in via SSH to your server, upload your docker-compose.yml, and start your complete infrastructure with one command.

2. Reproducible Environments Your local development looks exactly like your production server. No more "but it works on my machine!" problems.

3. Simple Updates New version of your app? Change the image tag in the docker-compose.yml, run docker-compose pull and docker-compose up -d – done.

4. Resource Efficiency On a VPS with 4GB RAM, you can easily host 5-10 smaller services simultaneously, thanks to the lean container architecture.

Practical Example: WordPress with MySQL

Here's a complete example of how you can selfhost WordPress:

version: '3.8'

services:
  wordpress:
    image: wordpress:latest
    ports:
      - '80:80'
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: ${DB_USER}
      WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wordpress_data:/var/www/html
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: ${DB_USER}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
    restart: unless-stopped

volumes:
  wordpress_data:
  db_data:

Here's how to start it on your server:

# Upload Docker Compose file
scp docker-compose.yml root@your-server.com:/opt/wordpress/

# On the server
ssh root@your-server.com
cd /opt/wordpress
echo "DB_USER=wpuser" > .env
echo "DB_PASSWORD=secure-password" >> .env
echo "DB_ROOT_PASSWORD=even-more-secure-password" >> .env

# Start
docker-compose up -d

Common Problems and Solutions

Problem: Container won't start

docker-compose logs service-name

Check the logs. Usually an environment variable is missing or a volume path doesn't exist.

Problem: Port already in use Change the port on the left side of the colon in docker-compose.yml: "8080:80" instead of "80:80"

Problem: Container can't access other services Check if all services are in the same network and if the service names are spelled correctly.

Best Practices for Docker Compose

  1. Always use restart: unless-stopped This makes your containers start automatically after a server reboot
  2. Separate development and production Use separate Compose files: docker-compose.yml for development, docker-compose.prod.yml for production
  3. Manage secrets securely Use .env files for local development and Docker Secrets or environment variables for production
  4. Use healthchecks So Docker automatically checks if your services are actually running
  5. Limit resources Especially important on smaller VPS – prevents one container from consuming all resources

From Local to Production: The Deployment Workflow

Here's what a typical workflow looks like:

  1. Develop locally
docker-compose up -d
# Make code changes, test
docker-compose restart webapp
  1. Deploy to server
# Push code to server (Git, rsync, scp)
ssh user@server
docker-compose pull
docker-compose up -d --build
  1. Monitor
    docker-compose logs -f
    docker-compose ps
    

Conclusion: Docker Compose for Selfhosting

Docker Compose transforms the chaos of individual containers into a well-organized system. It's the tool that takes you from "playing around with Docker" to "running real production environments."

The key advantages:

  • Everything in one place: The entire configuration is in one file
  • Isolated environments: You can run multiple projects in parallel on one server
  • Easy networking: Containers communicate via DNS names
  • Perfect for selfhosting: Ideal for VPS hosting on the provider of your choice

Whether you want to host a blog, a cloud storage solution, or a monitoring system, Docker Compose makes it simple, maintainable, and reproducible.

Next steps:

  • Get yourself an affordable VPS
  • Install Docker and Docker Compose
  • Start with a simple hobby project
  • Gradually expand your infrastructure

Want to learn more about deployment strategies and selfhosting? Check out our hosting solutions at lowcloud.io – optimized for Docker and modern container workflows.