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
dockerwithoutsudo(a member of thedockergroup). If you are not in that group, prefix every command withsudo.
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:
| Option | Purpose |
|---|---|
-d | Run in the background (detached) |
--name web | Name the container, so you do not need the ID |
-p 8080:80 | Publish container port 80 on host port 8080 |
-e KEY=value | Set an environment variable |
-v volume:/path | Mount a volume or host directory |
--rm | Delete the container when it exits |
-it | Interactive 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:
| Policy | Behavior |
|---|---|
no | Never restart (default) |
on-failure[:N] | Restart only on a non-zero exit code, optionally at most N times |
always | Always restart, including after a reboot, even if you stopped it manually before the reboot |
unless-stopped | Like 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
Dockerfileto package your own application as an image. - Describe the web server, database, volume and network from this tutorial in a single
compose.yamlfile and start them withdocker compose up -d. - Configure log rotation in
/etc/docker/daemon.jsonso container logs do not fill the disk.
