Try describing what you need:

selfhostedworld.com·Guide·Checked October 2026

Docker and Compose for beginners

Almost every self-hosted app ships a Compose file. This guide shows how to install Docker the right way, read a Compose file line by line, and run the few commands you will use every day.

Why apps ship a Compose file

An app needs a runtime, libraries, config and often a database. Installing all of that by hand differs on every machine. Docker packs the app and its dependencies into an image. A container is an isolated process started from that image. Each container has everything it needs to function, so it does not depend on what else is on your server.

An image is the template. A container is one running copy of it. The container is disposable. Docker says that when a container is deleted, the changes made inside it are deleted too. To keep data, store it outside the container, in a volume or a bind mount. Then you can delete and recreate the container, for example to update the app, and the data stays. This one idea explains most of the file below.

A Compose file is a text file that describes the containers, ports, storage and settings of one app. You write it once and start everything with one command. The app's authors usually write it for you.

Install Docker Engine from Docker's repository

On Debian 13 or Ubuntu 24.04, do not install the docker.io package from your distribution. Docker calls it an unofficial package that conflicts with the official ones. Use Docker's own apt repository instead. Follow the official pages for Ubuntu or Debian.

In short, you do four things:

  1. Remove old conflicting packages such as docker.io and docker-compose.
  2. Add Docker's signing key and apt repository.
  3. Install docker-ce, docker-ce-cli, containerd.io, docker-buildx-plugin and docker-compose-plugin.
  4. Test with sudo docker run hello-world.

The last package, docker-compose-plugin, gives you the docker compose command (with a space). The old docker-compose (with a hyphen) is Compose v1, written in Python. Docker now lists only Compose v2 and later as supported. Every command in this guide uses the space. Check it works:

docker compose version

The docker group is root access

By default you need sudo for every Docker command. Docker's post-install page shows how to add your user to the docker group, but it carries a warning: the docker group grants root-level privileges to the user. Docker's security page adds that only trusted users should be allowed to control the daemon, because a container can mount host folders without limit.

Anyone in that group can read or change any file on the server. For a one-person server this is a fair trade. Add only your own admin user:

sudo usermod -aG docker $USER

Log out and back in for it to apply. If you would rather keep typing sudo, that is fine too.

Read a Compose file line by line

Uptime Kuma is a small monitor that checks whether your sites and services are up. Its project publishes this Compose file. It is a good first example:

services:
  uptime-kuma:
    image: louislam/uptime-kuma:2
    restart: unless-stopped
    volumes:
      - ./data:/app/data
    ports:
      - "3001:3001"
  • services: lists the containers. This app has one, named uptime-kuma.
  • image: names the image to download, in the form name:tag. The tag picks a version. Compose accepts a tag or a digest. Here :2 follows the 2.x line. For tighter control, pin an exact version from the project's releases. Avoid :latest, because it can jump to a new major version on the day you pull.
  • restart: unless-stopped brings the container back after a crash or a reboot. It stays down only if you stopped it yourself. Docker's restart policy page says a container you stopped "isn't restarted even after Docker daemon restarts".
  • volumes: connects storage. ./data:/app/data means "the data folder next to this file appears inside the container as /app/data". The app writes its database there. Back up that folder to save your data.
  • ports: publishes a port. See below.

Ports: host:container

The format is "host:container". In "3001:3001", the left number is the port on your server, and the right number is the port inside the container. The docs say to quote the value so YAML does not misread it. Change the left side if 3001 is taken, for example "3002:3001". Leave the right side alone.

Here is the part that catches beginners. With no address in front, Docker binds to all interfaces. Docker also warns that publishing a port is insecure by default and makes it reachable from the outside world. Worse, if you use ufw or firewalld, Docker says that published ports bypass your firewall rules, so a firewall rule that says "deny" does not protect them. Why not open ports explains the risk.

To keep an app local to the server, put 127.0.0.1 in front:

    ports:
      - "127.0.0.1:3001:3001"

Now only the Docker host can reach the port. Reach it through Tailscale, a tunnel or a reverse proxy running on the same machine. Choose your front door helps you pick. Use 127.0.0.1 as your default.

Settings and secrets: environment and .env

Many apps read settings from environment variables. Set them with environment::

    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}

That example comes from a database image, not Uptime Kuma, which needs none. The ${DB_PASSWORD} part is filled from a file named .env that sits next to your Compose file. Compose loads it by itself:

DB_PASSWORD=pick-a-long-random-value

This keeps secrets out of the Compose file, so you can share or commit the Compose file safely. Do not commit .env. A service can also use env_file: to pass a whole file of variables into the container. Values in environment: override env_file:.

Run it

Give each app its own folder, with its own Compose file, .env and data directory:

mkdir -p ~/apps/uptime-kuma/data
cd ~/apps/uptime-kuma
curl -o compose.yaml https://raw.githubusercontent.com/louislam/uptime-kuma/master/compose.yaml
sed -i 's/"3001:3001"/"127.0.0.1:3001:3001"/' compose.yaml
docker compose up -d

The sed line changes the port to the 127.0.0.1 form before the first start. The downloaded file publishes the port on every interface, and the first visitor to an empty Uptime Kuma picks the admin password. Run docker compose ps and check that the port reads 127.0.0.1:3001->3001/tcp.

One folder per app keeps apps apart, so you always know where an app's files are. Many apps, one server builds on this layout.

To finish setup from your own computer, open an SSH tunnel, which forwards a port on your computer to the server over SSH:

ssh -L 3001:127.0.0.1:3001 you@your-server

Leave it open and browse to http://localhost:3001. Before you share an app explains why setup comes before any public address.

The everyday commands

Run these inside the app's folder.

Command What it does
docker compose up -d Create and start the app in the background. Run it again after any change to the file.
docker compose ps Show which containers run and their ports.
docker compose logs -f Follow the app's log. Press Ctrl+C to stop watching.
docker compose pull Download newer images for the tags in your file.
docker compose down Stop and remove the containers and the network.

See the reference pages for up, ps, logs, pull and down.

To update an app, run docker compose pull, then docker compose up -d. Compose recreates the containers whose image or settings changed. Back up first. Update without breaking things covers the safe routine.

Careful: down -v deletes data

docker compose down is safe for your data, because the data lives outside the container. Adding -v changes that. Docker says -v removes named volumes declared in the Compose file and anonymous volumes. If the app keeps its data in a named volume, such as uptime-kuma:/app/data, that command erases the database with no prompt.

The ./data folder in the file above is a bind mount, which -v leaves alone. Do not rely on that. Treat -v as "delete my data" and never type it by habit. Where your data lives explains volumes, bind mounts and what to back up.

When it fails, look here

Start with the log. From the app's folder:

docker compose ps
docker compose logs --tail 50

The last lines usually name the problem. Three failures account for most first-day trouble:

  • Port already in use. The error says the port is already allocated or the address is in use. Another program, or another app of yours, owns that host port. Find it with sudo ss -ltnp, then change the left number in ports:, for example "3002:3001".
  • Permission denied on the socket. If docker says it cannot connect to the daemon socket, your user is not in the docker group yet. Add it as shown above and log in again, or use sudo.
  • Permission denied on the data folder. The app runs as a user that cannot write to the folder you mounted. With a bind mount, you manage the host folder yourself, including its owner and permissions. Check what the app expects in its docs, then see Where your data lives.

Next step

Set up a fresh server with your first hour on a new server, then install Docker as above. Next, read Where your data lives before you run an app you care about.

selfhostedworld.com, tracking self-hosted open-source software