Docker Compose for Self-Hosted Apps: A Beginner-Friendly Guide

Docker Compose is the point where Docker usually stops feeling like a pile of one-off commands and starts feeling manageable. If you want to self-host more than one app, or even just keep one app organized properly, Compose gives you a repeatable way to describe the stack and bring it back exactly the same way later.

In this guide, you will learn what Docker Compose actually does, how to organize a self-hosted app folder, how to write a clean compose.yaml, how to start and inspect services, and how to update or remove a stack without panicking and clicking random things in your terminal.

What Docker Compose is in plain English

Docker Compose lets you define one or more containers in a YAML file and manage them as a group.

Instead of remembering a long docker run command every time, you keep the important settings in a file:

  • image name
  • ports
  • volumes
  • environment variables
  • restart policy
  • networks

Then you use commands like docker compose up -d or docker compose ps to manage the whole stack.

That matters for self-hosting because most real apps are not just one disposable container. They usually need persistent storage, a few settings, and a sane way to stop, start, and update them later.

Docker Compose vs the old docker-compose command

If you have been reading older tutorials, you may still see docker-compose with a hyphen.

For new installs, Docker's current docs use the Compose plugin form:

docker compose

That is the version this guide uses.

If docker compose version works on your server, you are on the right path.

Why Compose is such a big deal for self-hosting

Compose helps beginners and experienced admins for the same basic reason: it moves container settings out of your memory and into a file.

That gives you a few practical wins:

  • you can rebuild the stack later without guessing the original command
  • you can see ports, volumes, and variables in one place
  • you can back up the app folder with the configuration alongside it
  • you can stop and start the whole stack with predictable commands
  • you are less likely to create mystery containers you forget about three weeks later

This is one of the reasons so many self-hosted app projects publish a Compose example first.

What you need before you start

Before following along, make sure you already have:

  • a Linux server or VPS
  • Docker installed and working
  • a user account that can run Docker commands
  • basic SSH access to the machine

If Docker is not installed yet, start here first:

What a good self-hosted app folder looks like

A simple folder layout makes future updates and troubleshooting much less annoying.

For one app, a clean starting point looks like this:

/opt/uptime-kuma/
├── compose.yaml
├── .env
└── data/

That pattern works well because:

  • compose.yaml defines the stack
  • .env stores reusable variables
  • data/ holds persistent app files when you use bind mounts

Some apps use named Docker volumes instead of a local data/ folder. That is fine too. The important part is that you know where your real data lives before you trust the app with anything important.

Step 1: Confirm the Compose plugin is available

First, make sure the modern Docker Compose plugin works on your machine.

docker compose version

If Docker returns a version string, you are ready to continue.

If it says the command does not exist, go back to your Docker install and make sure the Compose plugin was installed.

Step 2: Create a folder for a sample app

To keep things practical, this guide uses a very small example stack based on the nginxdemos/hello container. It is lightweight, safe for a first Compose walkthrough, and easy to test in a browser.

Create the app folder.

sudo mkdir -p /opt/compose-demo

Now move into it.

cd /opt/compose-demo

If you plan to manage app folders as a non-root user, adjust ownership after creating the directory.

sudo chown "$USER":"$USER" /opt/compose-demo

Step 3: Create a .env file for reusable values

A .env file helps you avoid hardcoding everything directly in compose.yaml. It also makes simple changes less annoying later.

Create the file.

cat > .env <<'EOF'
APP_PORT=8080
TZ=Etc/UTC
EOF

This example is intentionally small. Real app stacks often store things like:

  • published ports
  • timezone
  • image tags
  • usernames
  • generated secrets
  • domain names

Important: A plain .env file is convenient, but it is not magical secret protection. Treat passwords and API keys carefully, especially if you sync app folders into Git or backups without thinking about it first.

Step 4: Create the compose.yaml file

Now create the actual Compose file.

cat > compose.yaml <<'EOF'
services:
  hello:
    image: nginxdemos/hello:plain-text
    container_name: compose-demo
    restart: unless-stopped
    ports:
      - "${APP_PORT}:80"
    environment:
      TZ: ${TZ}
EOF

This stack is small, but it already shows the most important Compose habits.

What each part does

services:

This is where you define the containers in the stack. Here I only have one service called hello.

image:

This tells Docker which image to pull and run. In this case, it uses a simple demo web server.

container_name:

This gives the running container an easy-to-recognize name. It is optional, but it helps beginners when reading logs or docker ps output.

restart: unless-stopped

This is a very common self-hosting setting. It tells Docker to restart the container automatically unless you intentionally stopped it.

ports:

This publishes container port 80 on the host port stored in .env. Because I set APP_PORT=8080, the site will be reachable on port 8080.

environment:

This passes environment variables into the container. Many real apps use this section for database settings, admin credentials, domain names, email config, or timezone values.

Step 5: Validate the Compose file before starting

Before you launch the stack, ask Docker to render the final config.

docker compose config

This is one of the most useful Compose commands for beginners. It helps you catch:

  • YAML formatting mistakes
  • missing variables
  • invalid interpolation from .env

If this command prints a clean combined config, your file is probably in good shape.

Step 6: Start the stack

Now bring the stack up in detached mode.

docker compose up -d

Docker will pull the image if needed, create the container, create the default network for the project, and start the service.

Detached mode means the container keeps running in the background after the command finishes. That is what you usually want for self-hosted apps.

Step 7: Check whether the service is running

List the services in the current stack.

