Docker Series - Docker Compose

Docker Series - Docker Compose
In: Docker
Table of Contents

Welcome back to the Docker series. In this post, we’re going to look at Docker Compose - a tool that makes managing multi-container applications much easier.

In the previous posts, we ran individual containers using plain Docker commands. This works fine for simple cases, but as soon as you have multiple services, volumes, networks, and restart policies, managing everything through individual commands becomes painful.

This is exactly where Docker Compose helps. Docker Compose lets you define your entire application stack in a single YAML file, describing services, networks, volumes, and how everything connects. Once defined, you can bring up the whole stack with a single command.

Docker Series - Hello Docker
As always, my goal here is to explain what Docker is using plain language and relatable examples, I hope to give you a clear understanding of what Docker is.

Revisiting Memos

Previously, we installed the memos app using plain Docker commands, as shown below. This works well and does the job, but you can also add this to a compose file. The beauty is that you can also define other services/containers within the same file.

docker run -d \
  --init \
  --name memos \
  --publish 5230:5230 \
  --volume ~/.memos/:/var/opt/memos \
  neosmemo/memos:stable

With Docker Compose, we can define the same thing in a file called compose.yml

services:
  memos:
    image: neosmemo/memos:stable
    container_name: memos
    volumes:
      - memos-data:/var/opt/memos
    ports:
      - 5230:5230
    restart: unless-stopped

volumes:
  memos-data:

Please note that here, I made a small change. Instead of using a bind mount (a directory directly from the host, denoted via ~/.memos/), we are using a Docker-managed volume called memos-data. This keeps the data isolated from the host filesystem and managed entirely by Docker. We could have used a bind mount, but I thought it would be easier for you to follow along this way. With bind mounts, you need to make sure your directory structure matches mine exactly. We’ll cover volumes in more detail in future posts.

To bring up the memos app using Docker Compose, you simply run.

docker compose up -d

This tells Docker Compose to start all services defined in the file. The -d option means detached mode, which runs the containers in the background so your terminal is free for other commands.

Similarly, you can stop the containers by running docker compose down as shown below.

💡
The default path for a Compose file is compose.yaml (preferred) or compose.yml that is placed in the working directory. Compose also supports docker-compose.yaml and docker-compose.yml for backwards compatibility of earlier versions. If both files exist, Compose prefers the canonical compose.yaml

Running Multiple Services

With Docker Compose, you can define multiple services in the same file. For example, if you want to run both memos and UniFi Network Application, your docker-compose.yml could look like this. I've explained how to run UniFi Network Application Docker in one of my previous posts, so feel free to check it out here.

Running Unifi Network Application in Docker
Users are now advised to switch to the linuxserver/unifi-network-application image, which is actively maintained and provides the latest version of the UniFi Network Application.
services:
  memos:
    image: neosmemo/memos:stable
    container_name: memos
    volumes:
      - memos-data:/var/opt/memos
    ports:
      - 5230:5230
    restart: unless-stopped

  unifi-db:
    image: mongo:7.0
    container_name: unifi-db
    environment:
      - MONGO_INITDB_ROOT_USERNAME=root
      - MONGO_INITDB_ROOT_PASSWORD=PASSWORD-DONT-SHARE
      - MONGO_USER=unifi
      - MONGO_PASS=PASSWORD-DONT-SHARE
      - MONGO_DBNAME=unifi
      - MONGO_AUTHSOURCE=admin
    volumes:
      - unifi-mongo-data:/data/db
      - ./init-mongo.sh:/docker-entrypoint-initdb.d/init-mongo.sh:ro
    restart: unless-stopped

  unifi-network-application:
    image: lscr.io/linuxserver/unifi-network-application:9.0.108
    container_name: unifi-network-application
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Etc/UTC
      - MONGO_USER=unifi
      - MONGO_PASS=PASSWORD-DONT-SHARE
      - MONGO_HOST=unifi-db
      - MONGO_PORT=27017
      - MONGO_DBNAME=unifi
      - MONGO_AUTHSOURCE=admin
      - MEM_LIMIT=1024
      - MEM_STARTUP=1024
    volumes:
      - unifi-app-data:/config
    ports:
      - 8443:8443
      - 3478:3478/udp
      - 10001:10001/udp
      - 8080:8080
      - 1900:1900/udp
      - 8843:8843
      - 8880:8880
      - 6789:6789
      - 5514:5514/udp
    restart: unless-stopped

volumes:
  memos-data:
  unifi-mongo-data:
  unifi-app-data:
💡
For UniFi to work correctly, you also need to have a file called init-mongo.sh in the same directory as the docker-compose.yml. This script is executed when the MongoDB container starts and is responsible for creating the initial database user and setting up permissions.
#!/bin/bash

if which mongosh > /dev/null 2>&1; then
  mongo_init_bin='mongosh'
else
  mongo_init_bin='mongo'
fi
"${mongo_init_bin}" <<EOF
use ${MONGO_AUTHSOURCE}
db.auth("${MONGO_INITDB_ROOT_USERNAME}", "${MONGO_INITDB_ROOT_PASSWORD}")
db.createUser({
  user: "${MONGO_USER}",
  pwd: "${MONGO_PASS}",
  roles: [
    { db: "${MONGO_DBNAME}", role: "dbOwner" },
    { db: "${MONGO_DBNAME}_stat", role: "dbOwner" }
  ]
})
EOF

The UniFi application requires a database instance, so using Docker Compose, we can define both the UniFi application and its required MongoDB database in the same file. This makes deployment much easier because everything needed is described in one place.

Running Unifi Network Application in Docker
Users are now advised to switch to the linuxserver/unifi-network-application image, which is actively maintained and provides the latest version of the UniFi Network Application.

In the compose file, you might have noticed the environment section. This is where we pass environment variables into the containers. These variables configure things like database credentials, time zones, memory limits, and more. This is a common way to pass configuration to containers without hardcoding them into the image.

For UniFi, you’ll also see PUID and PGID. These define the user and group IDs that the container should run as. On most Linux systems, the default user ID for a regular user is 1000, and the group ID is also 1000. This ensures the container can read and write to the mounted volumes without permission issues.

With this single file, you can bring up both services and their dependencies by running up and stopping everything by running down

docker compose up -d
docker compose down

As you can see, both applications are now available and reachable as we expected.

If you want to restart all services, you can use restart

docker compose restart

Or, if you only want to start or stop one service (like memos)

docker compose stop memos
docker compose start memos

This makes managing multi-container applications so much easier than running individual commands for each container.

Closing Up

Most of the self-hosted apps I run are defined in a single YAML file. So, if I ever need to deploy all my apps to a different host or if I want to share my setup with friends, I can simply give them the file. With a single command, they can bring everything up just like I have it.

There’s much more to Docker Compose than what we’ve seen so far, and there are many useful tweaks you can apply to fine-tune your setup. We’ll explore those in future posts.

Written by
Suresh Vina
Tech enthusiast sharing Networking, Cloud & Automation insights. Join me in a welcoming space to learn & grow with simplicity and practicality.
Comments
More from Packetswitch
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Packetswitch.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.