...

How to Install and Configure Docker on Ubuntu 24.04/26.04 LTS

Martin Klein

Reading time 1 minute

In this guide, we will install Docker Engine on Ubuntu 24.04/26.04 LTS from Docker’s official repository, verify that the daemon is running, and launch a hello-world test container.

We will then set up Docker Compose, build a small Nginx project, configure Docker to work without requiring sudo every time, and verify that Docker and its containers automatically start again after the VPS reboots.

We will also cover the essential commands for day-to-day work and how to safely update Docker Engine and Compose. Finally, we will go through common errors involving the daemon, docker.sock, packages, the repository, ports, and automatic startup.

By the end, you will have a basic Docker host running on Ubuntu, ready to run containerized applications.

What, How, What For, and Why

Division of Responsibilities: What Docker, Containers, and VPSs Do

Today, we will explore how modern applications are run on servers and why an additional layer such as Docker is needed between a VPS and the application itself.

We will cover both theory and practice. Let’s start with the theory.

A VPS answers the question, “Where will the application run?” while Docker answers, “How exactly will we run it there?”

A traditional setup without Docker looks like this:

All components are installed directly on the operating system. This is a standard, practical approach, especially for a small server running a single, simple application.

Now suppose we have several applications, each with its own dependencies. Over time, system packages, libraries, and configurations begin to overlap, and updating one project may affect another.

Docker helps organize this environment by adding another layer:

Each container has its own filesystem environment, set of dependencies, and processes. Containers share the VPS’s Linux kernel, making them significantly more lightweight than full-fledged virtual machines. They can also run on virtual machines without requiring nested virtualization.

A container can be thought of as a separate runtime environment within a server. The application sees only the files, libraries, and settings prepared specifically for it.

For example, a single VPS can run all of the following simultaneously:

  • Project-a → Node.js 20
  • Project-b → Node.js 22
  • Project-c → Python 3.12

These versions do not conflict with one another because they run in separate containers.


Why Containers Are More Convenient Than Installing Applications Manually

For those who remain particularly skeptical, I have prepared a few more arguments in favor of containers.

Suppose a server initially runs a single project on Node.js 20. A few months later, a second project is added that requires Node.js 22. Then PostgreSQL at one version, Redis at another, and several additional system libraries are added.

Over time, the VPS becomes an environment in which different applications depend on the same system packages.

At some point, updating one dependency may unexpectedly affect another project. In the best-case scenario, you will encounter a startup error and quickly identify the incompatible package. In the worst-case scenario, the application will stop working after the update, with the cause buried somewhere among library versions, system packages, and configuration settings.

Then, instead of enjoying some well-earned rest, you will have to play Sherlock Holmes. You can look forward to the highly “entertaining” tasks of reviewing logs, rolling back packages, and so on.

For a production or commercial server, the consequences can be more serious: application downtime, the service becoming unavailable to users, and the urgent need to restore a working configuration.

Docker reduces this coupling. Instead of installing all dependencies directly in Ubuntu, each application gets its own environment. Updating components in one container should not alter another project’s environment, which means less time spent dealing with unexpected conflicts.

For example:

Updating the second project’s environment does not require changing the first project’s Node.js version.

Another advantage is reproducibility.

If the environment is defined in a Dockerfile or Compose configuration, the same project can be deployed on another server without manually repeating the entire lengthy installation process.

Instead of following instructions containing dozens of commands, a developer can use a sequence like this:

  • Docker compose pull
  • Docker compose up -d

Of course, Docker does not make infrastructure fully automated. You still need to configure networking, volumes, environment variables, backups, and security.

However, it provides much better separation between the application itself and the host operating system.

This is why containers are particularly convenient when a server runs multiple services or when a project is regularly moved between development, staging, and production environments.

Where Docker Engine and Docker Compose Fit In

Docker is not a single program that does everything, everywhere, all at once.

Docker Engine is the foundation. It creates and runs containers and manages images, networks, and volumes.

When we run: docker run nginx

Docker Engine retrieves the Nginx image, creates a container, and starts it.

This is much easier to visualize in a diagram:

This is perfectly sufficient for a single container.

Now imagine a project in which the following must be started in the correct order:

  • Application;
  • PostgreSQL;
  • Redis;
  • Nginx;
  • Separate networks;
  • Multiple volumes.

Launching each component with the lengthy command docker run is no longer convenient.

This is where Docker Compose comes in.

It allows you to define the entire project in a single YAML file, for example:

services:

app:

image: my-app

db:

image: postgres:17

redis:

image: redis:7

After that, the entire setup can be started with a single command: docker compose up -d

Compose reads the configuration and tells Docker Engine which containers, networks, and volumes to create.

In other words, Docker Engine does the work, while Docker Compose provides a convenient way to define multiple related components at once.

The following diagram should make this clearer:

Now that we have covered the theory, let’s move on to the most interesting part—practice.

In this guide, we will install both components: first Docker Engine itself, followed by the Compose plugin. We will then verify the installation using a simple container and build a small project to see the difference between a standard docker run and using Compose.

Preparing Ubuntu for Docker Installation

Before copying commands from the documentation and starting the installation, take a moment to check the server itself: verify that the Ubuntu version and architecture are supported and that sufficient resources are available.

This takes only a few minutes and helps identify some common issues in advance.

Is Your Ubuntu Version Supported?

At the time of writing, the official Docker Engine supports the following 64-bit versions:

  • Ubuntu 26.04 LTS;
  • Ubuntu 24.04 LTS;
  • Ubuntu 22.04 LTS.

For this article, we will use the first two: Ubuntu 24.04 LTS and Ubuntu 26.04 LTS. Both can be used to install Docker Engine from Docker’s official APT repository.

LTS releases are also particularly convenient to work with: they have a long support lifecycle, so you do not have to regularly migrate your infrastructure to a new OS release solely to receive system updates.

Checking the System Version and Architecture

First, let’s view the Ubuntu system information: cat /etc/os-release

On Ubuntu 24.04, the output will contain values similar to these:

NAME=”Ubuntu”

VERSION=”24.04 LTS (Noble Numbat)”

VERSION_ID=”24.04″

VERSION_CODENAME=noble

For Ubuntu 26.04, the corresponding codename will be resolute.

You can also check the version using a shorter command: lsb_release -a

If lsb_release is unavailable, there is no need to install it solely for this check— /etc/os-release already contains the required information.

Now let’s check the architecture: dpkg --print-architecture

On most standard VPS instances, the result will be: amd64

This is the standard 64-bit x86 architecture used by servers with Intel and AMD processors.

On ARM servers, you may see: arm64

This is a different 64-bit architecture designed for ARM processors.

Either option works for installing Docker: Docker Engine supports Ubuntu on both amd64and arm64.

