Docker has become a standard tool for developers and system administrators who need to package, distribute, and run applications consistently across different environments. If you run Ubuntu and want to start using Docker, this guide walks through the complete installation process, explains the core concepts, and shows practical examples you can use immediately after setting things up.

The installation method covered here uses the official Docker repository. This approach gives you the latest stable release rather than relying on the version available in Ubuntu's default repositories, which is often several versions behind.

What Docker Actually Is

Docker packages applications and their dependencies into containers: lightweight, standalone units that include everything needed to run the software. A container is not a virtual machine. It runs on the host kernel, shares the host operating system, and starts in seconds rather than minutes. Multiple containers can run on the same host without interfering with each other.

The key benefit is consistency. If your application runs in a container on your development machine, it runs the same way on a testing server and in production. The "it works on my machine" problem largely disappears when everyone works with the same container image.

Docker Compose complements Docker by managing multi-container applications. Rather than running multiple docker run commands separately, you define your entire stack in a single configuration file and control it with straightforward commands. You can explore more about multi-container setups in this Docker Compose guide.

Why Install Docker from the Official Repository

Ubuntu includes Docker packages in its default repositories, but these versions often lag behind the current stable releases. Installing from the official Docker repository ensures you get the latest features, security updates, and bug fixes. The process takes a few extra minutes but pays off in long-term stability and access to newer capabilities.

The official repository also includes docker-compose-plugin, which integrates Docker Compose directly into the Docker CLI rather than requiring a separate installation.

Installing Docker on Ubuntu

The installation involves adding Docker's GPG key, adding the repository, and then installing the packages. Run each command in sequence.

sudo apt update

sudo apt install apt-transport-https ca-certificates curl gnupg lsb-release

These packages enable secure package transfers over HTTPS and provide tools needed for the repository setup.

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

This downloads Docker's GPG key and converts it to a format Ubuntu's package manager can use to verify package authenticity.

echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
 sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

This adds Docker's official repository to your system. The signed-by option links the repository to the GPG key you just installed.

sudo apt update

sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin

The docker-ce package is the Docker Engine itself. The containerd component handles container lifecycle management, and the compose plugin provides Docker Compose functionality.

Adding Your User to the Docker Group

By default, running Docker commands requires root privileges. You can add your user to the docker group to avoid typing sudo for every command.

sudo usermod -aG docker $USER

newgrp docker

The first command adds your current user to the docker group. The second command refreshes your group memberships so you do not need to log out and back in. After running these commands, verify your access by running a test command.

docker run hello-world

If Docker prints a message saying your installation appears to be working correctly, you are ready to proceed.

Note: Members of the docker group can effectively gain root access on the host system. Only add users you trust with that level of access. For shared systems, prefer using sudo docker instead of adding users to the docker group.

Verifying the Installation

Check the installed Docker version to confirm everything is set up correctly.

docker --version

You should see output similar to "Docker version 24.0.7, build afdd53b" or whatever current version is installed. The docker-compose-plugin version can be checked separately.

docker compose version

Understanding Docker Images and Containers

A Docker image is a read-only template with instructions for creating a container. Images are defined by a Dockerfile, which specifies the base image, the application code, dependencies, and configuration.

A container is a runnable instance of an image. You can run multiple containers from the same image, and they remain isolated from each other and from the host system. This isolation is what makes Docker useful for running different versions of the same software on one machine without conflicts.

Basic commands for working with images and containers include:

# List running containers
docker ps

# List all containers (including stopped)
docker ps -a

# List downloaded images
docker images

Running Your First Container

The simplest way to run a container is to use an existing image from Docker Hub, which is the public image registry maintained by Docker. Running an official image is a good way to verify your installation and understand how containers behave.

docker run nginx:alpine

This pulls the nginx:alpine image from Docker Hub if it is not already present, then starts a container from it. The container runs in the foreground, printing logs to your terminal. Press Ctrl+C to stop it.

Running containers in detached mode (background) is more practical for server use. You can also map ports to access services running inside the container from your host system.

docker run -d --name my_nginx -p 8080:80 nginx:alpine

This runs the container in the background with the name my_nginx. The -p flag maps port 80 inside the container to port 8080 on the host. Visit http://localhost:8080 to see the Nginx welcome page.

Managing running containers involves a few straightforward commands:

# View running containers
docker ps

# Stop the container
docker stop my_nginx

# Remove the container (after stopping)
docker rm my_nginx

Creating a Dockerfile for a PHP Application

A Dockerfile defines how to build your own application image. For a PHP web application served by Nginx, you typically need two Dockerfiles: one for the PHP service and one for Nginx.

Here is a basic Dockerfile for a PHP application using the official PHP FPM image:

FROM php:8.2-fpm-alpine

RUN docker-php-ext-install pdo pdo_mysql

COPY src/ /var/www/html/

RUN chown -R www-data:www-data /var/www/html

This Dockerfile starts from the official PHP Alpine image, installs the PDO and MySQL extensions, copies your application code into the container, and sets the correct ownership.

And the Nginx Dockerfile:

FROM nginx:alpine

