
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.
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.
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.
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.
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.
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.
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.
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:
.env to your .gitignoreWhy 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.
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
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.
restart: unless-stopped
This makes your containers start automatically after a server rebootdocker-compose.yml for development, docker-compose.prod.yml for production.env files for local development and Docker Secrets or environment variables for productionHere's what a typical workflow looks like:
docker-compose up -d
# Make code changes, test
docker-compose restart webapp
# Push code to server (Git, rsync, scp)
ssh user@server
docker-compose pull
docker-compose up -d --build
docker-compose logs -f
docker-compose ps
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:
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:
Want to learn more about deployment strategies and selfhosting? Check out our hosting solutions at lowcloud.io – optimized for Docker and modern container workflows.
Self-Host n8n on Hetzner: The Ultimate Guide to Free Automation
Learn how to self-host n8n on Hetzner with Docker. A complete step-by-step tutorial to save costs, ensure data sovereignty, and master workflow automation."
Self-Host Docmost with Docker Compose and Traefik: Complete Guide
Learn how to self-host Docmost on your own server using Docker Compose and Traefik as a reverse proxy. A step-by-step tutorial for GDPR-compliant documentation.