阅读视图

发现新文章,点击刷新页面。

Docker Compose: Define and Run Multi-Container Apps

Docker Compose is a tool for defining and running multi-container applications. Instead of starting each container separately with docker run , you describe all of your services, networks, and volumes in a single YAML file and bring the entire environment up with one command.

This guide explains how Docker Compose V2 works, walks through a practical example with a SvelteKit development server and PostgreSQL database, and covers the most commonly used Compose directives and commands.

Quick Reference

For a printable quick reference, see the Docker cheatsheet .

Command Description
docker compose up Start all services (foreground)
docker compose up -d Start all services in detached mode
docker compose down Stop and remove containers and networks
docker compose down -v Also remove named volumes
docker compose ps List running services
docker compose logs SERVICE View logs for a service
docker compose logs -f SERVICE Follow logs in real time
docker compose exec SERVICE sh Open a shell in a running container
docker compose stop Stop containers without removing them
docker compose build Build or rebuild images
docker compose pull Pull the latest images

Prerequisites

The examples in this guide require Docker with the Compose plugin installed. Docker Desktop includes Compose by default. On Linux, install the docker-compose-plugin package alongside Docker Engine.

To verify Compose is available, run:

Terminal
docker compose version
output
Docker Compose version v2.x.x

Compose V2 runs as docker compose (with a space), not docker-compose. The old V1 binary is end-of-life and not covered in this guide.

The Compose File

Modern Docker Compose prefers a file named compose.yaml, though docker-compose.yml is still supported for compatibility. In this guide, we will use docker-compose.yml because many readers still recognize that name first.

A Compose file describes your application’s environment using three top-level keys:

  • services — defines each container: its image, ports, volumes, environment variables, and dependencies
  • volumes — declares named volumes that persist data across container restarts
  • networks — defines custom networks for service communication (Compose creates a default network automatically)

Compose V2 does not require a version: field at the top of the file. If you see version: '3' in older guides, that is legacy syntax kept for backward compatibility, not something new Compose files need.

YAML uses indentation to define structure. Use spaces, not tabs.

Setting Up the Example

In this example, we will run a SvelteKit development server alongside a PostgreSQL 16 database using Docker Compose.

Start by creating a new SvelteKit project. The npm create command launches an interactive wizard — select “Skeleton project” when prompted, then choose your preferred options for TypeScript and linting:

Terminal
npm create svelte@latest myapp
cd myapp

If you already have a Node.js project, skip this step and use your project directory instead.

Next, create a docker-compose.yml file in the project root:

docker-compose.ymlyaml
services:
 app:
 image: node:20-alpine
 working_dir: /app
 volumes:
 - .:/app
 - node_modules:/app/node_modules
 ports:
 - "5173:5173"
 environment:
 DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/myapp
 depends_on:
 db:
 condition: service_healthy
 command: sh -c "npm install && npm run dev -- --host"

 db:
 image: postgres:16-alpine
 volumes:
 - postgres_data:/var/lib/postgresql/data
 environment:
 POSTGRES_USER: postgres
 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
 POSTGRES_DB: myapp
 healthcheck:
 test: ["CMD-SHELL", "pg_isready -U postgres"]
 interval: 5s
 timeout: 5s
 retries: 5

volumes:
 postgres_data:
 node_modules:

Compose V2 automatically reads a .env file in the project directory and substitutes ${VAR} references in the compose file. Create a .env file to define the database password:

.envtxt
POSTGRES_PASSWORD=changeme
Warning
Do not commit .env to version control. Add it to your .gitignore file to keep credentials out of your repository.

The password above is for local development only. We will walk through each directive in the compose file in the next section.

Understanding the Compose File

Let us go through each directive in the compose file.

services

The services key is the core of any compose file. Each entry under services defines one container. The key name (app, db) becomes the service name, and Compose also uses it as the hostname for inter-service communication — so the app container can reach the database at db:5432.

image

The image directive tells Compose which Docker image to pull. Both node:20-alpine and postgres:16-alpine are pulled from Docker Hub if they are not already present on your machine. The alpine variant uses Alpine Linux as a base, which keeps image sizes small.