COPY nginx.conf /etc/nginx/conf.d/default.conf

COPY public/ /var/www/html/

Build both images with the docker build command:

docker build -t myapp-php ./php

docker build -t myapp-nginx ./nginx

The -t flag tags the image with a name, making it easier to reference later.

Using Docker Compose for Multi-Container Applications

Docker Compose manages multi-container applications through a single configuration file. A docker-compose.yml file defines the services, networks, and volumes for your application stack. This approach simplifies running complex setups that would otherwise require multiple docker run commands with many flags.

For a typical web application with Nginx, PHP, and MySQL, the compose file looks like this:

version: '3.8'

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - ./public:/var/www/html
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - php
  php:
    image: php:8.2-fpm-alpine
    volumes:
      - ./public:/var/www/html
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: myapp
    volumes:
      - db_data:/var/lib/mysql

volumes:
  db_data:

Start the entire stack with one command:

docker-compose up -d

Useful Docker Compose commands for managing your stack:

# View logs from all services
docker-compose logs -f

# Stop everything
docker-compose down

# Stop and remove volumes (fresh start)
docker-compose down -v

Managing Data with Docker Volumes

Containers are ephemeral by design. When a container is removed, any data stored inside it disappears. For databases and other data that must persist, use Docker volumes. Volumes store data outside the container's filesystem, ensuring it survives container removal and updates.

# Create a named volume
docker volume create mydata

# Run a container with a volume
docker run -v mydata:/data nginx:alpine

# Inspect volume details
docker volume inspect mydata

In docker-compose, volumes are defined under the top-level volumes key and referenced by name in each service:

volumes:
  db_data:
    driver: local

The named volume db_data is then mounted to the MySQL service:

services:
  db:
    image: mysql:8.0
    volumes:
      - db_data:/var/lib/mysql

Networking Between Containers

Docker Compose automatically creates a network for your services. All containers defined in the compose file can communicate with each other using their service names as hostnames.

In the example above, the PHP service can reach the MySQL service at the hostname db on port 3306. Your application configuration in PHP should use db as the database hostname, not localhost.

# In your PHP application configuration
$dbHost = 'db';
$dbName = 'myapp';
$dbUser = 'root';
$dbPass = 'secret';

$pdo = new PDO("mysql:host=$dbHost;dbname=$dbName", $dbUser, $dbPass);

This is a common point of confusion when moving applications into Docker. The container running your application has its own network namespace and cannot reach localhost on the host machine. Using the service name from your compose file is the correct approach.

Useful Docker Commands for Daily Use

Once you have Docker running, a handful of commands cover most daily operations:

# See resource usage for all containers
docker stats

# See all containers regardless of state
docker ps -a

# View logs from a specific container
docker logs -f container_name

# Execute a command inside a running container
docker exec -it container_name /bin/sh

# Copy files between host and container
docker cp local_file.txt container_name:/path/
docker cp container_name:/path/file.txt local_file.txt

# Remove stopped containers, unused networks, and dangling images
docker system prune

The docker exec command is particularly useful for debugging. If a container is not behaving as expected, you can open a shell inside it and inspect the filesystem, running processes, and configuration.

Container Security Considerations

Running containers in production requires attention to security. Containers share the host kernel, so vulnerabilities in container images can affect the entire host. A few practical steps help reduce risk.

Use official images from trusted sources whenever possible. Official images are maintained by the software vendors or Docker's own team and receive regular security updates. Third-party images may not be updated as frequently.

Avoid running containers as root when possible. Many official images include a non-root user specifically for this purpose. If your image does not include one, consider creating a Dockerfile that adds a user.

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

USER appuser

Keep images updated. When new versions of base images become available, rebuild your custom images to incorporate security patches. You can learn more about hardening containers in this Docker security guide.

When Docker Might Be Overkill

Docker is powerful but introduces complexity. For a simple single-server setup with one or two applications, traditional installation methods are often simpler and easier to debug. You have direct access to the filesystem, straightforward log locations, and no additional abstraction layer to reason about.

If you run a single WordPress site on a VPS, a traditional LAMP setup is likely easier to manage than containerising WordPress, Nginx, MySQL, and PHP separately. You gain simplicity at the cost of some flexibility and environment consistency.

Docker becomes valuable as the complexity of your infrastructure grows. If you need to run multiple applications with conflicting dependencies, want to scale horizontally across multiple servers, or need to replicate a production environment locally for development, Docker handles these scenarios well. You might also consider pairing Docker with a reverse proxy setup, which you can explore in this Nginx reverse proxy guide.

Taking the Next Step with Docker

Installing Docker on Ubuntu is straightforward once you understand the repository setup and core concepts. The official Docker documentation provides detailed references for each command and configuration option if you need to go deeper into specific topics.

If you are planning to expose containerised services to the internet, a reverse proxy configuration helps manage multiple domains and automatically handles SSL certificates. Combining Docker with proper server security practices keeps your applications both accessible and protected.

If you encounter issues during installation or need help designing a containerised architecture for your project, you can get in touch with details about your setup, the applications you want to run, and your current server environment.