Docker Compose for Beginners: Self-Host Your First App
Learn Docker Compose step by step: install it, write your first compose.yaml, persist data with volumes, connect services and keep everything updated.
· 6 min read
Contents
- What you need
- Step 1: Install Docker and check Compose
- Step 2: Your first compose.yaml
- Step 3: Self-host a real app with persistent data
- Step 4: Connect multiple services
- The commands you will use every day
- Updating your apps
- Backing up
- Security basics before exposing anything
- Troubleshooting
- FAQ
- What is the difference between Docker and Docker Compose?
- compose.yaml or docker-compose.yml?
- Does a stopped container lose its data?
- Further reading
Docker Compose lets you describe an entire application — web server, database, background workers — in one readable file, and start it with one command. It is the easiest way to start self-hosting, and the same file works on your laptop, a $5 VPS or a Raspberry Pi.
By the end of this guide you will have a self-hosted uptime monitor running with persistent data, and you will understand every line of the file that runs it.
What you need
- A computer or server running Linux, macOS or Windows
- Docker Engine with the Compose plugin (installed together with Docker Desktop, or separately on Linux)
- Basic comfort with a terminal
Step 1: Install Docker and check Compose
On Windows or macOS, install Docker Desktop from the official site; Compose is included.
On Ubuntu or Debian, Docker provides a convenience script for test machines:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER # run docker without sudo (log out and back in)
For production servers, follow the repository-based install instead so updates come through your package manager.
Check that everything works:
docker --version
docker compose version
Note the command is docker compose (with a space). The older standalone docker-compose (with a hyphen) is deprecated.
Step 2: Your first compose.yaml
Create a folder and a file named compose.yaml:
mkdir hello-compose && cd hello-compose
services:
web:
image: nginx:alpine
ports:
- "8080:80"
Start it:
docker compose up -d
Open http://localhost:8080 and you should see the nginx welcome page. Here is what each line means:
| Line | Meaning |
|---|---|
services: | The list of containers that make up the app |
web: | A name you choose for this service |
image: nginx:alpine | The image to download and run |
ports: "8080:80" | Map port 8080 on your machine to port 80 in the container |
-d | Run in the background ("detached") |
Stop and remove it with docker compose down.
Step 3: Self-host a real app with persistent data
Now something useful: Uptime Kuma, an open-source monitor that checks whether your websites are up and alerts you when they are not.
mkdir ~/uptime-kuma && cd ~/uptime-kuma
services:
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
restart: unless-stopped
ports:
- "3001:3001"
volumes:
- ./data:/app/data
docker compose up -d
docker compose logs -f # watch it start; Ctrl+C to stop watching
Visit http://localhost:3001 (or http://your-server-ip:3001) and create an admin account.
Two new lines matter here:
volumes: ./data:/app/datastores the app's database in adatafolder next to your compose file. Containers are disposable; without a volume, every update or restart would wipe your monitors.restart: unless-stoppedbrings the container back after a crash or a server reboot, unless you stopped it yourself.
Step 4: Connect multiple services
Most apps need a database. Compose puts all services in the same file on a private network, where they can reach each other by service name.
services:
app:
image: ghcr.io/example/myapp:latest # replace with your app's image
restart: unless-stopped
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://app:${DB_PASSWORD}@db:5432/app
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: app
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
retries: 10
volumes:
db-data:
Things to notice:
- The app connects to the host
db— the service name — notlocalhost. - The database has no
ports:section, so it is reachable only by other containers, not from the internet. That is what you want. depends_onwithservice_healthywaits until PostgreSQL actually accepts connections, not just until its container starts.db-datais a named volume managed by Docker, an alternative to a folder path.${DB_PASSWORD}is read from a.envfile in the same folder.
Create that .env file and keep it out of version control:
echo "DB_PASSWORD=$(openssl rand -hex 24)" > .env
echo ".env" >> .gitignore
The commands you will use every day
docker compose up -d # start (or apply changes to) everything
docker compose ps # what is running
docker compose logs -f app # follow logs of one service
docker compose restart app # restart one service
docker compose exec db psql -U app # open a shell/command inside a container
docker compose down # stop and remove containers (volumes are kept)
docker compose down -v # also delete named volumes — this deletes your data
Updating your apps
docker compose pull # download newer images
docker compose up -d # recreate containers that have a new image
docker image prune -f # remove old, unused images
Pin images to a major version (like uptime-kuma:1 or postgres:16-alpine) rather than latest. You get bug fixes when you pull, without a surprise major upgrade that changes the data format.
Backing up
Your data lives in the volume folders. A simple backup for a bind-mounted folder:
docker compose stop
tar czf ~/backups/uptime-kuma-$(date +%F).tar.gz ./data
docker compose start
For databases, use the database's own dump tool so the backup is consistent:
docker compose exec -T db pg_dump -U app app > backup-$(date +%F).sql
Schedule it with cron — see Cron Expressions Explained — and copy backups to a different machine.
Security basics before exposing anything
- Do not expose databases. Leave out
ports:for services that only other containers use. - Put a reverse proxy in front (Caddy, Traefik or nginx) to get HTTPS instead of exposing raw ports.
- Use a firewall: on Ubuntu,
ufw allow 22,80,443/tcpthenufw enable. Be aware that ports published by Docker can bypass ufw rules, which is another reason to publish only what you need. - Keep secrets in
.env, never in a public repository.
Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
port is already allocated | Something else uses that host port | Change the left side, e.g. "8081:80" |
permission denied on the Docker socket | Your user is not in the docker group | sudo usermod -aG docker $USER, then log out and in |
| App cannot reach the database | Using localhost instead of the service name | Use db (the service name) as the host |
| Data disappears after an update | No volume configured | Add a volumes: mapping |
FAQ
What is the difference between Docker and Docker Compose?
Docker runs individual containers. Compose describes a group of containers, their networks and volumes in a file and manages them together.
compose.yaml or docker-compose.yml?
Both work. compose.yaml is the current preferred name; older projects use docker-compose.yml.
Does a stopped container lose its data?
Stopping does not delete anything. Removing a container (docker compose down) deletes everything inside it that is not stored in a volume.
Further reading
Keep reading
Self-Hosting on a Raspberry Pi: What Works and How to Set It Up
What a Raspberry Pi can realistically self-host, the hardware that makes it reliable, a step-by-step Docker setup and how to reach it securely from anywhere.
How to Back Up and Restore Docker Volumes the Right Way
Back up Docker named volumes and bind mounts safely, dump databases consistently, automate it with cron, copy backups off-site and test restores.
How to Secure a New Linux VPS: 10 Steps for the First Hour
A practical hardening checklist for a fresh Ubuntu or Debian server: a sudo user, SSH keys, firewall, automatic updates, fail2ban and backups.