Your First Docker Compose Stack: From Zero to a Running Service

A true beginner's guide to Docker Compose. Learn what images, containers, ports, and volumes actually are by deploying your first real service — the foundation every self-hosted app is built on.

On this page
  1. The four ideas, in one picture
  2. Build your first stack
  3. The five commands you’ll use forever
  4. Named volumes vs. bind mounts (the one nuance worth knowing)
  5. You just unlocked the whole catalogue

Almost every self-hosted app you’ll ever install — Immich, Homepage, Uptime Kuma, the entire media stack — is deployed the same way: with a short text file and one command. That file format is Docker Compose, and once it clicks, you can deploy virtually anything. This guide is the click.

We’ll skip the theory-heavy tour and just build something, explaining each new idea the moment it appears. By the end you’ll have a running service and — more importantly — you’ll understand the four concepts (images, containers, ports, volumes) that every future install reuses.

What you need first

A host with Docker installed. In a homelab that’s usually a Docker LXC or a VM on Proxmox. Everything below runs from that host’s command line. Confirm Docker is ready with docker –version and docker compose version.


The four ideas, in one picture

How a service actually runsImagethe templatenginx:latestContainerthe running instancePort 8080 → 80Volume /dataisolated · disposable · repeatablePorthow you reach it in a browserVolumewhere its data safely livesbuildahomelab.dev
  • Image — the read-only template for an app, downloaded from a registry (Docker Hub). Think recipe.
  • Container — a live, running copy made from an image. Think the finished dish. Disposable: delete it and start another anytime.
  • Port — a container is sealed off by default; publishing a port pokes one hole so your browser can reach the app.
  • Volume — storage that lives outside the container so your data survives when the container is replaced.

Hold those four in mind. The compose file is just a tidy way to declare them.


Build your first stack

1Make a folder for the stack

Each stack gets its own folder. The folder name becomes the project name, and the compose file lives inside it.

Create the project

mkdir -p ~/stacks/hello && cd ~/stacks/hello
2Write docker-compose.yml

Create a file named exactly docker-compose.yml. We’ll run nginx, a tiny, rock-solid web server, and serve a page from a folder on the host. Every line is labelled.

~/stacks/hello/docker-compose.yml

services:               # everything we want to run
web:                  # a name we pick for this service
  image: nginx:latest # the template to download and run
  container_name: hello-web
  ports:
    - "8080:80"       # host port 8080  ->  container port 80
  volumes:
    - ./site:/usr/share/nginx/html:ro   # serve our folder, read-only
  restart: unless-stopped               # start on boot, restart if it crashes
Reading the two mappings

Ports read host:container. “8080:80” means “browse to port 8080 on my machine, and Docker forwards it to port 80 inside the container.” Volumes read host:container too. ./site:/usr/share/nginx/html means “the site folder next to this file appears inside the container at nginx’s web root.” Change a file in ./site and the running site changes — the data lives on the host, not trapped in the container.

3Add a page to serve

Create the folder the volume points at, and drop in an index page.

Create the content

mkdir -p site
echo "<h1>It works. I run my own web server now.</h1>" > site/index.html
4Start it

One command builds and starts the whole stack in the background. The -d means “detached” — it runs without holding your terminal.

Launch the stack

docker compose up -d

Open http://YOUR-HOST-IP:8080 in a browser (replace YOUR-HOST-IP with the address of your Docker host — the machine you’ve been typing on). There’s your page — served by a container you defined in eight lines. You just deployed a service, and every future deploy will feel like this one.


The five commands you’ll use forever

These work in any stack folder, and they’re 90% of day-to-day Docker:

The essentials

docker compose up -d       # start (or apply changes to) the stack
docker compose ps          # what's running, and is it healthy?
docker compose logs -f      # live logs — your first stop when something's wrong
docker compose pull        # download newer images
docker compose down        # stop and remove the containers (volumes are kept)
Updating a service is two commands

This is the pattern for updating almost everything you self-host: docker compose pull to fetch the new image, then docker compose up -d to recreate the container with it. Because your data is in a volume, it carries straight over. That’s the whole update routine for Immich, Homepage, Uptime Kuma — all of them.


Named volumes vs. bind mounts (the one nuance worth knowing)

You just used a bind mount — a specific host folder (./site) mapped in. That’s perfect when you want to see and edit the files. The other kind is a named volume, which Docker manages for you:

A named volume instead of a host folder

services:
db:
  image: postgres:16
  volumes:
    - db-data:/var/lib/postgresql/data   # named volume, managed by Docker

volumes:
db-data:                                  # declared down here

Rule of thumb: use a bind mount for things you want to open and edit yourself (config files, a media library on your NAS share); use a named volume for opaque application data like databases, where you never touch the files directly. Both survive docker compose down. Both are how your data outlives the container.


You just unlocked the whole catalogue

That’s it — that’s the skill. Every self-hosted guide from here is a variation on the same four ideas and the same five commands. When you read an app’s install docs and see a docker-compose.yml, you now know exactly what each line does and how to run it.

Ready to use it for real? Point this skill at Immich for private photo backup, a Homepage dashboard, or Uptime Kuma monitoring — or head back to the full roadmap and keep building.


Related posts:

Sources: Docker Compose documentation, Docker documentation.

Comments

Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.