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
amd64orarm64, 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:
- Checks whether the required image is available on the server;
- If the image is unavailable, pulls it from a registry;
- Creates a new container;
- Prepares its file system;
- Configures the network;
- 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:
| Parameter | Meaning | Purpose |
| services | The list of services in the project | This section defines the containers that Compose should start |
| web | The name of the current service | The name is arbitrary. A larger project may also include app, db, redis, and other services |
| image: nginx:stable | The image used to create the container | In this case, we use the official stable Nginx image |
| ports: “8080:80” | Maps port 8080 on the VPS to port 80 in the container | Makes Nginx externally accessible through port 8080 on the VPS |
| restart: unless-stopped | The automatic restart policy | The 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:
| Component | What It Does |
| usermod | Modifies the settings of an existing user |
| -a | Appends the new value without removing the user from existing groups |
| -G docker | Adds the user to the supplementary docker group |
| $USER | Substitutes 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 ID | IMAGE | STATUS | PORTS |
| … | nginx:stable | Up … | 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:
| Policy | Behavior |
| no | Automatic restarts are disabled |
| on-failure | The container restarts if it exits with an error |
| always | Docker attempts to start the container again after it stops or the daemon restarts |
| unless-stopped | The 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:
| REPOSITORY | TAG | IMAGE ID | CREATED | SIZE |
| nginx | stable | … | … | … |
hello-world | latest | … | … | … |
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:
| Command | What It Does |
| docker ps | Shows which containers are currently running |
| docker logs | Shows the logs for a container |
| docker exec | Opens a shell inside a running container |
| docker stop/start | Starts/stops a container |
| docker restart | Restarts a container |
| docker images | Shows downloaded Docker images |
| docker compose up -d | Starts the Compose project in the background |
| docker compose ps | Shows the status of services in the Compose project |
| docker compose logs | Shows service logs in real time |
| docker compose down | Stops 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:
| Package | Purpose |
docker-ce | Docker Engine |
docker-ce-cli | Docker CLI |
containerd.io | Container runtime |
docker-buildx-plugin | Buildx |
docker-compose-plugin | Docker 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 Check | Command |
| Docker daemon is running | systemctl is-active docker |
| Docker is enabled to start automatically at boot | systemctl is-enabled docker |
| Docker CLI is available | docker –version |
| Compose is installed | docker compose version |
| Containers can be viewed without sudo | docker ps |
For our test Compose project, let’s also check the service itself:
| What to Check | Expected Result |
| docker compose ps | Nginx is in the Up state |
| curl http://127.0.0.1:8080 | The server returns the HTML for the Nginx page |
| VPS reboot | Docker 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.
