Skip to content

Docker Logs and Exec: Inspect a Running Container

When a containerised application misbehaves, two commands answer most questions: docker logs ("what did the app print?") and docker exec ("let me look inside"). In this lab you'll use both on the Nginx container from the Nginx lab: read its access and error logs, open a shell inside it, explore its files, change the web page while it runs, and see why those changes don't last.

Make sure the container is running (docker ps should show my-nginx). If not: docker run -d --name my-nginx -p 8080:80 nginx. All outputs below are from a real run on Docker Engine 29.

Part 1: docker logs

Where container logs come from

Docker captures everything the container's main process writes to standard output (stdout) and standard error (stderr). The official Nginx image links its access.log to stdout and its error.log to stderr, so every request appears in docker logs. Your own apps should do the same: log to the console, not to files inside the container.

To create some log entries, open http://localhost:8080 a few times, and once a page that doesn't exist, such as http://localhost:8080/missing-page.

Show the last lines: --tail

docker logs --tail 4 my-nginx
Output
172.17.0.1 - - [04/Nov/2026:13:39:17 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.18.0" "-"
172.17.0.1 - - [04/Nov/2026:13:39:17 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.18.0" "-"
172.17.0.1 - - [04/Nov/2026:13:39:17 +0000] "GET /missing-page HTTP/1.1" 404 153 "-" "curl/8.18.0" "-"
2026/11/04 13:39:17 [error] 26#26: *5 open() "/usr/share/nginx/html/missing-page" failed (2: No such file or directory), client: 172.17.0.1, server: localhost, request: "GET /missing-page HTTP/1.1", host: "localhost:8080"

The first three lines are the access log (stdout): client IP, time, request, status code and size. The last is the error log (stderr), explaining the 404: the file doesn't exist in /usr/share/nginx/html. The client IP 172.17.0.1 is Docker's bridge gateway, because the request came in through the published port.

Add timestamps: -t

docker logs -t --tail 2 my-nginx
Output
2026-11-04T13:39:17.196569718Z 2026/11/04 13:39:17 [error] 26#26: *5 open() "/usr/share/nginx/html/missing-page" failed (2: No such file or directory), client: 172.17.0.1, ...
2026-11-04T13:39:17.196619821Z 172.17.0.1 - - [04/Nov/2026:13:39:17 +0000] "GET /missing-page HTTP/1.1" 404 153 "-" "curl/8.18.0" "-"

-t adds the exact time (UTC) at which Docker received each line. Useful when the application's own log lines have no timestamp.

Follow live: -f

docker logs -f --tail 1 my-nginx

The command keeps running and prints new lines as they arrive, like tail -f. While it was running, we opened http://localhost:8080/?from=follow, and the new line appeared immediately:

Output
172.17.0.1 - - [04/Nov/2026:13:47:25 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.18.0" "-"
172.17.0.1 - - [04/Nov/2026:13:49:05 +0000] "GET /?from=follow HTTP/1.1" 200 896 "-" "curl/8.18.0" "-"

Press Ctrl+C to stop following. This only stops the logs command; the container keeps running.

Logs by time: --since and --until

docker logs --since 10m my-nginx          # last 10 minutes
docker logs --since 2026-11-04T13:00:00 my-nginx
docker logs --since 1h --until 30m my-nginx
OptionUse it to
--tail Nshow only the last N lines (the full log can be huge)
-ffollow new lines live
-tadd Docker's timestamp to each line
--since / --untillimit by time: 10m, 2h or an exact date-time

Logs also work on a stopped container. When a container exits right after starting, docker logs <name> is the first thing to run: the error message is usually there.

Part 2: docker exec

docker exec runs an extra command inside a running container, next to its main process. It doesn't restart or change the main process.

Run one command

docker exec my-nginx nginx -v
docker exec my-nginx head -3 /etc/os-release
Output
nginx version: nginx/1.31.8
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
NAME="Debian GNU/Linux"
VERSION_ID="13"

The Nginx image is built on Debian 13. (The nginx:alpine variant used in the introduction is built on Alpine Linux instead; the tag decides the base.)

Open an interactive shell: -it

docker exec -it my-nginx sh
  • -i (interactive) keeps standard input open so you can type.
  • -t allocates a terminal (TTY) so you get a prompt and proper output.
  • sh is the program to run. Many images also have bash; minimal ones may have only sh, or no shell at all.

You're now inside the container. Here is the real session (the commands we typed, followed by their output):

whoami
hostname
pwd
cd /usr/share/nginx/html
ls -l
head -4 index.html
ps aux
Output
root
ec8160a79506
/
total 8
-rw-r--r-- 1 root root 497 Oct 21 14:24 50x.html
-rw-r--r-- 1 root root 896 Oct 21 14:24 index.html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
ps: not found
  • whoami: you are root inside this container.
  • hostname: the container's hostname is its short container ID.
  • ls -l: the website is just two files. index.html is the welcome page you saw on localhost:8080.
  • ps: not found: images contain only what the app needs. Even common tools like ps, curl or vi are often missing. That keeps images small and safer.

Without ps, you can still see the main process (PID 1) through /proc:

cat /proc/1/cmdline | tr "\0" " "
Output
nginx: master process nginx -g daemon off;

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

Inspect the Nginx configuration

docker exec my-nginx cat /etc/nginx/conf.d/default.conf
Output (comments removed)
server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

This explains everything you've seen: Nginx listens on port 80 (which is why we published -p 8080:80) and serves files from /usr/share/nginx/html.

Change the web page while the container runs

docker exec my-nginx sh -c "echo '<h1>Hello from MakeMyCodes inside Docker</h1>' > /usr/share/nginx/html/index.html"

Refresh http://localhost:8080:

Output
<h1>Hello from MakeMyCodes inside Docker</h1>

No restart was needed: Nginx reads the file on every request.

Part 3: copy files with docker cp

docker cp copies files between your computer and a container (running or stopped). Copy Nginx's main configuration out:

docker cp my-nginx:/etc/nginx/nginx.conf ./nginx.conf

And copy a new page in:

docker cp ./about.html my-nginx:/usr/share/nginx/html/about.html

Open http://localhost:8080/about.html:

Output
<h1>Page copied with docker cp</h1>

Part 4: see what changed, and why it doesn't last

docker diff lists the changes in the container's writable layer compared with the image (A = added, C = changed, D = deleted):

docker diff my-nginx
Output (selected lines)
C /usr/share/nginx/html
A /usr/share/nginx/html/about.html
C /usr/share/nginx/html/index.html
C /etc/nginx/conf.d/default.conf
A /run/nginx.pid
A /var/cache/nginx/client_temp
...

Our two page changes are there, together with files Nginx itself created at start-up. Now remove the container and create a fresh one from the same image:

docker rm -f my-nginx
docker run -d --name my-nginx -p 8080:80 nginx
Output
<h1>Welcome to nginx!</h1>
about.html -> HTTP 404

The original welcome page is back and about.html is gone. Changes made with exec or cp live only in that container's writable layer, and docker rm deleted it. That's why:

  • exec is for debugging and inspecting, not for deploying changes.
  • Application files belong in the image (a Dockerfile COPY, as in the next labs).
  • Data that must survive belongs in a volume or a bind mount.

Interview questions

What is the difference between docker exec and docker run?

docker run creates and starts a new container from an image. docker exec runs an additional command inside an existing, running container.

What do -i and -t mean in docker exec -it?

-i keeps standard input open (interactive); -t allocates a pseudo-terminal. Together they give you a usable shell prompt.

Where does docker logs get its data?

From the stdout and stderr of the container's main process, captured by Docker's logging driver (json-file by default). Files the app writes inside the container don't appear in docker logs.

Are changes made with docker exec kept?

They stay while the container exists (even across stop and start) but are lost when it's removed. Put permanent changes in the image or a volume.

Summary

  • docker logs shows stdout/stderr; use --tail, -f, -t and --since.
  • docker exec -it <name> sh opens a shell inside a running container; exit leaves without stopping it.
  • docker cp copies files in and out; docker diff shows what changed.
  • Changes inside a container disappear with docker rm: use images and volumes for anything permanent.

Next lab: Build an ASP.NET Core Docker image (dotnet-day1:1.0) and run it on port 8081.