working_dir

The working_dir directive sets the working directory inside the container. All commands run in /app, which is where we mount the project files.

volumes

The app service uses two volume entries:

txt
- .:/app
- node_modules:/app/node_modules

The first entry is a bind mount. It maps the current directory on your host (.) to /app inside the container. Any file you edit on your host is immediately reflected inside the container, which is what enables hot-reload during development.

The second entry is a named volume. Without it, the bind mount would overwrite /app/node_modules with whatever is in your host directory — which may be empty or incompatible. The node_modules named volume tells Docker to keep a separate copy of node_modules inside the container so the bind mount does not interfere with it.

The db service uses a named volume for its data directory:

txt
- postgres_data:/var/lib/postgresql/data

This ensures that your database data persists across container restarts. When you run docker compose down, the postgres_data volume is kept. Only docker compose down -v removes it.

Named volumes must be declared at the top level under volumes:. Both postgres_data and node_modules are listed there with no additional configuration, which tells Docker to manage them using its default storage driver.

ports

The ports directive maps ports between the host machine and the container in "HOST:CONTAINER" format. The entry "5173:5173" exposes the Vite development server so you can open http://localhost:5173 in your browser.

environment

The environment directive sets environment variables inside the container. The app service receives the DATABASE_URL connection string, which a SvelteKit application can read to connect to the database.

Notice the ${POSTGRES_PASSWORD} syntax. Compose reads this value from the .env file and substitutes it before starting the container. This keeps credentials out of the compose file itself.

depends_on

By default, depends_on only controls the order in which containers start — it does not wait for a service to be ready. Compose V2 supports a long-form syntax with a condition key that changes this behavior:

yaml
depends_on:
 db:
 condition: service_healthy

With condition: service_healthy, Compose waits until the db service passes its healthcheck before starting app. This prevents the Node.js process from trying to connect to PostgreSQL before the database is accepting connections.

command

The command directive overrides the default command defined in the image. Here it runs npm install to install dependencies, then starts the Vite dev server with --host to bind to 0.0.0.0 inside the container (required for Docker to forward the port to your host):

txt
sh -c "npm install && npm run dev -- --host"

Running npm install on every container start is slow but convenient for development — you do not need to install dependencies manually before running docker compose up. For production, use a multi-stage Dockerfile to pre-install dependencies and build the application.

healthcheck

The healthcheck directive defines a command Compose runs inside the container to determine whether the service is ready. The pg_isready utility checks whether PostgreSQL is accepting connections:

yaml
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5

Compose runs this check every 5 seconds. Once the service passes its first check, it is marked as healthy and any services using condition: service_healthy are allowed to start. If the check fails 5 consecutive times, the service is marked as unhealthy.

Managing the Application

Run all commands from the project directory — the directory where your docker-compose.yml is located.

Starting Services

To start all services and stream their logs to your terminal, run:

Terminal
docker compose up

Compose pulls any missing images, creates the network, starts the db container, waits for it to pass its healthcheck, then starts the app container. Press Ctrl+C to stop.

To start in detached mode and run the services in the background:

Terminal
docker compose up -d

Viewing Status and Logs

To list running services and their current state:

Terminal
docker compose ps
output
NAME IMAGE COMMAND SERVICE STATUS PORTS
myapp-app-1 node:20-alpine "docker-entrypoint.s…" app Up 2 minutes 0.0.0.0:5173->5173/tcp
myapp-db-1 postgres:16-alpine "docker-entrypoint.s…" db Up 2 minutes 5432/tcp

To view the logs for a specific service:

Terminal
docker compose logs app

To follow the logs in real time (like tail -f):

Terminal
docker compose logs -f app

Press Ctrl+C to stop following.

Running Commands Inside a Container

To open an interactive shell inside the running app container:

Terminal
docker compose exec app sh

This is useful for running one-off commands, inspecting the file system, or debugging. Type exit to leave the shell.

Stopping and Removing Services

To stop running containers without removing them — preserving named volumes and the network:

Terminal
docker compose stop