The difference becomes apparent as soon as you start containers. Docker downloads prebuilt application images, and each image must be built for your server’s architecture. For example, an image built only for amd64 cannot simply be run in the usual way on arm64.

This is because the software in the image has already been compiled for a specific processor instruction set. A processor with a different architecture may not understand these instructions, so the container will not run correctly without a compatible build.

Popular images such as Nginx, PostgreSQL, and Redis generally do not have this issue because they are available for multiple architectures.

With that out of the way, you can now check the kernel version, available disk space, and amount of memory:

uname -r

df -h /

free -h

This allows us to identify the system we are working with before installation and determine whether the server has enough resources for at least the planned set of containers.

The basic check is complete. Now we need to decide, where exactly to install Docker from.

At first glance, this may not seem important: Ubuntu can install software using APT — its built-in package manager, which downloads software from dedicated storage locations known as repositories. Docker is available there as well, so you might think you can simply install the first package you find and move on. 

However, there is one caveat.

Why Install Docker from the Official Repository?

As discussed in the previous chapter, Docker is also available in Ubuntu’s standard repositories. This raises a reasonable question: why add another package source?

The reason is that a package from the distribution’s repository and the official Docker Engine may differ in terms of their source and update cycle.

Linux distributions may provide their own Docker packages, built and maintained by their maintainers. In other words, these may be different builds that differ from official Docker releases, lack the required plugins, or lag several versions behind.

Now let’s look at how to add Docker’s APT repository and install the following packages from it:

docker-ce

docker-ce-cli

containerd.io

docker-buildx-plugin

docker-compose-plugin

This ensures that Docker Engine, the CLI, Buildx, and Compose are installed from a single official source and can subsequently be updated through APT as usual.

There is also a shortcut—the official installer from get.docker.com. It can install Docker in just a few commands, but we recommend following the standard procedure to avoid problems.

Here is the diagram again to make this easier to understand and visualize:

Before adding the repository, however, you need to make sure that no packages that could conflict with the official installation remain on the VPS. We will address this in the next step.

Removing Anything That Could Interfere with Installation

Before adding a new package repository, check whether any old or alternative Docker components remain installed on the system.

This is especially relevant for a VPS that has been used before. On a fresh Ubuntu installation, the list will be empty. Therefore, if you have just rented a VPS and installed Ubuntu, you can skip this section.

Which Older Docker Packages May Conflict

Docker is not a single package. It consists of several components, including the engine itself, the command-line client, Compose, the container runtime, and additional plugins.

If Docker was previously installed using a different method, some of these components may still be present on the system.

Before installation, the official documentation recommends checking for the following packages:

  • docker.io
  • docker-compose
  • docker-compose-v2
  • docker-doc
  • docker-buildx
  • podman-docker
  • containerd
  • runc

There is no need to be intimidated by this list. We do not need to examine each package in detail—the key is to understand the main idea: older components may conflict with those installed by the official Docker Engine.

A good example is containerd.

This is one of the components directly involved in running containers. Installing Docker from the official repository provides the following package: containerd.io

It already includes a Docker-compatible version of containerd.

However, if the standalone containerdpackage was previously installed on the server, APT may detect two different variants of the same component. This can result in dependency conflicts, incompatible versions, or installation failure.

Therefore, it is easier to remove any older conflicting packages before installation.

How to Check for and Remove Conflicting Packages

First, check whether any of the listed packages are installed on the server:

 dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc

On a fresh system, most or all of these packages will not be installed.

If any conflicting components are found, you can remove them with the following command:

 sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc | cut -f1)

For beginners, I will explain what each part does, so that it is at least somewhat clear what we are entering on the command line.

Part one: dpkg --get-selections ...

This finds the installed packages from our list.

Second: cut -f1

This outputs only their names.

The result is then passed to: sudo apt remove

This allows APT to remove the conflicting packages it found.

In other words, this long command does something quite simple:

After removal, you can check for remaining packages: dpkg -l | grep -E 'docker|containerd|runc'

Once no conflicting packages remain, the system is ready for the installation of the official Docker Engine.

But this raises a natural question: if you remove the old Docker packages, will the containers and data be removed along with them?

What Happens to Existing Containers and Data

Fortunately, uninstalling the software package and deleting Docker data are separate operations.

Docker stores most of its operational data in the following directory: /var/lib/docker/

This data may include:

  • Containers;
  • Images;
  • Volumes;
  • Internal networks;
  • Docker system data.

The standard command sudo apt remove … removes installed packages but does not automatically delete the entire directory /var/lib/docker.

However, if you need to remove all containers completely, run:

sudo rm -rf /var/lib/docker

sudo rm -rf /var/lib/containerd

After verification, you can proceed with adding the official Docker repository.

Adding the official Docker repository

Install the required system packages

First, update the package index: sudo apt update

Now let’s install a few utilities required to securely connect the repository: sudo apt install -y ca-certificates curl

This is fairly straightforward.

Ca-certificates allows the system to trust HTTPS connections using certificates issued by known certificate authorities.

Curl is required to download Docker’s official key.

Create a directory for the APT keys: sudo install -m 0755 -d /etc/apt/keyrings

This is where Ubuntu will store the key that it will later use to verify the Docker packages.

Adding Docker’s GPG Key

Now download the official GPG key:

 sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc

And allow APT to read the file: sudo chmod a+r /etc/apt/keyrings/docker.asc

Why Is This Key Needed?

When Ubuntu downloads a package from an external repository, it needs to verify that the package was actually published by Docker and was not tampered with in transit.

In simplified terms, the process works as follows:

In other words, the GPG key is used to verify the origin of the packages.

If the key is missing or the signature does not match, APT will warn that it cannot verify the repository’s authenticity.

Adding the Docker APT Repository

We can now add the Docker repository itself.

Run:

echo \

