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 crashesWhat 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 startupvolumes 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 imageUpdating 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.