Understanding Docker Containers Before You Install
Docker has fundamentally changed how developers and system administrators deploy applications. Instead of dealing with conflicting dependencies or the classic "works on my machine" problem, you package everything your application needs into a container that runs consistently anywhere Docker is installed.
Before installing Docker on Ubuntu 18.04, it helps to understand the core concepts. Docker containers are lightweight, isolated environments that share the host kernel but remain separate from each other and from the host system. Unlike virtual machines, containers start in seconds rather than minutes and use far fewer resources because they do not emulate an entire operating system.
The two main components you will work with are images and containers. An image is a read-only template defined by a Dockerfile. A container is a running instance of an image. You can create multiple containers from the same image, each running independently with its own filesystem and process space.
Docker Compose adds another layer of functionality by letting you define multi-container applications in a single configuration file. Rather than running several docker run commands with multiple flags, you describe your entire stack once and control it with straightforward commands. This approach scales well as your applications grow in complexity.
Why Use the Official Docker Repository on Ubuntu
Ubuntu 18.04 includes Docker packages in its default repositories, but these versions typically lag behind current stable releases by several minor versions. The official Docker repository gives you the latest stable release with recent features, security patches, and bug fixes.
Using the official repository also simplifies updates. When a new Docker version becomes available, it appears through the standard package manager just like other system updates. This integration keeps your Docker installation consistent with how you manage the rest of your Ubuntu system.
The repository setup takes only a few minutes and involves three steps: installing prerequisites, adding Docker's GPG key, and configuring the repository itself.
Installing Prerequisites
Start by updating your package index and installing the packages required to access repositories over HTTPS. These packages are standard dependencies for secure package management on Ubuntu.
sudo apt update
sudo apt install apt-transport-https ca-certificates curl gnupg lsb-release
The apt-transport-https package enables your package manager to communicate over HTTPS. The ca-certificates package provides trusted SSL certificates, curl handles HTTP requests, and lsb-release supplies your Ubuntu version information for the repository setup.
Adding Docker's GPG Key
Docker signs its packages cryptographically to verify authenticity. You need to download Docker's GPG key and convert it to a format Ubuntu's package manager can use for verification.
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
This command downloads the key directly from Docker's servers and writes it to the keyring directory. The --dearmor option converts the key from OpenPGP format to the GPG format Ubuntu expects.
Configuring the Docker Repository
With the GPG key in place, add Docker's official repository to your system. The repository URL includes your Ubuntu version codename, which the lsb-release command automatically detects.
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
The signed-by option explicitly links this repository to the GPG key you installed. This configuration ensures apt will only accept packages from this repository if they are signed with the corresponding key.
Update your package index again to include the newly added repository.
sudo apt update
Installing Docker Engine and Components
Now install the Docker packages. The installation includes the Docker Engine, the container runtime, and the Docker Compose plugin.
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin
Each component serves a specific purpose. The docker-ce package is the Docker Engine itself, responsible for building and running containers. The containerd component handles the low-level container lifecycle, managing image storage, container execution, and networking. The docker-compose-plugin integrates Docker Compose functionality directly into the Docker CLI, letting you use the docker compose command instead of a separate docker-compose binary.
Verifying the Installation
After installation, check the installed versions to confirm everything is working correctly.
docker --version
docker compose version
You should see output indicating Docker Engine version 24.x or similar, along with a Docker Compose plugin version. If these commands return without error, the installation succeeded.
Run the hello-world image to verify that Docker can pull and execute images.
sudo docker run hello-world
This command downloads a minimal test image from Docker Hub, runs it in a container, and prints a confirmation message. If you see this message, your Docker installation is working correctly.
Avoiding sudo for Every Docker Command
By default, the Docker daemon requires root privileges. Running docker commands with sudo every time becomes tedious. You can add your user to the docker group to run Docker commands without elevated privileges.
sudo usermod -aG docker $USER
newgrp docker
The first command adds your current user to the docker group. The second command activates the group membership in your current shell without requiring you to log out and back in. After running these commands, verify access by running the hello-world image again without sudo.
docker run hello-world
Security note: Users in the docker group can effectively run commands with root privileges on the host system. Only add trusted users to this group. On shared systems or production servers, consider using sudo docker as a safer approach.
Pulling and Running Container Images
Docker Hub hosts thousands of official images you can pull and run immediately. Understanding how to use existing images before building your own helps you grasp how containers behave.
Running a container from an official image is straightforward. The following command pulls the Nginx Alpine image if it is not already cached locally, then starts a container from it.
docker run nginx:alpine
This runs the container in the foreground, with logs appearing in your terminal. Press Ctrl+C to stop it. For practical use, running containers in detached mode is more useful.
docker run -d --name my_nginx -p 8080:80 nginx:alpine
The -d flag runs the container in the background. The --name flag assigns a memorable name to the container. The -p flag maps port 80 inside the container to port 8080 on your host. Open a browser and visit http://localhost:8080 to see the Nginx welcome page.
Managing containers involves a few essential commands.
# List running containers
docker ps
# List all containers including stopped ones
docker ps -a
# Stop the container
docker stop my_nginx
# Remove the container
docker rm my_nginx
Building Custom Images with Dockerfiles
While running existing images is useful, you will eventually need to create images for your own applications. A Dockerfile describes the steps to build your image, from choosing a base image to installing dependencies and copying your code.
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
The FROM instruction specifies the base image. The RUN instruction executes commands during the build process. The COPY instruction transfers files from your host into the image filesystem.
Build the image with the docker build command, providing a tag name and the build context directory.
docker build -t myapp-php ./php
The tag makes the image easy to reference when running containers from it.
Managing Multi-Container Applications with Docker Compose
Most web applications involve multiple services: a web server, an application runtime, and a database. Running these as separate containers with individual docker run commands becomes unwieldy. Docker Compose lets you define the entire stack in a single docker-compose.yml file.
Here is a complete example for a typical PHP application with Nginx and MySQL.
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:
This configuration defines three services: Nginx as the web server, PHP FPM as the application runtime, and MySQL as the database. The depends_on instruction ensures services start in the correct order.
Start the entire stack with one command.
docker compose up -d
Useful commands for managing your compose stack include:
# View logs from all services
docker compose logs -f
# Stop all services
docker compose down
# Stop and remove volumes for a clean start
docker compose down -v
If you are working with more complex multi-container setups, the approach scales well beyond this basic example. You can explore more advanced patterns in a detailed Docker Compose guide.
Understanding Docker Volumes for Persistent Data
Containers are designed to be temporary. When you remove a container, any data stored inside it is lost. For data that must persist, such as database files, configuration, or user uploads, Docker provides volumes.
Volumes store data outside the container's writable layer, making it independent of the container lifecycle. You can create, inspect, and manage volumes separately from containers.
# 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 declared in the top-level volumes section and referenced by name in each service. The db_data volume in the previous compose file follows this pattern, ensuring database files survive container restarts and removals.
Networking Between Containers
Docker Compose automatically creates a network for services defined in the same compose file. Containers can communicate with each other using their service names as hostnames.
In the example above, the PHP service reaches the MySQL database using the hostname db on port 3306. Your application configuration should use this service name, not localhost.
# Correct approach in your 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 exists in its own network namespace. It cannot reach localhost on the host machine because localhost inside the container refers to the container itself, not the host. Using the service name from your compose file resolves this correctly.
Essential Commands for Daily Docker Use
With Docker installed and running, these commands cover most routine operations.
# Monitor resource usage for all containers
docker stats
# View all containers regardless of state
docker ps -a
# Follow logs from a specific container
docker logs -f container_name
# Open a shell 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/
# Clean up unused resources
docker system prune
The docker exec command is particularly valuable for troubleshooting. When a container misbehaves, you can open an interactive shell inside it to inspect the filesystem, examine running processes, and check configuration files.
Hardening Container Security
Containers share the host kernel, which means vulnerabilities in container images can potentially affect the host system. Taking a layered approach to security reduces risk significantly.
Use official images from trusted sources whenever possible. Official images receive regular updates and security patches from the software maintainers or Docker's team. Third-party images may not be maintained with the same diligence.
Running containers as a non-root user is good practice. Many official images include a non-root user specifically for this purpose. If your image does not include one, add a user in your Dockerfile.
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
Keep your images updated. When base image updates become available, rebuild your custom images to incorporate security patches. This practice applies not just to application dependencies but also to the underlying OS within your container images.
Limit container capabilities to only what your application needs. Docker grants containers a default set of capabilities, but most applications do not require all of them. Use the --cap-drop option to remove unnecessary capabilities.
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx:alpine
A comprehensive approach to container hardening involves multiple practices working together. You can learn more about these techniques in this guide to container hardening.
When Traditional Installation May Be Simpler
Docker adds meaningful complexity to server management. For straightforward single-server deployments with one or two applications, traditional installation methods are often easier to maintain. You have direct access to the filesystem, standard log locations, and no additional abstraction layer to reason about when something goes wrong.
If you run a single WordPress site or a basic PHP application on a VPS, a conventional LAMP setup may serve you better than containerising each component separately. You gain simplicity and straightforward debugging at the cost of some environment consistency.
Docker becomes valuable as your infrastructure grows in complexity. Running multiple applications with conflicting dependencies, scaling across several servers, or replicating production environments locally for development are scenarios where Docker excels. You can read more about when containerisation makes sense in this practical comparison of Docker versus traditional deployment.
Moving Forward with Docker on Ubuntu
Installing Docker on Ubuntu 18.04 from the official repository gives you a solid foundation for building, deploying, and managing containerised applications. The process involves straightforward steps that take effect immediately and integrate well with your system's package management.
Once Docker is running, experimenting with existing images from Docker Hub helps you understand how containers behave before building your own. Running a multi-container application with Docker Compose demonstrates how the pieces fit together in a realistic scenario.
If you are considering containerising your web applications or need help reviewing an existing Docker setup, it is worth preparing details about your current environment, the applications you want to run, and your deployment goals before reaching out for assistance.