“deb [arch=$(dpkg –print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \

$(. /etc/os-release && echo “${UBUNTU_CODENAME:-$VERSION_CODENAME}”) stable” | \

 sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

The command may look intimidating, but it does something fairly simple.

It automatically fills in:

  • The server architecture, such as amd64 or arm64, which we discussed earlier;
  • The codename of the current Ubuntu release;
  • The URL of the official Docker repository;
  • The path to the GPG key that APT should use to verify the packages.

For example, for Ubuntu 24.04 on a standard x86-64 VPS, the resulting entry will look roughly like this: deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable

In other words, we are telling APT: “For this Ubuntu version and architecture, use Docker’s official stable repository and verify its packages using the specified key.”

After adding the source, update the package index again: sudo apt update

APT will now retrieve package information from both the standard Ubuntu repositories and the Docker repository.

Verify That Ubuntu Can Find the Required Packages

Before installation, it is a good idea to verify that the new repository has been added successfully.

Let’s check the Docker Engine package: apt-cache policy docker-ce

The output should display the available version and repository URL: https://download.docker.com/linux/ubuntu

And the line Candidate should contain the version that APT proposes to install.

You can check the Compose plugin in a similar way: apt-cache policy docker-compose-plugin

If the packages are listed, Ubuntu has successfully completed the following steps:

  • Read the GPG key;
  • Added the official repository;
  • Downloaded its package index;
  • Found package versions compatible with our system.

If APT instead reports a signature error, an unknown repository, or cannot find docker-ce, do not proceed with the installation immediately. First, check the key path and the contents of docker.list, the Ubuntu codename, and then run the following command again: sudo apt update

Once docker-ce and docker-compose-plugin are listed among the available packages, the preparation is complete. You can now install Docker Engine itself.

Installing Docker Engine

What Exactly Is Installed with Docker

We will install several packages from the official repository:

  • Docker-ce
  • Docker-ce-cli
  • Containerd.io
  • Docker-buildx-plugin
  • Docker-compose-plugin

Let’s go through them one by one. Some of them have already appeared earlier in the article.

Docker-ce — Docker Engine itself, i.e., the core component of the system that manages containers.

Docker-ce-cli — the command-line client. It enables familiar commands such as:

docker ps

docker run

docker images

Containerd.io — a container runtime directly involved in running and managing containers at a lower level.

Docker-buildx-plugin extends the capabilities for building Docker images.

And docker-compose-plugin adds the modern command: docker compose

In other words, a single installation gives us both Docker Engine and Compose, which we will need a little later.

Install Docker Engine and containerd

Install the entire set with a single command:

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

APT will display a list of dependencies, download the packages from the configured Docker repository, and install them on the system.

Once the installation is complete, Docker usually starts automatically via systemd.

It looks something like this:

The Docker daemon is a background process that receives commands from the Docker client and performs the core operations: creating containers, downloading images, configuring networks, and managing volumes.

When you enter docker run nginx the command itself does not start the container directly. Docker CLI sends the request to the daemon, which then performs the necessary actions.

Checking the Installed Version

First, let’s check Docker itself: docker --version

The output will show the installed version in a format similar to this: Docker version 28.x.x, build …

The exact numbers will depend on the current release at the time of installation.

More detailed information can be obtained using the following command: docker version

It displays information about both sides:

  • Client
  • Server

Client is the Docker CLI that we use in the terminal.

Server — Docker Engine running in the background.

If the Server section is displayed correctly, the client was able to connect to the Docker daemon.

At this stage, you can also check Compose: docker compose version

If the plugin is installed correctly, the command will return its version.

Make sure the Docker daemon is running

Now let’s check the service status: sudo systemctl status docker --no-pager

Look for the following line: Active: active (running)

This means that the Docker daemon is running and ready to accept commands.

You can also run a shorter check: sudo systemctl is-active docker

Expected response: active

To check whether Docker is enabled to start automatically: sudo systemctl is-enabled docker

Typically, after installation, the result will be: enabled

We will return to automatic startup separately, because starting the Docker daemon itself and automatically starting specific containers are two different things.

If the service unexpectedly enters the failedstate, the first thing to do is check the logs: sudo journalctl -u docker --no-pager -n 50

You can usually find the reason why the daemon failed to start there.

If the status shows active (running), Docker Engine was installed successfully.

Running Your First Container

Docker Engine is installed, and the daemon is running. Now let’s test the entire workflow by asking Docker to download a prebuilt image, create a container from it, and run it.

The ideal choice for this is hello-world: a small test image that does virtually nothing except verify that Docker is installed and working properly.

What Happens When You Run docker run

Let’s start with the most important command: docker run

This command creates and starts a container from the specified image.

For example: docker run nginx

However, this short command performs several actions behind the scenes.

Docker:

  1. Checks whether the required image is available on the server;
  2. If the image is unavailable, pulls it from a registry;
  3. Creates a new container;
  4. Prepares its file system;
  5. Configures the network;
  6. Starts the process defined in the image.

In simplified terms:

It is important to distinguish between image and container.

A Docker image, or simply an image, is not a picture or a screenshot. It is a ready-made template for a container that already includes the required application files, libraries, dependencies, and basic configuration.

For example, the Nginx image contains everything Docker needs to quickly create a container with a running web server.

A container is a specific instance created from such an image.

Simply put:

  • Image = template
  • Container = a running copy of that template

A single image can be used to create multiple separate containers:

They are all based on the same image but are separate instances.

Running hello-world

Let’s move on. For the initial test, run: sudo docker run hello-world

For now, we’ll use sudo. We’ll configure Docker to run as a regular user a little later.

If the image hello-world is not yet on the VPS, Docker will first display: Unable to find image ‘hello-world:latest’ locally

This is not an error.

Instead, Docker is saying, “This image is not available locally, so I’ll try to download it.”

Docker will then pull the image and start the container.

The following message will appear in the successful output: Hello from Docker!

Below that, Docker briefly explains the sequence of actions it has just performed.

If you see Hello from Docker!, several components are working correctly:

  • The Docker CLI accepts commands;
  • The client connects to the Docker daemon;
  • The daemon can access a registry;
  • The image is downloaded successfully;
  • The container runtime can create and start a container.

This means that Docker is installed and can indeed create containers.

However, there is one caveat: after the hello-world command runs, the container exits almost immediately. Next, we will check where it ended up and how Docker displays containers that have already been created.

Checking the Created Container

After running hello-world you may encounter a small surprise.

Let’s try: sudo docker ps

The list may be empty. This is normal.

The command docker ps displays only running containers. The hello-world container prints a message and then exits immediately.

To also see stopped containers, add the -a option: sudo docker ps -a

A container based on the hello-world image should now appear in the list and its status should look something like this: Exited (0)

Code 0 means that the process completed successfully.

You can view the downloaded images separately: sudo docker images

The following will appear in the list: hello-world

The container has already stopped, but the image remains on the VPS and can be reused.

If you run the following command again: sudo docker run hello-world

Docker will not need to download the image again; it will use the local copy to create a new container.

Adding Docker Compose

Now that we’ve covered running a single container, let’s look at the second tool we’ll use going forward: Docker Compose.

We discussed its purpose at the beginning of the article: Compose helps manage multiple related containers through a single compose.yaml, instead of starting each one with a lengthy docker run command.

Checking Docker Compose

In our case, there is no need to install Compose separately. We installed the following package along with Docker Engine: docker-compose-plugin

This plugin adds the modern version of Docker Compose to the Docker CLI.

Let’s check the version: docker compose version

The output should include a line similar to this: Docker Compose version v2.x.x

If the package was omitted for any reason, you can install it separately:

sudo apt update

sudo apt install -y docker-compose-plugin

After that, run the following command again: docker compose version

Docker Compose or docker-compose: What’s the Difference?

You may sometimes encounter the command: docker-compose

With a hyphen.

This is the syntax of the legacy Docker Compose V1, which was distributed as a standalone tool.

The current Compose V2 works as a Docker CLI plugin, so the command now looks like this: docker compose

In other words, Compose became part of the standard Docker command structure:

  • docker run
  • docker ps
  • docker images
  • docker compose

Therefore, throughout the rest of the article, we will use the modern version: docker compose

Building Our First Mini-Project with Docker Compose

We’ve covered the theory behind Compose and verified that the plugin works. Now it’s time to put it to use.

For the first example, we won’t immediately set up a stack consisting of an application, a database, and Redis. We’ll use Nginx: it starts quickly, requires no complex configuration, and clearly demonstrates how compose.yamlworks.

The result will be a small project that we can start with a single command, check in a browser, stop, and restart without reconfiguring the container.

Creating the Project Directory

First, create a dedicated directory under /opt, where third-party applications and services are often installed on a server:

sudo mkdir -p /opt/docker-nginx sudo chown -R $USER:$USER /opt/docker-nginx cd /opt/docker-nginx

The second command gives the current user ownership of the directory, allowing you to create and edit the project files without having to use sudo each time.

This directory will contain the files for our small project.

You can check the current directory with the following command: pwd

The expected path is: /opt/docker-nginx

Docker itself does not require Compose projects to be stored in /opt. You can choose a different directory; the important thing is to keep each project’s configuration separate from other files. For server applications, /opt/<project-name> is a common and more organized option than placing the project directly in the user’s home directory.

Defining the service in compose.yaml

Now create the project’s main file: nano compose.yaml

Add the following:

services:

web:

image: nginx:stable

ports:

– “8080:80”

restart: unless-stopped

Let’s break down the configuration:

ParameterMeaningPurpose
servicesThe list of services in the projectThis section defines the containers that Compose should start
webThe name of the current serviceThe name is arbitrary. A larger project may also include app, db, redis, and other services
image: nginx:stableThe image used to create the containerIn this case, we use the official stable Nginx image
ports: “8080:80”Maps port 8080 on the VPS to port 80 in the containerMakes Nginx externally accessible through port 8080 on the VPS
restart: unless-stoppedThe automatic restart policyThe container will start again after Docker or the VPS restarts, unless it was previously stopped manually

The parameter ports is worth examining in a little more detail.

The request to VPS:8080 will be forwarded to: Nginx container:80

The container runs in its own network environment, so simply starting Nginx does not automatically make it externally accessible. We explicitly publish the required port.

As for restart: unless-stopped we will revisit it separately when we discuss automatically starting Docker and containers after a server reboot.

Start Nginx with a Single Command

Save compose.yaml and start the project: docker compose up -d

The parameter –d specifies detached mode—the container will continue running in the background, and the terminal will immediately return to the command prompt.

The first time you run Compose, it downloads the Nginx image if it is not already available on the VPS, creates the required resources, and starts the container.

Let’s check the project status: docker compose ps

The output should list the service web with the status Up and the published port: 0.0.0.0:8080->80/tcp

You can also check the container using a standard Docker command: docker ps

The only difference is that docker compose ps displays the containers in the current Compose project, while docker ps displays all running containers on the server. 

Testing the Application in a Browser

First, let’s test Nginx directly from the VPS: curl http://127.0.0.1:8080

If the container is running, the response will contain the HTML for the default Nginx page.

You can now open it in a browser: http://PUBLIC_IP:8080

Where PUBLIC_IP is the public IP address of your VPS.

The default page should appear: Welcome to nginx!

If the local curl request works but the page cannot be accessed from your computer, the issue is most likely not with Docker. Check the VPS firewall, the cloud provider’s Security Group, and whether TCP port 8080 is accessible.

Stopping and Restarting the Project

Now let’s explore one of the key benefits of Compose.

To stop and remove the containers for the current project, use the following command: docker compose down

Let’s check: docker compose ps

No services will be running.

Meanwhile, our compose.yaml file is still there. The entire project configuration remains in the file, so restarting it takes just one command: docker compose up -d

Compose will read the configuration again and recreate the service defined in it.

If you only need to stop the containers without removing them, you can use: docker compose stop

And then: docker compose start

The difference is straightforward:

  • Stop/start → stops and starts existing containers
  • Down/up → removes and recreates the project’s resources

I think you’ll agree that this is quite useful. Instead of having to remember the options used in the previous docker run command, we store the desired configuration in a single file and can reproduce it at any time.

Our first Compose project is ready. So far, however, we have either used sudo to work with Docker or relied on the current user’s permissions. Next, we will configure Docker to work properly from a standard user account.

Eliminating sudo from everyday use

This could be described as a form of workflow optimization. As mentioned earlier, many commands have to be run using sudo.

For example:

  • sudo docker ps
  • sudo docker images
  • sudo docker compose up -d

This is not an issue for a one-time check. However, if you use Docker regularly, typing sudo before every command is inconvenient.

Docker allows a regular user to access the daemon directly. This is done through the system docker group.

Why Docker Requires Elevated Privileges by Default

The Docker daemon manages containers, networks, volumes, and other system resources.

By default, its socket is located here: /var/run/docker.sock

Only root and users authorized to use Docker can access it.

When a regular user without the required permissions runs docker ps an error may occur, such as: permission denied while trying to connect to the Docker daemon socket

This indicates that the Docker CLI is installed and the daemon is running, but the current user does not have permission to access it.

Therefore, we previously used: sudo docker ps

sudo temporarily runs the command with elevated privileges, granting access to the Docker daemon.

However, instead of using sudo every time, you can grant the user the required permissions once.

Adding a User to the docker Group

Docker installation usually creates a system group named docker.

Let’s check: getent group docker

If the group exists, you will see a line similar to this: docker:x:999:

Now let’s add the current user: sudo usermod -aG docker $USER

For clarity, let’s break down the command in a table:

ComponentWhat It Does
usermodModifies the settings of an existing user
-aAppends the new value without removing the user from existing groups
-G dockerAdds the user to the supplementary docker group
$USERSubstitutes the name of the current user

The -a option is especially important here. If you use -G without -a, you may accidentally replace the user’s list of supplementary groups instead of adding a new one.

You can check the current groups with the following command: groups

However, immediately after usermod the group docker may not yet appear in the current SSH session.

There is no need to worry, as this is normal. The permissions have already been updated, but the current session is not yet aware of the change.

Apply the New Permissions

The easiest option is to exit the SSH session using the exit command and reconnect to the VPS.

After you log in again, the system will reload the user’s group membership.

If you do not want to reconnect, you can open a new shell session with the docker group: newgrp docker

After that, let’s check again: groups

The list should now include docker.

You can now verify that the daemon can be accessed without elevated privileges.

Checking docker ps without sudo

Run: docker ps

This time, sudo is not used.

If everything is configured correctly, Docker will display a list of running containers.

The Nginx container from the previous chapter should be listed, so the output will look something like this:

CONTAINER IDIMAGESTATUSPORTS
…nginx:stableUp …0.0.0.0:8080->80/tcp

You can also check Compose: docker compose ps

As you can see in the screenshot I included, it should also work without sudo.

If you see permission deniedagain instead of the container list, first check the current user’s groups: groups

If docker is not listed, the new permissions most likely have not yet been applied to the current SSH session. Reconnect to the server or run: newgrp docker

After that, try again: docker ps

If the docker group already exists, we need to investigate further. Let’s proceed step by step, starting by checking the permissions on the Docker socket: ls -l /var/run/docker.sock

The output usually looks something like this: srw-rw—- 1 root docker … /var/run/docker.sock

The important thing here is that the socket belongs to the docker group and that the group has read and write permissions.

If the permissions appear correct but the error persists, make sure the Docker daemon is running: sudo systemctl status docker --no-pager

Restart the service if necessary: sudo systemctl restart docker

Then run it again: docker ps

However, if /var/run/docker.sock has an unexpected owner or group, restarting Docker usually recreates the socket with the correct settings.

Important. Do not attempt to resolve the issue with a command such as: sudo chmod 666 /var/run/docker.sock

Although this may eliminate the permission denied error, we do not want to do this because it would also allow every local user on the server to control Docker.

And this brings us to an important point: access to the Docker daemon inherently grants extensive privileges. That is why we added the user to the docker group instead of simply making the socket accessible to everyone.

Let’s examine why this group needs to be treated with almost as much caution as root access.


Why Access to the docker Group Is Almost Equivalent to Root Access

You can now run commands without sudo.

However, there is an important caveat.

A user from the group docker is granted permission to control the Docker daemon directly. The Docker daemon, in turn, can run containers with extensive access to the system.

For example, you can allow a container to access directories on the VPS itself:

  • /home
  • /etc
  • /var

You can even mount the host’s system directories inside the container.

This creates the following chain of access:

In other words, a user who is not technically root can use Docker to access data and capabilities that are unavailable to a regular account.

This is why membership in the docker group effectively provides privileges close to root access. It is like giving a butler access to your home. The butler has a different role and does not own the house, but can still rummage through your dirty laundry.

This leads to a few simple rules:

  • Do not add arbitrary users to the docker group;
  • Do not grant this access to accounts that do not need it;
  • Do not treat the absence of sudo as an additional layer of protection;
  • On a multi-user server, decide in advance who actually needs access to Docker.

For a personal VPS or a server managed by a single administrator, this approach is perfectly reasonable.

On a shared system, however, membership in the docker group should be granted only to users who genuinely need it.

Docker commands can now be run without repeatedly using sudo.

The next logical question is: what happens after the VPS reboots? Will Docker start automatically, and will our containers come back up with it? We will address that next.

Configuring Docker to Survive a Reboot

So, let’s continue. We ended the previous chapter with a perfectly reasonable question: what happens if the VPS reboots?

Docker itself may start automatically, but that does not guarantee that all the containers will come back up with it. It is important to distinguish between two things:

  • Starting the Docker daemon automatically;
  • Starting specific containers automatically.

Essentially, the “manager” must start first, followed by the services it manages.

How Docker Works with systemd

On Ubuntu, Docker runs as a system service.

Its startup and status are monitored by systemd — the standard service management system for Linux.

We already used it to check Docker with the following command: sudo systemctl status docker

Put very simply, it works like this:

In other words, systemd does not manage each container directly. Its role is to start the Docker daemon itself.

Docker then determines which containers need to be brought back online.

Think of it as reopening a store after a power outage: systemd first powers up the building and its equipment, and Docker then determines which “departments” inside should start operating automatically.

Enabling the Docker daemon to start automatically

Let’s check whether Docker is enabled to start automatically: systemctl is-enabled docker

If the response is enabled, everything is already configured.

The next time Ubuntu boots, systemd will automatically start the Docker daemon.

If it displays disabled, instead, enable automatic startup: sudo systemctl enable docker

You can enable and start the service immediately with a single command: sudo systemctl enable --now docker

The option –now means not only enabling the service to start automatically, but also starting it immediately.

Verify the result:

systemctl is-enabled docker

systemctl is-active docker

Expected output:

enabled

active

Docker itself is now ready to survive a reboot.

Containers, however, are a little more complicated.

Why a Container Does Not Always Start with Docker

Suppose we have a running Nginx container.

We rebooted the VPS, and the Docker daemon successfully returned to the activestate, but the container remained stopped.

Why?

Because Docker does not assume that every container ever created should automatically start after a reboot.

A single server may host:

  • Production services;
  • Temporary test containers;
  • Stopped old projects;
  • Containers that the administrator intentionally stopped.

If Docker automatically started everything indiscriminately, a reboot could result in a highly unexpected set of running services or failures caused by conflicts over ports and other resources.

Therefore, the behavior is configured separately for each container using a restart policy.

We have already used one such setting in our compose.yaml: restart: unless-stopped

Now let’s examine what it means and what alternatives are available.

Understanding restart policies

The restart policy tells Docker under what conditions the container should be restarted.

Main options:

PolicyBehavior
noAutomatic restarts are disabled
on-failureThe container restarts if it exits with an error
alwaysDocker attempts to start the container again after it stops or the daemon restarts
unless-stoppedThe container restarts automatically unless the user has manually stopped it

For a typical web service, the following option is often convenient: restart: unless-stopped

This is the policy we specified for Nginx.

The logic is roughly as follows: “If the service crashes or the server reboots, bring it back up. But if I stopped it myself, I had a reason to do so.”

Example diagram:

What if you manually run this first: docker compose stop

Docker will remember that the container was stopped intentionally, and unless-stopped will not restart it just because the VPS rebooted.

With always the behavior is more persistent: Docker will attempt to bring the container back after daemon startup regardless of its previous state, except for the nuances of manually stopping it before the daemon itself is restarted.

For production services, the most common choices are unless-stopped or always, while on-failure is useful for tasks that should be restarted only after an abnormal termination.

Verifying automatic startup after a reboot

First, let’s make sure our Nginx instance is running: docker compose ps

Or: docker ps

The container must be in the following state: Up

Now let’s reboot the VPS: sudo reboot

The SSH connection will drop immediately—this is normal.

Wait for the server to boot back up, then reconnect to it via SSH.

First, let’s check Docker: systemctl is-active docker

Expected result: active

Now let’s inspect the container: docker ps

If the policy restart: unless-stopped takes effect, Nginx will once again have the status: Up

Finally, let’s test the service itself: curl http://127.0.0.1:8080

If the default Nginx page appears, the entire chain has recovered automatically:

Our Docker host now requires no manual intervention after a normal reboot.

However, knowing how to start containers is not enough for day-to-day work. You also need to be able to quickly check their status, view logs, access a container, and remove unused resources.

Next, we will cover this basic set of commands.

Docker Commands You Actually Need Every Day

How to View Containers

Let’s start with a command we’re already familiar with: docker ps

It shows only running containers.

The output typically includes:

  • Container ID;
  • Image;
  • Command;
  • Uptime;
  • Published ports;
  • Container name.

To list all containers, including stopped ones: docker ps -a

This is especially useful when a container seems to have disappeared after an error. Often, it has not disappeared at all—it has simply exited and therefore does not appear in the regular docker ps output.

If you only need the container IDs: docker ps -q

For a more compact view, you can use:

 docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"

This is more convenient when several services are already running on the server.

Checking downloaded images

Containers are created from images, so it is useful to periodically check which images have already been downloaded to the VPS.

To do this, use docker images or the more modern alternative: docker image ls

Both commands display roughly the same information:

REPOSITORYTAGIMAGE IDCREATEDSIZE
nginxstable………
hello-worldlatest………

Here you can see:

  • the image name;
  • the tag;
  • the ID;
  • the creation date;
  • the size.

If you have updated the project several times or experimented with different versions, old images may gradually take up disk space.

That is why docker image ls is one of those commands worth running from time to time.

Reading Logs and Finding Errors

If the container is running but the application is behaving unexpectedly, the logs are usually the first place to look.

Command: docker logs CONTAINER_NAME

For example, docker logs docker-nginx-web-1 will display the output of the application running inside the container.

If there are many logs, you can view only the most recent lines: docker logs --tail 50 CONTAINER_NAME

And to monitor them in real time: docker logs -f CONTAINER_NAME

The -f option works much like tail -f: new lines appear directly in the terminal as the application runs.

To exit this mode, press Ctrl + C.

This is one of the most useful Docker commands for troubleshooting.

If the application crashes after startup, docker logs will often reveal the cause: a configuration error, a missing environment variable, a database connection issue, or a port that is already in use.

Accessing a Running Container

Sometimes you need to inspect a container’s files or run a command directly inside it.

For this purpose, use: docker exec

For example: docker exec -it CONTAINER_NAME bash

The options are as follows:

  • -i keeps standard input open;
  • -t creates an interactive terminal.

This effectively opens a separate shell inside the container.

The command prompt will change, and commands will run in the container’s environment.

For example, ls will show the container’s file system, not the VPS’s.

However, not every image includes bash.

Minimalist images often use only: sh

The command will then be: docker exec -it CONTAINER_NAME sh

To return to the VPS, run the following command: exit

It is important to remember that a container is not a separate, full-fledged VM. Accessing it for diagnostic purposes is fine, but relying on ongoing manual changes is not recommended as the primary way to configure the application. Such changes may be lost when the container is recreated.

Configuration is best stored in a Dockerfile, compose.yaml, volumes, and other reproducible sources.

Stopping, Starting, and Restarting Containers

There are three main commands for managing an existing container.

Stop: docker stop CONTAINER_NAME

Start it again: docker start CONTAINER_NAME

Restart: docker restart CONTAINER_NAME

For example: docker restart docker-nginx-web-1

Incidentally, restart essentially stops and then starts the container again.

If the container does not respond to the standard stop command, there is another option: docker kill CONTAINER_NAME

However, you should not use it unless necessary.

Docker stop first gives the process time to shut down gracefully, whereas docker kill terminates it forcibly.

This is similar to the difference between shutting down a computer normally and unplugging it from the power outlet.

How to Remove an Unneeded Container or Image

A stopped container continues to exist and take up space.

You can remove it using the following command: docker rm CONTAINER_NAME

If the container is still running, Docker will generally not allow you to remove it in the usual way.

First, docker stop CONTAINER_NAME, and then: docker rm CONTAINER_NAME

As in the previous chapter, a force option is also available: docker rm -f CONTAINER_NAME

However, you should use this option only if you understand the consequences.

-f forcibly stops a running container and immediately removes it. This means that the process inside the container is not given sufficient time to shut down gracefully. Depending on the application, this may result in incomplete operations, lost recent changes, or data corruption.

Simply put:

  • docker stop → allows the application to shut down gracefully
  • docker rm → removes a container that has already been stopped
  • docker rm -f → forcibly stops the container and removes it immediately

For a test container such as hello-world, this is generally not a concern. However, with a database or a running application, it is best to use the standard stop command first.

The image is removed separately: docker rmi IMAGE_NAME

For example: docker rmi hello-world or docker image rm hello-world

If existing containers still depend on the image, Docker will warn you and will not remove it without additional steps.

You can periodically check for unused resources: docker system df

The command shows how much space is used by:

  • Images;
  • Containers;
  • Local volumes;
  • Build cache.

The command docker system prune can clean up some unused resources.

However, use this command with greater care: before confirming, always check exactly what Docker is going to remove.

Be especially careful with volumes, as they may contain actual application data.

Basic Docker Compose Commands

When a project is managed with Compose, it is more convenient to work with the services defined in compose.yaml rather than with individual containers.

Start the project: docker compose up -d

Check its status: docker compose ps

View the logs for all services: docker compose logs

Monitor the logs: docker compose logs -f

Stop containers without removing them: docker compose stop

Start them again: docker compose start

Restart: docker compose restart

To completely stop the project and remove the created Compose containers and networks: docker compose down

To apply the new configuration after modifying compose.yaml: docker compose up -d

Compose compares the current state against the project definition and recreates any changed services if necessary.

For day-to-day work, a very short cheat sheet is all you need:

CommandWhat It Does
docker psShows which containers are currently running
docker logsShows the logs for a container
docker execOpens a shell inside a running container
docker stop/startStarts/stops a container
docker restartRestarts a container
docker imagesShows downloaded Docker images
docker compose up -dStarts the Compose project in the background
docker compose psShows the status of services in the Compose project
docker compose logsShows service logs in real time
docker compose downStops the project and removes the containers and networks created by Compose

This set is sufficient for most basic tasks.

Updating Docker Safely

We have covered the basic commands. We can now check container status, view logs, restart a service, and remove unused resources.

However, like any other system component, Docker also needs to be updated from time to time.

The “see an update, install it immediately” approach is not always the best one, especially if a live project is already running on the VPS. A new version of Docker Engine may require restarting the daemon, which could also affect running containers.

Checking for Available Updates

First, update the local package index: sudo apt update

This command does not install anything yet. It only retrieves the latest package version information from the configured repositories, including Docker’s official repository.

You can now check whether updates are available for any Docker components:

 apt list --upgradable 2>/dev/null | grep -E 'docker|containerd'

If there are no matching lines, there is currently nothing to update.

You can also check the installed and available versions of the main packages:

 apt-cache policy docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

For each package, we are particularly interested in two lines:

  • Installed:
  • Candidate:

The first shows the version currently installed on the VPS. The second shows the version that APT will offer to install from the configured repositories.

For example, if they differ:

  • Installed: 5:28.x.x…
  • Candidate: 5:29.x.x…

This means that an update is available for Docker Engine.

Before proceeding, it is also useful to check what is currently running: docker ps

If a production application is already running on the server, you should also make sure you understand which containers could be affected by a potential Docker restart.

Now we know exactly what we are going to update. The next question is equally important: what should be backed up before we start modifying the live system.

What to Back Up Before Upgrading

Upgrading Docker itself generally does not delete your containers, images, or volumes.

However, this does not mean that you no longer need a backup.

The problem may not be the installation process itself. After the upgrade, the application may unexpectedly fail to start, or the new version may prove incompatible with the current configuration.

Therefore, before upgrading, you should back up at least everything needed to restore the project properly.

In our example, this primarily means compose.yaml

If the project uses additional files, back them up as well:

  • .env
  • Dockerfile
  • Nginx configuration files
  • application files

For example, you can simply copy the Compose file: cp compose.yaml compose.yaml.backup

However, configuration is only half the story.

If the container handles real persistent data, such as PostgreSQL, an up-to-date backup must be available before the update of the data itself.

The logic is as follows:

  • Project configuration → allows you to recreate the containers
  • Data backup → allows you to restore their contents

Important. One does not replace the other.

It is also useful to record the current Docker versions:

docker --version

docker compose version

And the container status: docker ps

If anything changes after the upgrade, this will provide a baseline for troubleshooting.

Once everything necessary has been backed up, you can proceed with the upgrade.

Updating Docker Engine and Compose

Because we installed Docker from the official APT repository, we can update it using the same package manager.

Let’s update the package index again: sudo apt update

Then update only the Docker components:

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

This way, we avoid updating the entire Ubuntu system and update only the Docker packages we need.

As a reminder, the following packages are updated:

PackagePurpose
docker-ceDocker Engine
docker-ce-cliDocker CLI
containerd.ioContainer runtime
docker-buildx-pluginBuildx
docker-compose-pluginDocker Compose

APT will display the versions it plans to install and prompt for confirmation if the command is run without -y.

The docker service may restart while Docker Engine is being updated. It is therefore best to perform the update at a time when brief application downtime will not cause any issues.

Once the update is complete, check the versions again:

docker --version

docker compose version

There is one caveat, however. New version numbers alone do not mean that the update was fully successful. We also need to verify that the daemon is still running and that all required containers continue to operate after the update.

Verifying Containers After the Update

Let’s start with the Docker daemon: systemctl is-active docker

Expected: active

For more detailed status information: sudo systemctl status docker --no-pager

Now let’s check the containers: docker ps

Our Nginx should be back in the Up state.

This is where the setting restart: unless-stopped that we added earlier comes in handy. If the Docker daemon restarted during the update, this policy allows the required container to resume operation automatically.

However, you should verify more than just whether the containers have started.

The application may be listed as Up but still not work correctly. Therefore, we will separately verify that Nginx is actually responding: curl http://127.0.0.1:8080

If the default Nginx page is returned as HTML, the entire chain is working:

For a real project, rather than performing a single check with curl you should test its core functionality: open the website, send an API request, or verify that the application can connect to the database.

Only then can the update be considered truly complete.

If any of these steps fails, proceed with diagnostics.

What to Do If Problems Occur After an Update

First, do not immediately delete containers or images or reinstall Docker.

First, you need to determine at what level the failure occurred. The solution may be much simpler than you think. 

If the Docker daemon itself is not running, check its status: sudo systemctl status docker --no-pager

Then check the logs: sudo journalctl -u docker --no-pager -n 100

If Docker is running, but a specific container has stopped: docker ps -a

The command will also display exited containers.

After that, check its logs: docker logs CONTAINER_NAME

For a Compose project:

docker compose ps

docker compose logs

This provides a step-by-step diagnostic workflow:

This makes it much easier to identify the cause.

If the problem appeared specifically after the update, also compare the new Docker version with the version you recorded before the update. For production systems, you should also review the release notes for the relevant release and check for changes that could affect the current configuration.

That covers safe updates. However, Docker issues do not occur only after a version change: the daemon may become unavailable, a user may lose access to the socket, a repository may return an error, or a running container may unexpectedly become inaccessible from a browser.

Finally, we will examine the most common issues and determine where to begin troubleshooting in each case.

If Docker Isn’t Working: Troubleshooting Common Errors

Docker daemon unavailable

One of the most common errors looks like this:

Cannot connect to the Docker daemon at unix:///var/run/docker.sock

Is the docker daemon running?

In this case, the Docker CLI is installed and the docker command is available, but the client cannot communicate with the daemon.

Let’s start with the obvious: systemctl is-active docker

If we get inactive or failed we will check the detailed status: sudo systemctl status docker --no-pager

Let’s try to start the service: sudo systemctl start docker

Let’s check again: docker ps

If Docker fails to start, check the systemd journal: sudo journalctl -u docker --no-pager -n 100

This is usually where the actual cause can be found: a configuration error, a dependency issue, or another system failure.

It is important to distinguish between two situations:

  • Cannot connect to the Docker daemon — the client cannot communicate properly with the daemon;
  • Permission denied — the daemon may be running, but the specific user is not allowed to access it.

We will address the second situation next.

Permission Denied When Accessing docker.sock

We already encountered this error in the chapter on working without sudo, so we will not repeat the entire docker group configuration process here.

If the command docker ps returns permission denied while trying to connect to the Docker daemon socket, first check whether the current user belongs to the docker group: groups

If docker is unavailable, add the user: sudo usermod -aG docker $USER

After that, either reconnect via SSH or run: newgrp docker

If the group already exists, check the socket permissions: ls -l /var/run/docker.sock

Typically, you should see something like this: srw-rw—- 1 root docker … /var/run/docker.sock

This means that the socket belongs to the docker group and that members of this group can access it.

If the permissions look correct but the error persists, let’s check the daemon itself: systemctl is-active docker

And, if necessary: sudo systemctl restart docker

Then run it again: docker ps

Docker or Compose Not Found

Sometimes the issue is even simpler.

If the terminal returns docker: command not found the shell could not find the Docker CLI.

First, let’s check whether the package is installed: dpkg -l | grep docker-ce-cli

And then: which docker

If Docker was installed according to our instructions, the command should normally be available in the standard system path.

If the package is missing, install the required packages again:

sudo apt update

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

Another scenario is that Docker is running, but the command docker compose version is not recognized.

Check the Compose plugin: dpkg -l | grep docker-compose-plugin

If it is missing:

sudo apt update

sudo apt install docker-compose-plugin

After that: docker compose version

Another common source of confusion is attempting to use docker-compose with a hyphen.

As we discussed earlier, this is the old Compose V1 syntax. Our configuration uses the modern Compose V2 syntax: docker compose, without a hyphen.

Package, GPG Key, and Repository Issues

If the error occurs during sudo apt update or while installing docker-ce, the problem usually lies not with the Docker daemon itself, but with APT.

For example, APT may report the following:

  • The repository signature could not be verified;
  • The GPG key is unavailable;
  • The repository does not contain a Release file;
  • The docker-ce package could not be found;
  • A dependency conflict was detected.

In this case, it is best not to proceed blindly with the installation.

First, let’s check the repository file: cat /etc/apt/sources.list.d/docker.list

Then check for the GPG key: ls -l /etc/apt/keyrings/docker.asc

And run it again: sudo apt update

If the Docker packages still cannot be found after this, check: apt-cache policy docker-ce

The output must include the official address: download.docker.com

If it is not present, APT is not using the Docker repository.

It is also worth revisiting the section on conflicting packages. Older docker.io, containerd, runc or alternative components may interfere with the installation of official packages.

You can check for them as follows: dpkg -l | grep -E 'docker|containerd|runc'

However, do not start removing everything indiscriminately. First, examine the specific APT error and use it to identify the conflicting package.

The container is running, but the service is unavailable

This is a particularly tricky situation.

Run: docker ps

The container is in the Up state, but the website still does not open in the browser.

In this case, the Up status itself only indicates that the container’s main process is running. It does not guaranteethat the application is externally accessible.

Let’s start by checking the service directly from the VPS: curl http://127.0.0.1:8080

If Nginx responds locally, the container and application are most likely working.

Then check the port mapping: docker ps

In our example, there should be an entry such as: 0.0.0.0:8080->80/tcp

This means that port 8080 on the VPS is mapped to port 80 in the container.

If the port is published and a local curl request works, but the service is inaccessible from a browser on an external computer, the cause is usually outside the container:

  • The firewall on the VPS;
  • The cloud provider’s Security Group;
  • A blocked external port;
  • An incorrect IP address or port in the address.

If there is no response even from curlhttp://127.0.0.1:8080, then check the logs: docker logs CONTAINER_NAME (or, for Compose: docker compose logs)

This gives us a simple flow:

Let’s move on.

Docker or containers do not start after a reboot

The final common issue is related to rebooting the VPS.

Here again, it is important to distinguish between the two levels discussed earlier:

First, check Docker itself:

systemctl is-enabled docker

systemctl is-active docker

The expected output is:

enabled

active

If Docker is not configured to start automatically: sudo systemctl enable --now docker

If the daemon failed to start after the reboot:

sudo systemctl status docker --no-pager

sudo journalctl -u docker --no-pager -n 100

However, suppose Docker is already active, but the container is still missing.

Let’s check all containers: docker ps -a

If the required container exists but is in the Exitedstate, the problem may be related to the restart policy.

For the Compose project, let’s check the configuration: restart: unless-stopped

And the status: docker compose ps

If the container was supposed to start automatically but exited again, check its logs: docker compose logs

Alternatively: docker logs CONTAINER_NAME

In other words, after a reboot, follow the same diagnostic sequence:

At this point, troubleshooting most common issues comes down to a simple, straightforward sequence of steps.

Final Check

We installed Docker Engine, verified Compose, launched test containers, configured Docker to run without sudo and start automatically after a reboot, and reviewed the essential commands.

Before finishing, let’s make sure everything works as expected:

What to CheckCommand
Docker daemon is runningsystemctl is-active docker
Docker is enabled to start automatically at bootsystemctl is-enabled docker
Docker CLI is availabledocker –version
Compose is installeddocker compose version
Containers can be viewed without sudo docker ps

For our test Compose project, let’s also check the service itself:

What to CheckExpected Result
docker compose psNginx is in the Up state 
curl http://127.0.0.1:8080The server returns the HTML for the Nginx page
VPS rebootDocker and Nginx start again automatically

If all these checks pass, the basic Docker setup is complete.

Conclusion

Our environment is now ready for regular use.

We installed Docker Engine and Compose from the official repository, launched our first container and Compose project, configured Docker to run without requiring sudo every time, and covered automatic startup, updates, basic commands, and common errors.

This is enough to use an Ubuntu VPS as a foundation for containerized projects. What comes next depends on the specific task: instead of the test Nginx instance, you can run an application, connect a database, Redis, a reverse proxy, and other services, defining them all in a single Compose project.

Thank you for your attention!

FAQ

Can Docker be installed on Ubuntu using the apt install docker.io command?

Yes, but this guide uses Docker’s official APT repository. This way, Docker Engine, the CLI, Buildx, and Compose are installed from a single official source and subsequently updated through APT. Docker’s official documentation also recommends removing conflicting distribution packages before installing Docker this way.

What is the difference between docker compose and docker-compose?

 docker compose is the modern version of Docker Compose that runs as a Docker CLI plugin. docker-compose refers to the separate standalone version and is now considered a legacy approach. For new Linux installations, it is best to use the Compose plugin.

Do I need to use sudo every time?

No. You can add the user to the docker system group, after which Docker commands can be run without sudo.

However, this convenience effectively grants the user root-level privileges, so only trusted accounts should be added to the docker group.

Does Docker start automatically after Ubuntu reboots?

On Ubuntu, the Docker service is usually configured to start automatically after installation. You can verify this with the following command: systemctl is-enabled docker

If necessary, enable automatic startup using: sudo systemctl enable docker

However, starting the Docker daemon and automatically starting specific containers are two different things. A restart policy must be configured separately for containers.

Why is the container in the Up state, but the website is inaccessible?

The Up status means that the main process inside the container is running. It does not guarantee that the application is accessible externally.

Check the published ports using: docker ps

Then try accessing the service directly from the VPS. If it responds locally but not externally, check the firewall, Security Group, and whether the required TCP port is accessible.

How do I verify that Docker is installed correctly?

At a minimum, run the following checks:

docker --version

docker compose version

systemctl is-active docker

docker run hello-world

If the Docker daemon is active, Compose displays its version, and hello-world completes successfully with a welcome message, the basic installation is working correctly.

Sources

  1. Docker Docs — Install Docker Engine on Ubuntu
  2. Docker Docs — Linux post-installation steps for Docker Engine
  3. Docker Docs — Install the Docker Compose plugin
  4. Docker Docs — Start containers automatically

Subscribe to our newsletter and receive articles and news

    Check out our other materials