Install Docker Compose and Deploy Nginx on Ubuntu

Docker Compose extends Docker with declarative configuration files that define multi-container deployments. This guide covers installation of Docker Compose on Ubuntu 22.04, deployment of an Nginx web server, and addition of Heimdall as a second service to demonstrate extensibility.

Prerequisites

This guide was tested on Ubuntu 22.04 Server in September 2023, updated January 2025. You need sudo or root access to install packages and manage containers. Basic familiarity with YAML syntax and Docker concepts is helpful but not required.

Install Docker and Docker Compose

Modern Ubuntu releases include Docker Compose v2 as part of the docker.io package. Install both Docker and Compose with a single command:

sudo apt update
sudo apt install docker.io docker-compose-v2 -y

Docker Compose v2 integrates directly with the Docker CLI. Commands use docker compose (two words) rather than the deprecated docker-compose executable. The v2 plugin provides better integration and is the actively maintained version.

Verify the installation:

docker compose version

Test Docker functionality with the hello-world image:

sudo docker run hello-world

The output confirms Docker downloaded the test image and executed it successfully:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
719385e32844: Pull complete
Digest: sha256:4f53e2564790c8e7856ec08e384732aa38dc43c52f02952483e3f003afbf23db
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

Create a Compose Configuration File

Docker Compose uses YAML files to define services, networks, and volumes. Create a directory structure for the configuration:

sudo mkdir -p /opt/docker
sudo nano /opt/docker/docker-compose.yml

YAML format is strict about indentation. Use spaces, not tabs. Each indentation level uses two spaces. Create a basic configuration for Nginx:

services:
  nginx:
    container_name: nginx
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/opt/docker/nginx/config:/etc/nginx/conf.d:ro"
      - "/opt/docker/nginx/logs:/var/log/nginx"
      - "/opt/docker/nginx/html:/usr/share/nginx/html"
    restart: unless-stopped

The services section defines each container. Port mappings use the format host:container where the first value is the port on the host system and the second is the port inside the container. Volume mappings follow the same pattern, with host paths on the left and container paths on the right. The :ro suffix marks the config volume as read-only.

Modern Compose files do not require a version number at the top. Docker Compose v2 uses the presence of the services key to identify valid compose files.

Create the required directories:

sudo mkdir -p /opt/docker/nginx/{config,logs,html}

Start the containers from the /opt/docker directory:

cd /opt/docker
sudo docker compose up -d

The -d flag runs containers in detached mode as background services. Docker pulls the nginx image and creates the container:

Creating network "docker_default" with the default driver
Pulling nginx (nginx:latest)...
latest: Pulling from library/nginx
a803e7c4b030: Pull complete
8b625c47d697: Pull complete
4d3239651a63: Pull complete
Digest: sha256:32da30332506740a2f7c34d5dc70467b7f14ec67d912703568daff790ab3f755
Status: Downloaded newer image for nginx:latest
Creating nginx ... done

Verify the container is running:

sudo docker ps

The output shows active containers with their port mappings and status:

CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                                      NAMES
431d8c2e491a   nginx:latest   "/docker-entrypoint.…"   35 seconds ago   Up 34 seconds   0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp   nginx

Configure Nginx

Create a minimal Nginx configuration at /opt/docker/nginx/config/nginx.conf:

server {
  listen 80;
  server_name _;
  root /usr/share/nginx/html;
}

The underscore in server_name acts as a catch-all that matches any hostname. The root path is the location inside the container, which maps to /opt/docker/nginx/html on the host through the volume definition.

Create a basic HTML file at /opt/docker/nginx/html/index.html:

<!DOCTYPE html>
<html>
<body>
<h1>Docker Compose Test</h1>
<p>Nginx is running in a container.</p>
</body>
</html>

Restart the nginx container to load the configuration:

sudo docker restart nginx

The page should now be accessible at the server IP address on port 80. If you encounter issues, check the logs directory at /opt/docker/nginx/logs/. File permissions are the most common problem. The nginx user inside the container must be able to read files in the html directory.

Add a Second Service

Heimdall is a dashboard application for organizing links to self-hosted services. It demonstrates how to run multiple services from a single compose file. Add the heimdall service to docker-compose.yml below the nginx section:

