--- title: "How to View Docker Logs and Troubleshoot Containers" description: "Read and filter Docker container logs, set up log rotation, and diagnose containers that crash, run out of memory, cannot bind ports or fill the disk on Ubuntu." category: "Docker Kubernetes" language: en source: https://cubepath.com/docs/docker-kubernetes/docker-logs-and-troubleshooting updated: 2026-09-25 --- # How to View Docker Logs and Troubleshoot Containers When a container misbehaves, its logs and its state are the first places to look. Docker captures everything a container writes to standard output and standard error, and it records why a container stopped, which exit code it returned and whether the kernel killed it for using too much memory. In this tutorial you will read and filter container logs, configure log rotation so logs cannot fill the disk, and work through a repeatable method for diagnosing the most common container failures on Ubuntu 24.04. ## Prerequisites To follow this tutorial you need: - A server running Ubuntu 24.04 LTS, for example a CubePath VPS. - A non-root user with `sudo` privileges, added to the `docker` group. - Docker Engine installed from Docker's official repository. ## How Docker logging works Docker only collects what the main process of the container (PID 1) writes to **stdout** and **stderr**. An application that writes to a file inside the container, such as `/var/log/app.log`, produces no output in `docker logs`. Well-built images either log to stdout directly or, like the official Nginx image, symlink their log files to `/dev/stdout` and `/dev/stderr`. The **logging driver** decides where those streams go. The default is `json-file`, which stores one JSON line per message under `/var/lib/docker/containers//`. Check the driver in use: ```bash docker info --format '{{.LoggingDriver}}' ``` ```output json-file ``` ## Step 1 - Reading container logs Start a test container that serves a page and logs every request: ```bash docker run -d --name web -p 127.0.0.1:8080:80 nginx:alpine curl -s -o /dev/null http://127.0.0.1:8080 curl -s -o /dev/null http://127.0.0.1:8080/missing ``` Show all logs of the container: ```bash docker logs web ``` The output mixes Nginx's startup messages (on stderr) with the access log lines (on stdout): ```output /docker-entrypoint.sh: Configuration complete; ready for start up 172.17.0.1 - - [25/Sep/2026:10:02:11 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.5.0" "-" 172.17.0.1 - - [25/Sep/2026:10:02:12 +0000] "GET /missing HTTP/1.1" 404 153 "-" "curl/8.5.0" "-" ``` The flags you will use most: | Command | What it shows | |---|---| | `docker logs -f web` | Follow new lines as they arrive (Ctrl+C to stop) | | `docker logs --tail 100 web` | Only the last 100 lines | | `docker logs --since 10m web` | Lines from the last 10 minutes (also `2h` or a timestamp) | | `docker logs --since 2026-09-25T09:00:00 --until 2026-09-25T10:00:00 web` | A specific time window | | `docker logs -t web` | Prefix each line with Docker's timestamp | `docker logs` writes the container's stdout to your stdout and its stderr to your stderr, so to search both with `grep` you must merge them: ```bash docker logs web 2>&1 | grep ' 404 ' ``` ```output 172.17.0.1 - - [25/Sep/2026:10:02:12 +0000] "GET /missing HTTP/1.1" 404 153 "-" "curl/8.5.0" "-" ``` For a Compose project, `docker compose logs` works the same way and prefixes each line with the service name. Run it from the project directory: ```bash docker compose logs -f --tail 50 service_name ``` ## Step 2 - Configuring log rotation With the default configuration, `json-file` logs grow without limit. A chatty container can fill the disk and take every other service on the server down with it. Set limits globally in the Docker daemon configuration: ```bash sudo nano /etc/docker/daemon.json ``` ```json { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } } ``` Each container now keeps at most three files of 10 MB. If the file already exists with other settings, merge these keys into it rather than replacing it, and keep it valid JSON. Restart Docker to apply the change: ```bash sudo systemctl restart docker ``` > Important: Log options apply only to containers created **after** the change. Existing containers keep their old settings until you remove and recreate them (for Compose projects, `docker compose up -d --force-recreate`). Verify the settings on a newly created container: ```bash docker rm -f web docker run -d --name web -p 127.0.0.1:8080:80 nginx:alpine docker inspect --format '{{.HostConfig.LogConfig}}' web ``` ```output {json-file map[max-file:3 max-size:10m]} ``` You can also set limits for a single service in `compose.yaml`, which overrides the daemon defaults: ```yaml services: web: image: nginx:alpine logging: driver: json-file options: max-size: "10m" max-file: "3" ``` ### Sending logs to journald instead If you already collect system logs with journald, the `journald` driver sends container output there, where it is rotated by journald's own limits. Set `"log-driver": "journald"` in `daemon.json`, restart Docker and recreate the containers. `docker logs` keeps working, and you can also query by container name: ```bash journalctl CONTAINER_NAME=web --since "10 minutes ago" ``` ## Step 3 - Checking a container's state and exit code When a container is not running, `docker ps -a` shows how it ended: ```bash docker run -d --name broken alpine sh -c 'echo "starting"; exit 3' docker ps -a --filter name=broken ``` ```output CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a41c9e2f0d11 alpine "sh -c 'echo \"starti…" 5 seconds ago Exited (3) 4 seconds ago broken ``` For the full picture, query the state fields directly: ```bash docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}} error={{.State.Error}}' broken ``` ```output status=exited exit=3 oom=false restarts=0 error= ``` The exit code usually tells you where to look next: | Exit code | Meaning | Where to look | |---|---|---| | `0` | The main process finished normally | The command ends instead of staying in the foreground | | `1`, `2`, other small numbers | The application reported an error | `docker logs` | | `125` | Docker could not create or start the container | The error printed by `docker run` | | `126` / `127` | Command not executable / not found | `CMD` or `ENTRYPOINT` in the image, missing binary | | `137` | Killed with SIGKILL, often by the out-of-memory killer | `OOMKilled` field, memory limits | | `139` | Segmentation fault | Application or architecture mismatch | | `143` | Stopped with SIGTERM, for example by `docker stop` | Normally expected | Logs remain available after the container exits, so `docker logs broken` shows `starting`. Remove it with `docker rm broken`. ## Step 4 - Inspecting a running container Open a shell inside a running container to check files, environment variables or connectivity from its point of view. Minimal images such as Alpine have `sh` instead of `bash`: ```bash docker exec -it web sh ``` A few useful one-off commands without an interactive shell: ```bash docker exec web env docker exec web cat /etc/nginx/conf.d/default.conf docker top web ``` Many production images do not contain a shell or network tools at all. Instead of installing tools into them, attach a debugging container to the target's network namespace. The `nicolaka/netshoot` image bundles `curl`, `dig`, `ss`, `tcpdump` and more: ```bash docker run --rm -it --network container:web nicolaka/netshoot ``` Inside it, `localhost` is the `web` container, so `curl -I http://localhost` and `ss -ltn` show what that container sees. To copy a file out of a container, including a stopped one, use `docker cp`: ```bash docker cp web:/etc/nginx/nginx.conf ./nginx.conf ``` ### Watching resource usage and events `docker stats` shows live CPU, memory, network and disk I/O per container. Use `--no-stream` for a single snapshot: ```bash docker stats --no-stream ``` ```output CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS 5d2f8a7c1b33 web 0.00% 3.4MiB / 1.915GiB 0.17% 1.2kB / 1.4kB 0B / 4.1kB 3 ``` `docker events` streams what the daemon does: starts, stops, health status changes, OOM kills. It is the quickest way to catch a container stuck in a restart loop: ```bash docker events --since 30m --filter type=container --filter event=die ``` ## Step 5 - Reading the Docker daemon logs When the problem is Docker itself (containers fail to start with daemon errors, networks cannot be created, the service does not come up), read the daemon's logs from journald: ```bash sudo journalctl -u docker.service --since "1 hour ago" --no-pager ``` If `sudo systemctl restart docker` fails, the most common cause is a syntax error in `/etc/docker/daemon.json`. Validate it: ```bash sudo dockerd --validate --config-file=/etc/docker/daemon.json ``` ```output configuration OK ``` For more detail, add `"debug": true` to `daemon.json`, restart Docker, reproduce the problem, and remove the setting afterwards because debug logging is very verbose. ## Common problems and fixes ### The container exits immediately A container runs only as long as its main process. If `docker ps -a` shows `Exited (0)`, the command finished: the process daemonized itself (for example a service started with its default background mode) or the image's `CMD` was overridden with something short-lived. Run the process in the foreground, as the official images do (`nginx -g 'daemon off;'`). If the exit code is not 0, read `docker logs`: a missing environment variable or configuration file is the usual cause. To explore an image whose default command crashes, start it with a shell instead: ```bash docker run --rm -it --entrypoint sh image_name ``` ### The port is already in use ```text Bind for 0.0.0.0:80 failed: port is already allocated ``` Another container or a host process is using the port. Find out which: ```bash docker ps --filter publish=80 sudo ss -ltnp 'sport = :80' ``` Stop the conflicting service or publish your container on another host port (`-p 8081:80`). ### The container is killed with exit code 137 Check whether the kernel's out-of-memory killer stopped it: ```bash docker inspect --format '{{.State.OOMKilled}}' container_name sudo dmesg | grep -i 'killed process' ``` If `OOMKilled` is `true`, the container hit its `--memory` limit or the host ran out of RAM. Raise the limit, reduce the application's memory usage (worker count, cache sizes, JVM heap), or add memory to the server. A `137` with `OOMKilled=false` means something else sent SIGKILL, for example `docker kill` or a stop that exceeded the grace period. ### The disk is full Show how much space images, containers, volumes and the build cache use: ```bash docker system df ``` ```output TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 14 5 4.2GB 2.9GB (69%) Containers 6 5 120MB 8kB (0%) Local Volumes 4 3 1.1GB 240MB (21%) Build Cache 52 0 1.8GB 1.8GB ``` Remove stopped containers, unused networks, dangling images and build cache: ```bash docker system prune ``` Add `-a` to also remove every image not used by a container. Volumes are only removed with `--volumes`, so review them first. If `docker system df` looks small but `/var/lib/docker` is still large, find oversized container logs: ```bash sudo du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail -5 ``` That means log rotation is missing: apply Step 2 and recreate the affected containers. ### Containers cannot resolve DNS names Test resolution from a container on the same network: ```bash docker run --rm alpine nslookup cubepath.com ``` If this fails while `resolvectl query cubepath.com` works on the host, restart Docker so it re-reads the host's resolver configuration. As a last resort, set explicit servers in `daemon.json` with `"dns": ["1.1.1.1", "8.8.8.8"]`. For name resolution between containers, remember that it only works on user-defined networks, not on the default `bridge`. ## Conclusion You can now read and filter container logs, keep them from filling the disk with rotation, and follow a clear path when something breaks: check the state and exit code, read the logs, inspect the container from inside or through a debug container, and fall back to the daemon logs when Docker itself is the problem. As next steps, ship logs to a central system such as Loki or Graylog, add health checks to your images so `docker ps` reports real application health, and review our Docker networking guide to troubleshoot connectivity between containers.