Do you really need Docker Compose for a single-service application?
To run a single container you usually add an image name, a port mapping, a few volume definitions and a few environment variables to the docker run command. The command line grows quickly, gets lost in the terminal history and becomes hard to carry over from one server to another as-is. When the server restarts, or when the setup is moved to another machine, you have to remember that long command correctly, bury it in a script, or type it out again by hand. Docker Compose removes that problem even for single-service setups: every parameter describing how the container should run is collected in a single docker-compose.yml file.
What is Docker Compose, and how is it different from docker run?
Docker Compose is a tool for defining and managing an application with a single YAML file; although it was designed for multiple containers, it offers the same advantages in single-service projects. Modern docker compose (v2, built into the Docker CLI as a plugin) now uses the Compose Specification and does not require the top-level version: field; that field is deprecated and is ignored even if it is present in the file. The old standalone docker-compose (the hyphenated, Python-based v1 release) is a legacy tool that is no longer maintained and should not be preferred for new installations.
Compared with docker run, Compose's main difference is that it is declarative rather than imperative: instead of writing the command step by step, you define in YAML what the final state of the container should be. This has three practical consequences. First, the file can be added to version control (Git); when a port or an environment variable changes, that change is clearly visible in the commit history. Second, the same file produces the same result when run on any server with Docker installed; that makes the setup repeatable. Third, the entire lifecycle of the service is managed from a single file with a few short commands; there is no need to run separate commands such as docker run, docker stop and docker rm by hand and in the right order.
The basic structure of docker-compose.yml
A Compose file defines one or more services under the services: key. A single-service setup usually contains the following fields: which image (image) the service is built from, the ports exposed to the outside, volume definitions for persistent data, environment variables and a restart policy. A typical example is below:
The image field specifies a ready-made image on Docker Hub or another registry; if you want to build an image from your own source code, build: . can be used instead of image. Every line under ports is in the form HOST:CONTAINER: to the left of the colon is the port on the host machine, to the right the port the application listens on inside the container. The restart field decides when the container is restarted automatically.
| Restart value | Behaviour |
|---|---|
| no | The default value. The container is never restarted automatically under any circumstances. |
| on-failure | The container is restarted if it stops with an error code (any code other than 0). |
| unless-stopped | Unless it has been stopped by hand, the container also starts automatically when the Docker service restarts (after a server reboot, for example). |
| always | The container is restarted every time the Docker service restarts, even if it was stopped by hand. |
Bind mount or named volume?
Volume definitions are written with a similar HOST:CONTAINER logic, but the two different types should not be confused. A definition such as ./data:/app/data is a bind mount: it mounts a relative directory on the host machine straight into the container, and the files are directly visible and editable on the host through the normal file system. A definition such as dbdata:/var/lib/postgresql/data, on the other hand, is a named volume; this is storage managed by Docker itself and not tied to a particular path on the host file system. If a named volume is used, that volume's name must also be declared separately in the top-level volumes: block of the file; otherwise Compose throws an error.
The basic workflow: up, logs, down
Once the file has been saved under the name docker-compose.yml, the whole lifecycle of the service is managed with a few commands in the same directory:
docker compose up -d— starts the service in the background (detached mode); the container keeps running even if the terminal window is closed.docker compose ps— lists the running state of the services belonging to this file.docker compose logs -f— keeps following the service's log output live (follow mode).docker compose down— stops the service and removes the containers and network it created; named volumes are not deleted by default.
Common mistakes
A single-service Compose file may look simple, but the mistakes below are made repeatedly and usually leave the service unexpectedly unreachable or gone after a server restart.
- Getting the port order wrong: the value under
portsis always inHOST:CONTAINERorder. When that order is reversed, Compose does not raise an error, but the expected address cannot be reached from the browser; because the port opened on the host is a different number. - Overwriting node_modules inside the container with a bind mount: mounting the whole project directory as a bind mount covers the dependency folder installed inside the container during the image build with the version from the host (sometimes missing or built for a different platform), and the application fails to start with a module not found error.
- Skipping the restart policy: if the field is left out entirely the default value is
no, meaning the container does not come back automatically after a server restart or a crash. For services that must run permanently,unless-stoppedoralwaysshould be stated explicitly. - Using the latest tag: writing only the image name without a version (which implicitly corresponds to the
latesttag) can mean a different image content is pulled on everypull. That removes the repeatability that is the whole point of a Compose file; a fixed version tag (for examplenode:20-alpine) should be used for the image.
Generating docker-compose.yml instead of writing it by hand
Writing all of the fields above by hand in the right order, with the right indentation and the right separators is error-prone, especially for people not used to YAML; one extra space or a misplaced hyphen can invalidate the file. Entering the service name, image, restart policy, port list, volume list and environment variables into a form and getting a valid docker-compose.yml straight away is a practical shortcut, particularly for single-service and repetitive setups.
Once you have saved the generated file into your project directory as docker-compose.yml, you can start it with docker compose up -d, follow the logs with docker compose logs -f and stop it when needed with docker compose down.