Install Docker Compose on Ubuntu 22.04 by adding Docker’s signed APT repository, installing Compose v2, and launching a small Apache and MariaDB stack. You can reach a working local test in about ten minutes without exposing the database or web port to the public internet.
Prerequisites
- A 64-bit Ubuntu 22.04 LTS (Jammy Jellyfish) VPS with at least 2 GB of RAM and 10 GB of free disk space.
- SSH access as
root, or a regular account with workingsudoprivileges. - Outbound HTTPS access to Docker’s package repository and Docker Hub.
- A VPSLake Linux VPS if you need an Ubuntu server with root access.
- No domain is required for this private test. You will need a domain later if you place a public application behind a TLS reverse proxy.
The commands use sudo for a regular SSH account. If you are already logged in as root, remove sudo from each command.
Step 1: Confirm Jammy and refresh APT
Docker publishes a separate package repository for each Ubuntu release, so confirm that this server really is Ubuntu 22.04 before adding the Jammy source.
grep -E '^(PRETTY_NAME|VERSION_CODENAME)=' /etc/os-release
dpkg --print-architecture
sudo apt update
The release check should show Jammy and a supported architecture such as amd64 or arm64:
PRETTY_NAME="Ubuntu 22.04.5 LTS"
VERSION_CODENAME=jammy
amd64
Do not continue if apt update reports a broken mirror, DNS failure, or unsigned source. A clean package index is needed before Docker’s repository can be trusted.
Step 2: Add Docker’s signed Ubuntu repository
The keyring and deb822 source file let APT verify Docker packages without the deprecated apt-key command. This source is pinned to Ubuntu 22.04’s jammy suite.
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: jammy
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
Check that APT can see a Docker Engine candidate from the new source:
apt-cache policy docker-ce
The Candidate should contain a version from download.docker.com, and the repository line should mention jammy. Docker’s Ubuntu installation guide lists Jammy as a supported release and documents this repository method.
Step 3: Install Docker Engine and Compose v2
Compose is maintained as a Docker CLI plugin, so install it with the Engine rather than downloading the old standalone docker-compose binary.
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo systemctl is-active docker
sudo docker version --format 'Client {{.Client.Version}} / Server {{.Server.Version}}'
sudo docker compose version
You should see an active daemon, matching client and server versions, and a Compose v2 version. Patch numbers change over time, so do not compare the exact numbers:
active
Client 29.x.y / Server 29.x.y
Docker Compose version v2.x.y
Run Docker’s smoke test before creating a project. It downloads a small image, starts a container, prints a confirmation, and exits:
sudo docker run --rm hello-world
The output should contain:
Hello from Docker!
Your installation appears to be working correctly.
If Docker was already installed, check docker compose version first. Do not mix an old docker.io or standalone docker-compose installation with Docker’s docker-ce packages until you have identified which installation owns your existing containers.
Step 4: Give your SSH user controlled Docker access
The Docker socket is root-owned by default. The docker group removes repeated sudo prompts, but membership is effectively administrative access to the VPS.
sudo groupadd --force docker
sudo usermod -aG docker "$USER"
Disconnect from SSH and open a new session so the group membership is loaded. Then confirm that the account can reach the daemon:
id -nG
docker info --format 'Server {{.ServerVersion}}'
The group list should include docker, followed by the Docker server version. Until you reconnect, use sudo docker compose ... or run newgrp docker for the current shell. For a stricter privilege boundary, use Docker’s rootless mode instead of adding a user to this group.
Step 5: Create an Apache and MariaDB Compose project
A Compose file keeps service definitions, the private network, and persistent storage together. This example serves a predictable page with Apache and starts MariaDB with a health check, while binding Apache only to the VPS loopback interface.
Create the project directory and its test page:
mkdir -p ~/compose-jammy/public
cd ~/compose-jammy
printf '%s\n' 'Docker Compose on Ubuntu 22.04 is running.' > public/index.html
Create a protected environment file. Replace both uppercase values with long, unique passwords before starting the stack; they are placeholders, not safe credentials.
tee .env >/dev/null <<'EOF'
MARIADB_ROOT_PASSWORD=YOUR_STRONG_ROOT_PASSWORD
MARIADB_PASSWORD=YOUR_STRONG_APP_PASSWORD
EOF
chmod 600 .env
Now create compose.yaml:
services:
web:
image: httpd:2.4-alpine
ports:
- "127.0.0.1:8081:80"
volumes:
- ./public:/usr/local/apache2/htdocs:ro
depends_on:
database:
condition: service_healthy
database:
image: mariadb:11
environment:
MARIADB_DATABASE: compose_demo
MARIADB_USER: compose_app
MARIADB_PASSWORD: ${MARIADB_PASSWORD:?Set MARIADB_PASSWORD in .env}
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:?Set MARIADB_ROOT_PASSWORD in .env}
volumes:
- mariadb_data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 12
volumes:
mariadb_data:
The read-only bind mount prevents Apache from changing the page, and the named mariadb_data volume survives container replacement. MariaDB has no ports entry, so it is reachable only from the Compose network; Apache’s 127.0.0.1 binding likewise prevents direct public access. Docker’s Compose file reference explains the service, volume, and dependency keys you can add to a real application.
Step 6: Validate and start the stack
Render the Compose configuration before pulling images so a YAML error or missing password is caught early.
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
config --quiet should produce no output and return status 0. The first pull may take a few minutes. Both services should then be running, with the database marked healthy and Apache bound to loopback:
NAME IMAGE SERVICE STATUS PORTS
compose-jammy-web-1 httpd:2.4-alpine web Up 127.0.0.1:8081->80/tcp
compose-jammy-database-1 mariadb:11 database Up (healthy)
Container names can differ if you use another directory. Select services by the SERVICE column, such as web or database, in later commands.
Verify Docker Compose on Ubuntu 22.04
Test the page and make a database query from inside the database container. Together, these checks prove that Compose can start both services and that the persistent database accepts connections.
curl --fail --silent --show-error http://127.0.0.1:8081
docker compose exec database sh -c 'mariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" -e "SELECT 1 AS compose_ok" "$MARIADB_DATABASE"'
docker compose logs --tail 20 web database
The first command should print the page text, and the query should return one:
Docker Compose on Ubuntu 22.04 is running.
compose_ok
1
Because port 8081 is bound to 127.0.0.1, a browser on your workstation cannot reach it directly. For private browser testing, create an SSH tunnel from your workstation with ssh -L 8081:127.0.0.1:8081 USER@SERVER_IP, then open http://127.0.0.1:8081 locally. If you later publish an application, put it behind TLS and apply a deliberate firewall policy; Docker warns that published container ports can bypass ordinary UFW rules. VPSLake’s UFW firewall generator can help draft a host policy.
Troubleshooting
docker: 'compose' is not a docker command
The Compose CLI plugin is missing, or your shell is using a different Docker installation than the one configured above. Refresh the Jammy package index and install the plugin:
sudo apt update
sudo apt install -y docker-compose-plugin
docker compose version
Use docker compose with a space. The hyphenated docker-compose command belongs to the legacy standalone installation and is not required for Compose v2.
permission denied while trying to connect to the Docker daemon socket
The current SSH session has not loaded the new docker group, or the account was not added to it. Reconnect, run id -nG, and retry; sudo docker compose ... is a temporary workaround. Remember that docker group membership grants root-equivalent control.
E: Package 'docker-ce' has no installation candidate
APT is not reading Docker’s Jammy repository, commonly because the source file has the wrong suite or the previous update failed. Inspect the source and refresh the package index:
cat /etc/apt/sources.list.d/docker.sources
sudo apt update
apt-cache policy docker-ce
The file should contain Suites: jammy, and apt update must complete without an error. If docker.io is already installed, do not blindly remove it on a server with existing workloads; plan the package migration first.
dependency failed to start: container compose-jammy-database-1 is unhealthy
Read the database logs and check that .env contains both password variables with no spaces around the equals signs:
docker compose logs database
docker compose config
MariaDB stores its initial credentials in mariadb_data. Correcting .env does not change a password in an already initialized volume. For a disposable test only, docker compose down -v deletes that volume before a clean docker compose up -d; never use -v when the database contains data you need.
Hardening
- Keep
.envat mode600, exclude it from source control, and move production secrets to a managed secret store. - Keep application ports on loopback until authentication, TLS, and the firewall policy are ready. Do not expose MariaDB to the public internet.
- Back up
mariadb_dataand test restoring it before treating the stack as production data. - Pin production images to reviewed tags or digests, then update deliberately with
docker compose pullfollowed bydocker compose up -d.
FAQ
Is Docker Compose included when I install Docker on Ubuntu 22.04?
It is available as the docker-compose-plugin package when you install Docker Engine from Docker’s official APT repository. Verify the installation with docker compose version; the old docker-compose executable is a separate legacy tool.
Why does this guide use compose.yaml instead of docker-compose.yml?
Compose v2 recognizes both names, but compose.yaml is the current default filename and makes the newer workflow clear. If an existing project uses docker-compose.yml, you can keep it or select another file with docker compose -f FILE up -d.
Does docker compose down delete the MariaDB data?
Normal down removes the containers and Compose network but retains the named mariadb_data volume. Adding -v also deletes that volume, so reserve it for resetting disposable test data.
How do I expose the web service safely?
Keep the sample bound to loopback and place a TLS reverse proxy in front of it, usually on ports 80 and 443. Allow only the required public ports in UFW, account for Docker’s packet-filtering behavior, and never publish MariaDB just to make the application work.