Tutorials

Docker Compose for Home Server — From a Single YAML File to a Clean-Running Service

Docker Compose for Home Server — From a Single YAML File to a Clean-Running Service

Several years ago, when I first used a used laptop as a home server, I spent hours just making sure one web application could run. There is a docker run command that is almost as long as a toll road, ports that must be remembered one by one, and a volume that is written who knows where. It was like assembling a wardrobe without instructions: every bolt was there, but no one knew what order they were in.

It wasn't until I got to know Docker Compose that everything changed. One docker-compose.yml file can capture the entire service configuration, from images, ports, volumes, environment variables, to dependencies between containers. In this article, I want to share how I use Docker Compose on my everyday home server, from the most basic structure to the little quirks that make life easier.

Why Docker Compose, not Regular Docker?

Running a container with the docker run command is like cooking with a recipe that you only have in your head. If you forget one spice, the results will be different. If you want to share the recipe with another computer, you have to rewrite the command from scratch. Docker Compose converts the recipe into a default document: file docker-compose.yml.

Imagine you have a three-layer service: web application, database, and reverse proxy. With Docker, you have to run three different docker run commands, create a manual network, then pray everything can talk to each other. With Docker Compose, just one file, then one command: docker compose up -d. All containers are in one project, one virtual network, and one set of rules.

In addition, Docker Compose makes the update process neater. Want to change the image version? Change one line in the file, then run docker compose up -d again. Containers that are already running will be updated without you needing to memorize a sequence of commands. That's why I consider Docker Compose a mandatory foundation for anyone serious about building a home server.

Docker-compose.yml file structure that I use most often

Docker Compose files are written in YAML format. YAML is indent sensitive, so one wrong space can result in the entire file being rejected. I usually use two spaces for each level of indentation. Even though it seems trivial, this habit saves me from many mistakes in the middle of the night.

Here is the simplest template that I use in almost every project:

version: "3.8"

services:
  app:
    image: nginx:alpine
    container_name: app_web
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
    environment:
      - NGINX_HOST=app.lan
    networks:
      - app_net

networks:
  app_net:
    driver: bridge

There are several important parts here. The services section defines the container that will run. Each service has a name, image, and other configurations. container_name makes it easier for us to recognize containers when we see docker ps. restart: unless-stopped means the container will come back on after rebooting, unless we shut it down manually.

The ports section connects the host port with the container port. The format is HOST:CONTAINER. So 8080:80 means that traffic on port 8080 of the host computer will be forwarded to port 80 inside the container. The volumes section makes the data in the container persist even if the container is deleted. In the example above, the ./html folder on the host is mounted to /usr/share/nginx/html on the container in read-only mode.

Real Example: Running Kuma Uptime with One Command

As an exercise, I want to show you how to run Uptime Kuma, a monitoring tool I've discussed many times before. This tool is light, useful, and suitable for understanding how Docker Compose works in practice.

First, create a new folder and go into it:

mkdir -p ~/compose/uptime-kuma
cd ~/compose/uptime-kuma

Then create a docker-compose.yml file with the following contents:

version: "3.8"

services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    container_name: uptime-kuma
    restart: unless-stopped
    ports:
      - "3001:3001"
    volumes:
      - ./data:/app/data
    environment:
      - TZ=Asia/Jakarta

networks:
  default:
    name: uptime-kuma-net

After the file is saved, run:

docker compose up -d

The up command will read the file, download the image if it doesn't already exist, create a container, and run it. The -d option means detached, so our terminal is not trapped in the log container. A few seconds later, Kuma Uptime can be accessed at http://localhost:3001.

If at any time you want to stop the service, just run docker compose down. This command will stop and delete the container, but the data in the ./data folder remains safe because it is stored on the host. If you want to see the log, docker compose logs -f will display the log of all services in real-time.

Tips for Managing Volumes, Network, and Restart Policy

In my experience, three things most often frustrate beginners: data is lost when containers are deleted, containers can't communicate with each other, and services don't come back up after a reboot. Luckily, all three problems have simple solutions in Docker Compose.

Volume is the key so that data does not disappear. There are two common ways: bind mount and named volume. Bind mount mounts the host folder directly to the container, suitable for frequently edited configuration files. Named volumes are managed by Docker itself, neater for the database. Example named volume:

volumes:
  db_data:

services:
  db:
    image: postgres:15-alpine
    volumes:
      - db_data:/var/lib/postgresql/data

Network allows containers in one project to call each other by service name. So if you have a web service and a database service, from within the web container you can access the database with the hostname db. Docker Compose automatically creates a network bridge for each project, so we don't need to think about IP addresses.

Restart policy determines what happens when the container stops. I most often use unless-stopped for services that must always be on, such as monitoring or reverse proxy. Other options like always or on-failure are also available, but unless-stopped gives us manual control while debugging.

When Should You Update and Backup?

Updating images in Docker Compose is very easy, but that doesn't mean it has to be done every day. I have a habit: check for image updates once a week, then test them on non-critical services first. Method:

docker compose pull
docker compose up -d

The pull command downloads the latest image version according to the tag written in the file. After that, up -d will recreate the container with the new image. If a problem occurs, we can quickly restore the old tags in the file, then run up -d again.

For backups, the most important thing is the volume folder. I usually create archives from the project folder and its volumes. For example, for the Kuma Uptime project above:

tar -czvf uptime-kuma-backup-$(date +%F).tar.gz ~/compose/uptime-kuma

This backup includes docker-compose.yml files, configuration, and application data. Later, we just extract the archive and run docker compose up -d on another computer. This is one of the beauties of Docker Compose: our services become portable, like a neatly packed suitcase.

Conclusion

Docker Compose changed the way I look at home servers. From what was initially manual and fragile, it became more structured, easy to back up, and easier to move. One YAML file may look simple, but it is the blueprint for the entire service ecosystem that we build.

If you are just starting out, I recommend practicing the Kuma Uptime example above. From there, you can experiment adding databases, reverse proxies, or other self-hosted applications. The most important thing is not to memorize each command, but to understand the pattern: define service, mount volume, set network, then run.

If there is a part that is still confusing, don't hesitate to write in the comments column. I'm also still learning, and often the best mistakes come from trying new things in the middle of the night.