To start them again after stopping:

Terminal
docker compose start

To stop containers and remove them along with the network:

Terminal
docker compose down

Named volumes (postgres_data, node_modules) are preserved. Your database data is safe.

To also remove all named volumes — this deletes your database data:

Terminal
docker compose down -v

Use this when you want a completely clean environment.

Common Directives Reference

The following directives are not used in the example above but are commonly needed in real projects.

restart

The restart directive controls what Compose does when a container exits:

yaml
services:
 app:
 restart: unless-stopped

The available policies are:

  • no — do not restart (default)
  • always — always restart, including on system reboot
  • unless-stopped — restart unless the container was explicitly stopped
  • on-failure — restart only when the container exits with a non-zero status

Use unless-stopped for long-running services in single-host deployments.

build

Instead of pulling a pre-built image, build tells Compose to build the image from a local Dockerfile :

yaml
services:
 app:
 build:
 context: .
 dockerfile: Dockerfile

context is the directory Compose sends to the Docker daemon as the build context. dockerfile specifies the Dockerfile path relative to context. If both the image and a build context exist, build takes precedence.

env_file

The env_file directive injects environment variables into a container from a file at runtime:

yaml
services:
 app:
 env_file:
 - .env.local

This is different from Compose’s native .env substitution. The .env file at the project root is read by Compose itself to substitute ${VAR} references in the compose file. The env_file directive injects variables directly into the container’s environment. Both can be used together.

networks

Compose creates a default bridge network connecting all services automatically. You can define custom networks to isolate groups of services or control how containers communicate:

yaml
services:
 app:
 networks:
 - frontend
 - backend
 db:
 networks:
 - backend

networks:
 frontend:
 backend:

A service only communicates with other services on the same network. In this example, app can reach db because both share the backend network. A service attached only to frontend — not backend — has no route to db.

profiles

Profiles let you define optional services that only start when explicitly requested. This is useful for development tools, debuggers, or admin interfaces that you do not want running all the time:

yaml
services:
 app:
 image: node:20-alpine

 adminer:
 image: adminer
 profiles:
 - tools
 ports:
 - "8080:8080"

Running docker compose up starts only app. To also start adminer, pass the profile flag:

Terminal
docker compose --profile tools up

Troubleshooting

Port is already in use
Another process on your host is using the same port. Change the host-side port in the ports: mapping. For example, change "5173:5173" to "5174:5173" to expose the container on port 5174 instead.

Service cannot connect to the database
If you are not using condition: service_healthy, depends_on only ensures the db container starts before app — it does not wait for PostgreSQL to be ready to accept connections. Add a healthcheck to the db service and set condition: service_healthy in depends_on as shown in the example above.

node_modules is empty inside the container
The bind mount .:/app maps your host directory to /app, which overwrites /app/node_modules with whatever is on your host. If your host directory has no node_modules, the container sees none either. Fix this by adding a named volume entry node_modules:/app/node_modules as shown in the example. Docker preserves the named volume’s contents and the bind mount does not overwrite it.

FAQ

What is the difference between docker compose down and docker compose stop?
docker compose stop stops the running containers but leaves them on disk along with the network and volumes. You can restart them with docker compose start. docker compose down removes the containers and the network. Named volumes are kept unless you add the -v flag.

How do I view logs for a specific service?
Run docker compose logs SERVICE, replacing SERVICE with the service name defined in your compose file (for example, docker compose logs app). To follow logs in real time, add the -f flag: docker compose logs -f app.

How do I pass secrets without hardcoding them in the Compose file?
Create a .env file in the project directory with your credentials (for example, POSTGRES_PASSWORD=changeme) and reference them in the compose file using ${VAR} syntax. Compose reads the .env file automatically. Add .env to your .gitignore so credentials are never committed to version control.

Can I use Docker Compose in production?
Compose works well for single-host deployments — it is simpler to operate than Kubernetes when you only have one server. Use restart: unless-stopped to keep services running after reboots, and keep secrets in environment variables rather than hardcoded values. For multi-host deployments, look at Docker Swarm or Kubernetes.