services:
  nginx:
    container_name: nginx
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/opt/docker/nginx/config:/etc/nginx/conf.d:ro"
      - "/opt/docker/nginx/logs:/var/log/nginx"
      - "/opt/docker/nginx/html:/usr/share/nginx/html"
    restart: unless-stopped

  heimdall:
    image: lscr.io/linuxserver/heimdall:latest
    container_name: heimdall
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Berlin
    volumes:
      - "/opt/docker/heimdall/config:/config"
    ports:
      - "8080:80"
    restart: unless-stopped

The PUID and PGID values set the user and group ID that the containerized process runs under. Match these to your user account if you need the container to write files that your user can modify. Use id to find your UID and GID values. Set TZ to your local timezone.

Create the heimdall directory:

sudo mkdir -p /opt/docker/heimdall/config

Start both services:

cd /opt/docker
sudo docker compose up -d

Compose only recreates containers with configuration changes. Nginx remains running while heimdall is pulled and started:

nginx is up-to-date
Pulling heimdall (lscr.io/linuxserver/heimdall:latest)...
latest: Pulling from linuxserver/heimdall
Creating heimdall ... done

Heimdall is now accessible on port 8080. The compose file defines both services and manages them as a unit.

Use Environment Variables

Environment files reduce duplication when the same values appear in multiple services. Create a file named .env in the same directory as docker-compose.yml:

TIMEZONE=Europe/Berlin
PUID=1000
PGID=1000

Reference variables in the compose file with ${VARIABLE_NAME} syntax:

services:
  nginx:
    container_name: nginx
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/opt/docker/nginx/config:/etc/nginx/conf.d:ro"
      - "/opt/docker/nginx/logs:/var/log/nginx"
      - "/opt/docker/nginx/html:/usr/share/nginx/html"
    restart: unless-stopped

  heimdall:
    image: lscr.io/linuxserver/heimdall:latest
    container_name: heimdall
    environment:
      - PUID=${PUID}
      - PGID=${PGID}
      - TZ=${TIMEZONE}
    volumes:
      - "/opt/docker/heimdall/config:/config"
    ports:
      - "8080:80"
    restart: unless-stopped

Docker Compose reads .env automatically when present in the same directory as the compose file. Change values in one location to update all services. Store sensitive values like API keys or passwords in the .env file and restrict permissions:

sudo chmod 600 /opt/docker/.env

Root starts Docker containers and can read the .env file regardless of permissions. The PUID and PGID values only take effect after the container starts.

Update and Maintain Containers

Update the Docker package through apt:

sudo apt update
sudo apt upgrade docker.io docker-compose-v2

Update container images by pulling new versions. From the directory containing docker-compose.yml:

cd /opt/docker
sudo docker compose pull

Compose checks for newer images:

Pulling nginx    ... done
Pulling heimdall ... done

Recreate containers that have updates available:

sudo docker compose up -d

Compose only restarts services with changes:

heimdall is up-to-date
Recreating nginx ... done

To update a single container, pull the specific image:

sudo docker pull nginx:1.26

Stop and remove the old container:

sudo docker stop nginx
sudo docker rm nginx

Recreate it from the compose file:

sudo docker compose up -d --force-recreate

All data and configuration files stored in mapped volumes persist across container updates. Data inside the container that is not mapped to a volume is lost when the container is removed.

To remove a service completely, stop it, delete the container, and remove its section from docker-compose.yml:

sudo docker stop heimdall
sudo docker rm heimdall

Edit docker-compose.yml to remove the heimdall service definition. Mapped directories remain on the host and must be deleted manually if no longer needed.

View logs for troubleshooting:

sudo docker logs nginx
sudo docker logs heimdall

Or follow logs in real time:

sudo docker logs -f nginx

Container management becomes simpler as the compose file grows. Start all services with one command, update everything with two commands, and maintain a readable record of your infrastructure configuration.

AI assistance is used on this site for language, formatting, and turning research into a consistent template. It is not used to perform the underlying research or verify technical claims. Every command, configuration, and step in this post is tested by hand before publication.

Leave a Reply

Your email address will not be published. Required fields are marked *

ten + twelve =