docker compose ps

You should see the hello service running and port 8080 mapped.

For an app that exposes a web interface, this is one of the quickest sanity checks after startup.

Step 8: Test the app in a browser or with curl

If you are on the server itself, test the app with curl.

curl http://localhost:8080

If you are testing from your own computer and the VPS firewall allows it, open:

http://YOUR_SERVER_IP:8080

You should get a simple response from the demo container. If that works, your Compose stack is alive and reachable.

Step 9: Read logs when something looks wrong

If a container starts and then exits, logs are usually the first place to look.

docker compose logs

If you only want one service, specify it directly.

docker compose logs hello

For chatty apps, add -f to follow logs live.

docker compose logs -f hello

This is much more useful than guessing. A lot of beginner container problems are actually obvious once you read the logs for thirty seconds.

Step 10: Stop and start the stack cleanly

To stop the services without deleting them, use:

docker compose stop

To start them again later, use:

docker compose start

This is useful when you want a temporary pause without rebuilding anything.

Step 11: Remove the stack when you are done

To remove the containers and the default network created by the project, run:

docker compose down

That removes the running stack, but it does not automatically delete named volumes or bind-mounted app data.

That is a good thing. It means down is usually safer than beginners expect.

The most important Compose habits for real self-hosted apps

Once you move past toy examples, these are the habits worth keeping.

Keep one app per folder

Do not throw unrelated Compose files into one giant random directory. A dedicated folder per app makes backups, restores, and troubleshooting much easier.

Use .env for the settings that actually change

Good candidates include:

  • published ports
  • hostnames
  • image tags
  • timezone
  • secret values you generate once

Not every stack needs .env, but it becomes useful quickly when you want cleaner files or lighter edits.

Know whether your app uses bind mounts or named volumes

This matters for backups.

A bind mount might look like this:

volumes:
  - ./data:/config

A named volume might look like this:

volumes:
  - app-data:/config

Both can be valid. The important question is: if the VPS died tonight, would you know what to restore?

Do not edit production stacks carelessly

A small YAML typo can break a stack on restart. When you change a real app:

  1. review the compose file carefully
  2. run docker compose config
  3. take a backup if the app matters
  4. then apply the change

That order is much less exciting than breaking your database, which is exactly why it is the better order.

How to update a typical Compose-based app

For many self-hosted apps, the routine update flow looks like this.

Pull newer images.

docker compose pull

Recreate the containers with the new image.

docker compose up -d

Confirm the services are healthy.

docker compose ps

If you want to clean up unused old image layers afterward, you can do that separately.

docker image prune

Read the app's official release notes before updating anything important. Some apps need extra upgrade steps, schema migrations, or version-specific warnings.

Common problems and quick fixes

docker compose says it cannot find a configuration file

You are probably in the wrong directory.

Check where you are.

pwd

Then make sure compose.yaml or docker-compose.yml exists in that folder.

find . -maxdepth 1 \( -name 'compose.yaml' -o -name 'docker-compose.yml' \)

A port is already in use

That usually means another service is already listening on the host port you picked.

Change the host-side port in .env, then apply the stack again. For example, switch from 8080 to 8081 if something else already owns 8080.

Variables are not being substituted the way you expected

Run this first:

docker compose config

If the rendered output is wrong, check your .env file for typos, missing values, or quotation mistakes.

docker compose down removed the app but not the data

That is normal for persistent data. Compose removes the containers and network, but it does not automatically wipe named volumes or bind-mounted folders unless you explicitly tell it to.

This is usually a safety feature, not a bug.

Where to go next

Once you are comfortable with Compose, the next useful tutorials are:

Docker's official docs are also worth bookmarking:

Final thoughts

Docker Compose is not flashy, but it is one of the most useful skills you can learn early in self-hosting. Once you understand how a good Compose file is structured, later tutorials make a lot more sense because you are reading a configuration pattern instead of memorizing disconnected shell commands.

That is the real win: fewer mystery commands, cleaner app folders, and a much better chance that future-you will still understand what present-you built.

Frequently Asked Questions

Is Docker Compose different from docker-compose?
On current Docker installs, Compose is usually the integrated plugin you run as `docker compose` with a space. Older guides may still show `docker-compose` with a hyphen, but the modern plugin form is the better default for new setups.
Can Docker Compose manage a single container?
Yes. Compose is still useful for single-container apps because it keeps ports, volumes, restart policy, and environment variables in one reusable file.
Will docker compose down delete my app data?
Not by itself if your important data is stored in named volumes or bind-mounted folders. It removes the containers and network, but the persistent data stays unless you explicitly remove volumes too.

Related articles

🐳
Docker & Containers
How to Install Docker on Ubuntu and Run Your First Container
Install Docker Engine on Ubuntu using Docker's official apt repository, verify that it works, and optionally set it up so you can run Docker commands without sudo.
📊
Monitoring
How to Install Uptime Kuma with Docker Compose
Install Uptime Kuma with Docker Compose on a Linux server, create your first admin account, and start monitoring websites or services from a simple web dashboard.
🎬
Media Servers
How to Install Immich with Docker Compose
Install Immich with Docker Compose on a Linux server, set up storage paths safely, start the stack, and finish the first web-based admin setup.
☁️
File & Cloud Storage
How to Install Nextcloud with Docker Compose
Install Nextcloud with Docker Compose using MariaDB and Redis, keep the data persistent, and finish the first web setup without turning the stack into a science project.