What is the difference between a bind mount and a named volume?
A bind mount (./host-path:/container-path) maps a specific directory from your host into the container. Changes on either side are immediately visible on the other. A named volume (volume-name:/container-path) is managed entirely by Docker — it persists data independently of the host directory structure and is not tied to a specific path on your machine. Use bind mounts for source code (so edits take effect immediately) and named volumes for database data (so it persists reliably).

Conclusion

Docker Compose gives you a straightforward way to define and run multi-container development environments with a single YAML file. Once you are comfortable with the basics, the next step is writing custom Dockerfiles to build your own images instead of relying on generic ones.

How to Install Git on Debian 13

Git is the world’s most popular distributed version control system used by many open-source and commercial projects. It allows you to collaborate on projects with fellow developers, keep track of your code changes, revert to previous stages, create branches , and more.

This guide covers installing and configuring Git on Debian 13 (Trixie) using apt or by compiling from source.

Quick Reference

For a printable quick reference, see the Git cheatsheet .

Task Command
Install Git (apt) sudo apt install git
Check Git version git --version
Set username git config --global user.name "Your Name"
Set email git config --global user.email "you@example.com"
View config git config --list

Installing Git with Apt

This is the quickest way to install Git on Debian.

Check if Git is already installed:

Terminal
git --version

If Git is not installed, you will see a “command not found” message. Otherwise, it shows the installed version.

Use the apt package manager to install Git:

Terminal
sudo apt update
sudo apt install git

Verify the installation:

Terminal
git --version

Debian 13 stable currently provides Git 2.47.3:

output
git version 2.47.3

You can now start configuring Git.

When a new version of Git is released, you can update using sudo apt update && sudo apt upgrade.

Installing Git from Source

The main benefit of installing Git from source is that you can compile any version you want. However, you cannot maintain your installation through the apt package manager.

Install the build dependencies:

Terminal
sudo apt update
sudo apt install libcurl4-gnutls-dev libexpat1-dev cmake gettext libz-dev libssl-dev gcc wget

Visit the Git download page to find the latest version.

At the time of writing, the latest stable Git version is 2.53.0.

If you need a different version, visit the Git archive to find available releases.

Download and extract the source to /usr/src:

Terminal
wget -c https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.53.0.tar.gz -O - | sudo tar -xz -C /usr/src

Navigate to the source directory and compile:

Terminal
cd /usr/src/git-*
sudo make prefix=/usr/local all
sudo make prefix=/usr/local install

The compilation may take some time depending on your system.

If your shell still resolves /usr/bin/git after installation, open a new terminal or verify your PATH and binary location with:

Terminal
which git
echo $PATH

Verify the installation:

Terminal
git --version
output
git version 2.53.0

To upgrade to a newer version later, repeat the same process with the new version number.

Configuring Git

After installing Git, configure your username and email address. Git associates your identity with every commit you make.

Set your global commit name and email:

Terminal
git config --global user.name "Your Name"
git config --global user.email "youremail@yourdomain.com"

Verify the configuration:

Terminal
git config --list
output
user.name=Your Name
user.email=youremail@yourdomain.com

The configuration is stored in ~/.gitconfig:

~/.gitconfigconf
[user]
name = Your Name
email = youremail@yourdomain.com

You can edit the configuration using the git config command or by editing ~/.gitconfig directly.

For a deeper walkthrough, see How to Configure Git Username and Email .

Troubleshooting

E: Unable to locate package git
Run sudo apt update first and verify you are on Debian 13 repositories. If sources were recently changed, refresh package metadata again.

git --version still shows an older version after source install
Your shell may still resolve /usr/bin/git before /usr/local/bin/git. Check with which git and adjust PATH order if needed.

Build fails with missing headers or libraries
One or more dependencies are missing. Re-run the dependency install command and then compile again.

make succeeds but git command is not found
Confirm install step ran successfully: sudo make prefix=/usr/local install. Then check /usr/local/bin/git exists.

FAQ

Should you use apt or source on Debian 13?
For most systems, use apt because updates are integrated with Debian security and package management. Build from source only when you need a newer Git release than the repository version.

