Docker has dozens of commands, but day-to-day work uses a small set: run a container, see what is running, read its logs, get a shell inside it, and clean up afterwards. In this tutorial you will learn those commands hands-on by running an Nginx web server and a PostgreSQL database, attaching storage and a private network, setting resource limits and restart policies, and freeing disk space at the end. The examples use Ubuntu 24.04, but the Docker commands are identical on any Linux distribution.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 with Docker Engine installed from Docker's official repository, for example a CubePath VPS.
  • A non-root user who can run docker without sudo (a member of the docker group). If you are not in that group, prefix every command with sudo.

Check that the client can reach the daemon:

docker version --format '{{.Server.Version}}'
28.4.0

Step 1 - Running your first container

docker run creates a container from an image and starts it. If the image is not present locally, Docker pulls it from Docker Hub first. Start Nginx in the background (-d), give the container a name and publish container port 80 on port 8080 of the host:

docker run -d --name web -p 8080:80 nginx:1.29
Unable to find image 'nginx:1.29' locally
1.29: Pulling from library/nginx
...
Status: Downloaded newer image for nginx:1.29
6f1c2e9a0b7d4c3e8f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e

The long string is the container ID. Always use an explicit tag such as nginx:1.29 instead of the implicit latest, so you know exactly which version runs.

Confirm the web server answers:

curl -I http://localhost:8080
HTTP/1.1 200 OK
Server: nginx/1.29.1

The most common docker run options are:

OptionPurpose
-dRun in the background (detached)
--name webName the container, so you do not need the ID
-p 8080:80Publish container port 80 on host port 8080
-e KEY=valueSet an environment variable
-v volume:/pathMount a volume or host directory
--rmDelete the container when it exits
-itInteractive terminal, for shells and one-off tools

For a throwaway interactive container, combine --rm and -it. This starts a shell in an Ubuntu container and deletes it when you type exit:

docker run --rm -it ubuntu:24.04 bash

Step 2 - Listing and inspecting containers

List running containers:

docker ps
CONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS         PORTS                                     NAMES
6f1c2e9a0b7d   nginx:1.29   "/docker-entrypoint.…"   2 minutes ago   Up 2 minutes   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   web

Add -a to include stopped containers. That is where containers that crashed on startup appear:

docker ps -a

To show only the columns you care about, use --format:

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

docker inspect prints the full configuration and state of a container as JSON. Use a Go template to extract a single value, such as the container's IP address on its network:

docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web
172.17.0.2

Check live CPU and memory usage of all running containers once, without the continuous refresh:

docker stats --no-stream
CONTAINER ID   NAME   CPU %   MEM USAGE / LIMIT     MEM %   NET I/O          BLOCK I/O   PIDS
6f1c2e9a0b7d   web    0.00%   9.1MiB / 3.82GiB      0.23%   1.2kB / 1.05kB   0B / 4.1kB  5

Step 3 - Reading logs and running commands inside a container

Everything a container writes to stdout and stderr is available through docker logs. Show the last 20 lines with timestamps:

