Docker Compose, explained

You ran an app from a file you didn't fully understand. Let's fix that. Once you can read a compose file, you can install almost anything, because nearly every self-hosted project hands you one of these and says "run this."

A compose file is just a plain-text description of how to run an app: which image, which ports, where any data goes, what settings it needs. Here's the exact file from last lesson, annotated.

services:
  excalidraw:                            # a name you choose for this app
    image: excalidraw/excalidraw:latest  # which image to run
    container_name: excalidraw           # what the running container is called
    ports:
      - "3030:80"                        # your-side : app-side
    restart: unless-stopped              # start on boot, restart if it crashes
A compose file tells Docker the image, port, data folder, settings, and restart rule. compose.yml image: fireflyiii/core ports: 8080:8080 volumes: ./data:/var environment: ... restart: unless-stopped what runs the app package how you open it browser address where data lives keep this folder come back up after reboot
A compose file tells Docker the image, port, data folder, settings, and restart rule.

What each line is doing

image is the app you're running. See that :latest on the end? It means "whatever's newest". That's fine for something simple like Excalidraw, but on bigger apps "newest" can change under you and break things, so many people pin a version instead, like image: someapp/thing:2, to keep updates on their own terms. Good habit to know about now, worth doing later.

ports is the line people trip on, and yours is a great example because the two numbers differ. It reads your-side:app-side. The right number, 80, is the port the app uses inside its container. The left, 3030, is the port you open on the server. So you visit :3030 and Docker quietly hands it to the app's 80. When two apps both want the same inside port, you just change the left number and they stop fighting. That's also the fix when Docker refuses to start something with a port is already allocated error: pick a different left number and go again.

restart: unless-stopped means the app comes back on its own after a reboot or a crash, unless you deliberately stopped it. You want this on basically everything, so your apps quietly return after a power cut.

The two lines you'll meet on almost everything else

Excalidraw was simple enough to skip them, but most apps add a volumes and an environment section, so they'll look like this tucked under the service:

    volumes:
      - ./data:/app/data       # your folder : the app's data folder
    environment:
      - TZ=Europe/London       # a setting the app reads on startup

volumes reads just like ports, your-folder:app-folder. It connects a real folder on your server to the folder inside the container where the app writes its data, which is what keeps that data alive when the container is replaced. It matters enough that it gets its own lesson next. environment is just a list of settings, a timezone, a password, a database connection, that the app reads when it starts. When an install guide says "set this value", that's where it goes.

The five commands you'll actually use

Run these from inside the app's folder, next to its compose file:

docker compose up -d      # start (or apply changes), in the background
docker compose ps         # is it running?
docker compose logs -f    # watch its output; Ctrl+C to stop watching
docker compose down       # stop and remove the container
docker compose pull       # download a newer image

Updating an app is just two of those together: docker compose pull to grab the newer image, then docker compose up -d to restart it on the new version. Docker replaces the container, and for apps with a volume, the data folder is untouched, so nothing is lost.

That's genuinely enough Compose to run a whole server's worth of apps. The one line worth understanding properly is that data folder, so let's look at where your data actually lives.