Does compiling from source replace the apt package automatically?
No. Source builds under /usr/local and can coexist with the apt package in /usr/bin. Your PATH order determines which binary runs by default.

How can you remove a source-installed Git version?
If you built from the source tree, run sudo make prefix=/usr/local uninstall from that same source directory.

Conclusion

We covered two ways to install Git on Debian 13: using apt, which provides Git 2.47.3, or compiling from source for the latest version. The default repository version is sufficient for most use cases.

For more information, see the Pro Git book .

How to Revert a Commit in Git

The git revert command creates a new commit that undoes the changes introduced by a specified commit. Unlike git reset , which rewrites the commit history, git revert preserves the full history and is the safe way to undo changes that have already been pushed to a shared repository.

This guide explains how to use git revert to undo one or more commits with practical examples.

Quick Reference

Task Command
Revert the last commit git revert HEAD
Revert without opening editor git revert --no-edit HEAD
Revert a specific commit git revert COMMIT_HASH
Revert without committing git revert --no-commit HEAD
Revert a range of commits git revert --no-commit HEAD~3..HEAD
Revert a merge commit git revert -m 1 MERGE_COMMIT_HASH
Abort a revert in progress git revert --abort
Continue after resolving conflicts git revert --continue

Syntax

The general syntax for the git revert command is:

Terminal
git revert [OPTIONS] COMMIT
  • OPTIONS — Flags that modify the behavior of the command.
  • COMMIT — The commit hash or reference to revert.

git revert vs git reset

Before diving into examples, it is important to understand the difference between git revert and git reset, as they serve different purposes:

  • git revert — Creates a new commit that undoes the changes from a previous commit. The original commit remains in the history. This is safe to use on branches that have been pushed to a remote repository.
  • git reset — Moves the HEAD pointer backward, effectively removing commits from the history. This rewrites the commit history and should not be used on shared branches without coordinating with your team.

As a general rule, use git revert for commits that have already been pushed, and git reset for local commits that have not been shared.

Reverting the Last Commit

To revert the most recent commit, run git revert followed by HEAD:

Terminal
git revert HEAD

Git will open your default text editor so you can edit the revert commit message. The default message looks like this:

plain
Revert "Original commit message"

This reverts commit abc1234def5678...

Save and close the editor to complete the revert. Git will create a new commit that reverses the changes from the last commit.

To verify the revert, use git log to see the new revert commit in the history:

Terminal
git log --oneline -3
output
a1b2c3d (HEAD -> main) Revert "Add new feature"
f4e5d6c Add new feature
b7a8c9d Update configuration

The original commit (f4e5d6c) remains in the history, and the new revert commit (a1b2c3d) undoes its changes.

Reverting a Specific Commit

You do not have to revert the most recent commit. You can revert any commit in the history by specifying its commit hash.

First, find the commit hash using git log:

Terminal
git log --oneline
output
a1b2c3d (HEAD -> main) Update README
f4e5d6c Add login feature
b7a8c9d Fix navigation bug
e0f1a2b Add search functionality

To revert the “Fix navigation bug” commit, pass its hash to git revert:

Terminal
git revert b7a8c9d

Git will create a new commit that undoes only the changes introduced in commit b7a8c9d, leaving all other commits intact.

Reverting Without Opening an Editor

If you want to use the default revert commit message without opening an editor, use the --no-edit option:

Terminal
git revert --no-edit HEAD
output
[main d5e6f7a] Revert "Add new feature"
2 files changed, 0 insertions(+), 15 deletions(-)

This is useful when scripting or when you do not need a custom commit message.

Reverting Without Committing

By default, git revert automatically creates a new commit. If you want to stage the reverted changes without committing them, use the --no-commit (or -n) option:

Terminal
git revert --no-commit HEAD

The changes will be applied to the working directory and staging area, but no commit is created. You can then review the changes, make additional modifications, and commit manually:

Terminal
git status
git commit -m "Revert feature and clean up related code"

This is useful when you want to combine the revert with other changes in a single commit.

Reverting Multiple Commits