docker logs --tail 20 -t web
2026-09-25T10:20:14.112093551Z 172.17.0.1 - - [25/Sep/2026:10:20:14 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/8.5.0" "-"

Follow the log in real time with -f (press Ctrl+C to stop), or limit it to a time window with --since:

docker logs -f --since 10m web

docker exec runs a command in a container that is already running. Use it to check the Nginx configuration:

docker exec web nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Or open an interactive shell. Many images include bash; minimal images such as Alpine only have sh:

docker exec -it web bash

Type exit to leave. The container keeps running, because you only ended the extra shell process.

To copy files between the host and a container, use docker cp:

docker cp web:/etc/nginx/conf.d/default.conf ./default.conf

Step 4 - Stopping, starting and removing containers

Stop the container. Docker sends SIGTERM, waits 10 seconds for a clean shutdown, and then sends SIGKILL:

docker stop web

The container still exists with its configuration and filesystem; docker ps -a shows it as Exited. Start it again:

docker start web

Restart it in one step, for example after changing configuration inside it:

docker restart web

To delete a container, stop it first and then remove it, or force both in one command with -f:

docker rm -f web

Removing a container deletes its writable layer. Any data it wrote outside a volume is gone, which is why the next step matters.

Step 5 - Keeping data with volumes

Containers are meant to be disposable, so persistent data belongs in a volume. Create a named volume:

docker volume create pgdata

Run PostgreSQL with the volume mounted at its data directory. Replace your_strong_password with a real password:

docker run -d --name db \
  -e POSTGRES_PASSWORD=your_strong_password \
  -v pgdata:/var/lib/postgresql/data \
  postgres:17

The first start initializes the database, which takes a few seconds; docker logs db shows database system is ready to accept connections when it is done. Then create a table and a row to prove persistence:

docker exec -it db psql -U postgres -c "CREATE TABLE notes (t text); INSERT INTO notes VALUES ('still here');"

Now delete the container entirely and create a new one with the same volume:

docker rm -f db
docker run -d --name db \
  -e POSTGRES_PASSWORD=your_strong_password \
  -v pgdata:/var/lib/postgresql/data \
  postgres:17

Wait a few seconds for PostgreSQL to start, then query the table:

docker exec -it db psql -U postgres -c "SELECT * FROM notes;"
     t
------------
 still here
(1 row)

The data survived because it lives in the volume, not in the container. List volumes and see where Docker stores one on disk:

docker volume ls
docker volume inspect -f '{{.Mountpoint}}' pgdata
/var/lib/docker/volumes/pgdata/_data

For configuration files or website content you edit on the host, use a bind mount instead: -v /srv/site:/usr/share/nginx/html:ro mounts a host directory, and :ro makes it read-only inside the container.

Step 6 - Connecting containers with a network

Containers on the default bridge network cannot find each other by name. A user-defined network adds built-in DNS, so containers reach each other by container name. Create a network:

docker network create appnet

Connect the running database to it:

docker network connect appnet db

Now start a temporary container on the same network and check that it reaches the database by the name db, using pg_isready from the PostgreSQL image:

docker run --rm --network appnet postgres:17 pg_isready -h db
db:5432 - accepting connections

Notice that the database never published a port with -p. It is reachable only from containers on appnet, which is exactly what you want for a database. Inspect which containers belong to a network with:

docker network inspect -f '{{range .Containers}}{{.Name}} {{end}}' appnet
db

Step 7 - Setting restart policies and resource limits

By default a container stays stopped after it crashes or the server reboots. A restart policy tells Docker what to do:

PolicyBehavior
noNever restart (default)
on-failure[:N]Restart only on a non-zero exit code, optionally at most N times
alwaysAlways restart, including after a reboot, even if you stopped it manually before the reboot
unless-stoppedLike always, but stays stopped if you stopped it manually

unless-stopped is the usual choice for long-running services. You can also cap memory and CPU so one container cannot starve the others. Run Nginx again with both:

docker run -d --name web --restart unless-stopped \
  --memory 256m --cpus 0.5 \
  -p 8080:80 nginx:1.29

Check the limits Docker applied:

docker inspect -f 'restart={{.HostConfig.RestartPolicy.Name}} memory={{.HostConfig.Memory}} nanocpus={{.HostConfig.NanoCpus}}' web
restart=unless-stopped memory=268435456 nanocpus=500000000

You can change both on a running container without recreating it:

docker update --restart on-failure:5 --memory 512m --memory-swap 512m web

A container that exceeds its memory limit is killed by the kernel. You can tell by its state:

docker inspect -f 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' web

Exit code 137 with oom=true means the container ran out of memory; 143 means it was stopped with SIGTERM, and 1 usually means the application itself failed (check docker logs).

Step 8 - Managing images

List the images stored locally:

docker images
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
postgres     17        a1b2c3d4e5f6   2 weeks ago   438MB
nginx        1.29      0f1e2d3c4b5a   3 weeks ago   192MB
ubuntu       24.04     9a8b7c6d5e4f   4 weeks ago   78.1MB
hello-world  latest    74cc54e27dc4   8 months ago  10.1kB

Download an image without running it, for example to update a tag to its newest build before recreating a container:

docker pull nginx:1.29

Give an image an additional name, which is how you prepare it for pushing to a private registry:

docker tag nginx:1.29 registry.example.com/web/nginx:1.29

Remove an image you no longer need. Docker refuses if a container (even a stopped one) still uses it:

docker rmi ubuntu:24.04

Step 9 - Cleaning up disk space

Images, stopped containers, unused volumes and build cache accumulate quickly. See what uses space:

docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          4         2         708.2MB   78.1MB (11%)
Containers      2         2         1.1kB     0B (0%)
Local Volumes   1         1         47.6MB    0B (0%)
Build Cache     0         0         0B        0B

Remove stopped containers, unused networks, dangling images and build cache in one command:

docker system prune

Docker lists what it is going to delete and asks for confirmation. To also remove every image not used by a container, add -a. Volumes are only removed if you add --volumes, and that deletes the data in any volume not attached to a container, so use it with care.

Finally, remove what you created in this tutorial:

docker rm -f web db
docker volume rm pgdata
docker network rm appnet

Troubleshooting

The container exits right after docker run -d: run docker ps -a to see the exit code, then docker logs name to see why. A common cause is a missing required environment variable, such as POSTGRES_PASSWORD for the PostgreSQL image.

Conflict. The container name "/web" is already in use: a stopped container with that name still exists. Remove it with docker rm web or choose another name.

port is already allocated: another container or host process uses that host port. Find it with docker ps or sudo ss -tlnp | grep :8080.

Containers cannot reach each other by name: they are on the default bridge network. Put them on the same user-defined network as in Step 6.

exec: "bash": executable file not found: the image has no bash. Use docker exec -it name sh.

Conclusion

You have run, inspected, stopped and removed containers, read their logs, opened shells inside them, kept data in a volume, connected containers over a private network, and applied restart policies, resource limits and cleanup. These commands cover most everyday container work.

As next steps, you can:

  • Write a Dockerfile to package your own application as an image.
  • Describe the web server, database, volume and network from this tutorial in a single compose.yaml file and start them with docker compose up -d.
  • Configure log rotation in /etc/docker/daemon.json so container logs do not fill the disk.