Docker Compose basics: run a small app and its database together
Running one container with docker run is easy. Running an app, its database and a cache, each with ports, volumes and settings, means a very long command per service, and nobody can remember them all six months later. Docker Compose solves that: you describe the whole setup in one file, and start or stop everything with a single command. For a VPS running a few small projects, it is the tidy way to work.
What Compose gives you
- One file that documents how your services fit together.
- One command to start, stop, update or rebuild everything.
- Repeatability: copy the file to another server and get the same result.
- Networking for free: services can reach each other by name.
It is installed as a plugin if you followed How to install Docker on Ubuntu the official way. Check it with docker compose version. Note the space: the modern command is docker compose, not docker-compose.
A first example
Create a folder for the project and a file called compose.yaml inside it:
services:
db:
image: mariadb:11
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MARIADB_DATABASE: appdb
MARIADB_USER: app
MARIADB_PASSWORD: ${DB_PASSWORD}
volumes:
- dbdata:/var/lib/mysql
app:
image: my-app:latest
restart: unless-stopped
depends_on:
- db
environment:
DB_HOST: db
DB_NAME: appdb
DB_USER: app
DB_PASSWORD: ${DB_PASSWORD}
ports:
- "127.0.0.1:3000:3000"
volumes:
dbdata:
Indent with spaces only, never tabs. See How to edit config files without breaking them for YAML pitfalls.
What the pieces mean
serviceslists each container. Their names, heredbandapp, are also their network names, so the app reaches the database at the host namedb.imageis what to run. Pin a version, such asmariadb:11, rather thanlatestfor databases, so an update does not surprise you.restart: unless-stoppedbrings it back after a crash or reboot.environmentpasses settings in. Values like${DB_PASSWORD}are read from a file named.envnext tocompose.yaml.volumeskeeps data outside the container. Without it, the database disappears when the container is recreated.portspublishes a port. The127.0.0.1:prefix keeps it private to the server, so only a reverse proxy on the same machine can reach it. That matters, because Docker bypasses the firewall for published ports. See Set up a firewall on your VPS with UFW.depends_onstartsdbbeforeapp. It waits for the container to start, not for the database to be ready, so your app should retry its connection.
The secrets file
Create .env in the same folder:
DB_ROOT_PASSWORD=a-long-random-password
DB_PASSWORD=another-long-random-password
Keep it out of version control, and restrict who can read it: chmod 600 .env. Never put real passwords inside compose.yaml if you commit that to a repository.
The commands you will use
docker compose up -d
docker compose ps
docker compose logs -f app
docker compose down
up -dcreates and starts everything in the background.psshows the status of the services.logs -f appfollows one service's output. Read it the same way as any log. See How to read your server logs.downstops and removes the containers. Named volumes stay, so your data survives. Add-vonly when you want to delete the data too, because that is irreversible.
Updating
To pull newer images and restart only what changed:
docker compose pull
docker compose up -d
Before updating a database image to a new major version, read its release notes, back up first and be ready to roll back. Database upgrades can change the on-disk format.
Backing up
The data lives in volumes or mounted folders. Back those up on a schedule, and test a restore. For databases, a dump is safer than copying live files. See How to back up your VPS with rsync and The 3-2-1 backup rule.
Putting it behind a web server
For a website, keep the container's port private, as in the example, and let nginx handle HTTPS and the public address. The configuration is similar to Run a Node.js app on your VPS with PM2 and nginx, pointing proxy_pass at the published local port.
Common problems
| Problem | Likely cause |
|---|---|
| "yaml: line X" error | A tab, or bad indentation |
| App cannot reach the database | Wrong host name. Use the service name, db, not localhost |
| Database empty after recreating | No volume was set |
| Variables show as blank | .env missing, or in a different folder |
| Port already in use | Another program uses that port |
| Disk fills up | Old images and logs. Run docker system prune carefully |
Compose is a small tool with a big payoff. A well-kept compose.yaml is also documentation, so the next person, or you in half a year, knows how everything runs.
Still stuck? Open a ticket on Discord - run /ticket in any channel, pick a department, and include your Server ID or Invoice ID so we can jump straight in.