Reverting a Range of Commits

To revert a range of consecutive commits, specify the range using the .. notation:

Terminal
git revert --no-commit HEAD~3..HEAD

This reverts the last three commits. The --no-commit option stages all the reverted changes without creating individual revert commits, allowing you to commit them as a single revert:

Terminal
git commit -m "Revert last three commits"

Without --no-commit, Git will create a separate revert commit for each commit in the range.

Reverting Multiple Individual Commits

To revert multiple specific (non-consecutive) commits, list them one after another:

Terminal
git revert --no-commit abc1234 def5678 ghi9012
git commit -m "Revert selected commits"

Reverting a Merge Commit

Merge commits have two parent commits, so Git needs to know which parent to revert to. Use the -m option followed by the parent number (usually 1 for the branch you merged into):

Terminal
git revert -m 1 MERGE_COMMIT_HASH

In the following example, we revert a merge commit and keep the main branch as the base:

Terminal
git revert -m 1 a1b2c3d
  • -m 1 — Tells Git to use the first parent (the branch the merge was made into, typically main or master) as the base.
  • -m 2 — Would use the second parent (the branch that was merged in).

You can check the parents of a merge commit using:

Terminal
git log --oneline --graph

Handling Conflicts During Revert

If the code has changed since the original commit, Git may encounter conflicts when applying the revert. When this happens, Git will pause the revert and show conflicting files:

output
CONFLICT (content): Merge conflict in src/app.js
error: could not revert abc1234... Add new feature
hint: After resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' and run 'git revert --continue'.

To resolve the conflict:

  1. Open the conflicting files and resolve the merge conflicts manually.

  2. Stage the resolved files:

    Terminal
    git add src/app.js
  3. Continue the revert:

    Terminal
    git revert --continue

If you decide not to proceed with the revert, you can abort it:

Terminal
git revert --abort

This restores the repository to the state before you started the revert.

Pushing a Revert to a Remote Repository

Since git revert creates a new commit rather than rewriting history, you can safely push it to a shared repository using a regular push:

Terminal
git push origin main

There is no need for --force because the commit history is not rewritten.

Common Options

The git revert command accepts several options:

  • --no-edit — Use the default commit message without opening an editor.
  • --no-commit (-n) — Apply the revert to the working directory and index without creating a commit.
  • -m parent-number — Specify which parent to use when reverting a merge commit (usually 1).
  • --abort — Cancel the revert operation and return to the pre-revert state.
  • --continue — Continue the revert after resolving conflicts.
  • --skip — Skip the current commit and continue reverting the rest.

Troubleshooting

Revert is in progress
If you see a message that a revert is in progress, either continue with git revert --continue after resolving conflicts or cancel with git revert --abort.

Revert skips a commit
If Git reports that a commit was skipped because it was already applied, it means the changes are already present. You can proceed or use git revert --skip to continue.

FAQ

What is the difference between git revert and git reset?
git revert creates a new commit that undoes changes while preserving the full commit history. git reset moves the HEAD pointer backward and can remove commits from the history. Use git revert for shared branches and git reset for local, unpushed changes.

Can I revert a commit that has already been pushed?
Yes. This is exactly what git revert is designed for. Since it creates a new commit rather than rewriting history, it is safe to push to shared repositories without disrupting other collaborators.

How do I revert a merge commit?
Use the -m option to specify the parent number. For example, git revert -m 1 MERGE_HASH reverts the merge and keeps the first parent (usually the main branch) as the base.

Can I undo a git revert?
Yes. Since a revert is just a regular commit, you can revert the revert commit itself: git revert REVERT_COMMIT_HASH. This effectively re-applies the original changes.

What happens if there are conflicts during a revert?
Git will pause the revert and mark the conflicting files. You need to resolve the conflicts manually, stage the files with git add, and then run git revert --continue to complete the operation.

Conclusion

The git revert command is the safest way to undo changes in a Git repository, especially for commits that have already been pushed. It creates a new commit that reverses the specified changes while keeping the full commit history intact.

If you have any questions, feel free to leave a comment below.

❌