EduFlow

Section 1 · Containers from First Principles

Running Containers: Ports, Env, Logs and Signals

18 min3 min readFill in the blanks

Lesson at a glance

Run notes-api in the background with ports, environment variables and resource limits, read its logs, get a shell inside it, and make it shut down cleanly on SIGTERM.

  1. 1Run it in the background
  2. 2See what’s running and what it’s saying
  3. 3Stopping: SIGTERM, then SIGKILL
  4. 4Restart policies and log growth
  5. 5Clean up

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Building an image is half the job; the other half is running it the way production will. Last year a Skylane service dropped a few hundred requests on every deploy. Nothing was wrong with the code. The process ignored the polite “please stop” signal and was killed mid-request ten seconds later. This lesson covers the runtime side, ending with that shutdown.

Run it in the background

Terminal
docker run -d --name notes \
  -p 8080:3000 \
  -e PORT=3000 \
  -e DATABASE_URL=postgres://notes:notes@host.docker.internal:5432/notes \
  --memory 512m --cpus 1 \
  notes-api:dev
  • -d detaches, so the container runs in the background and Docker prints its ID.
  • --name notes gives it a name you can type instead of the ID.
  • -p 8080:3000 maps host port 8080 to container port 3000. Read it as “outside:inside”. Two containers can both listen on 3000 inside, as long as they’re published on different host ports.
  • -e sets environment variables. Use --env-file .env.local when there are many.
  • --memory and --cpus set cgroup limits. If the process needs more memory than that (plus whatever swap Docker allows it on hosts that have swap), the kernel’s OOM killer ends it.

host.docker.internal resolves to your machine from inside a container on Docker Desktop; on Linux add --add-host=host.docker.internal:host-gateway. You’ll replace this with a proper Compose network in section 3.

See what’s running and what it’s saying

Terminal
docker ps                      # running containers
docker logs -f notes           # follow stdout/stderr
docker logs --since 10m notes  # recent output only
docker inspect notes --format '{{.State.Status}} {{.NetworkSettings.Ports}}'

docker logs shows whatever the process wrote to stdout and stderr. That’s the contract for containers: log to the standard streams, one line per event, and let the platform collect them. Writing to a log file inside the container hides logs from every tool and loses them when the container goes.

To look around inside:

Terminal
docker exec -it notes sh

exec starts an extra process in the running container; -it gives you an interactive terminal. It’s for looking, not for fixing. In section 2 you’ll move to a base image with no shell at all, and you’ll learn other ways in.

Stopping: SIGTERM, then SIGKILL

docker stop notes sends SIGTERM to PID 1 and waits 10 seconds (change it with -t). If the process is still alive, Docker sends SIGKILL, which can’t be caught. Exit codes tell you which happened:

Exit code Meaning
0 The app exited cleanly by itself
143 128 + 15: the process died from SIGTERM without handling it
137 128 + 9: killed by SIGKILL (stop timeout or out of memory)

Linux treats PID 1 specially: a signal the process hasn’t registered a handler for is ignored, instead of killing it. Node.js registers no SIGTERM handler by default, so node dist/server.js as PID 1 ignores docker stop and gets killed after 10 seconds. In-flight requests die with it.

Handle the signal: stop accepting connections, finish what’s in flight, close the database pool, exit.

src/server.ts
const server = app.listen(port, '0.0.0.0', () => console.log(`notes-api listening on ${port}`));

process.on('SIGTERM', () => {
  console.log('SIGTERM received, draining');
  server.close(async () => {
    await pool.end();
    process.exit(0);
  });
  // Safety net: don't hang forever on a stuck keep-alive connection
  setTimeout(() => process.exit(1), 8000).unref();
});

Now docker stop returns in well under a second and the container exits with 0. This handler matters even more in Kubernetes, which sends the same signal on every deploy, scale-down and node drain.

Restart policies and log growth

On a single server without an orchestrator, --restart unless-stopped tells Docker to bring the container back after a crash or a host reboot, unless you stopped it yourself. Kubernetes and Compose have their own restart settings, so you’ll rarely set this on a laptop, but it’s the first thing to check on that one old VM nobody wants to touch.

On long-lived Linux hosts, also watch log volume. The default json-file logging driver keeps everything a container prints unless you configure max-size and max-file in the daemon settings. A chatty container can fill a disk in a weekend; I’ve been paged for exactly that.

Clean up

Terminal
docker rm -f notes          # stop and remove
docker container prune      # remove all stopped containers
docker image prune          # remove dangling images
docker system df            # what's using disk

You can now build, run, observe and stop a container properly. Section 2 turns this image into one you’d be comfortable shipping: smaller, non-root, versioned and scanned.

Key takeaways

  • -p 8080:3000 maps host port 8080 to container port 3000; the left side is your machine, the right side is the app.
  • Pass configuration with -e or --env-file at run time so one image serves every environment.
  • Containers should log to stdout and stderr; docker logs and every orchestrator collect from there.
  • docker stop sends SIGTERM, waits 10 seconds, then sends SIGKILL; exit code 137 means the process was killed.
  • A Node.js process at PID 1 needs its own SIGTERM handler, or it ignores the signal and gets killed mid-request.

Further reading

Check your understanding

4 questions

Question 1 of 4

You run docker run -d -p 8080:3000 notes-api:dev. Which URL reaches the API from your laptop?

Question 2 of 4

docker stop notes takes exactly ten seconds and docker ps -a shows Exited (137). What happened?

Question 3 of 4

Where should notes-api write its request logs inside a container?

Question 4 of 4

A teammate wants to fix a typo in a config file inside the running production container with docker exec. What’s the problem?

Practice

Fill in the blanks

Day-to-day container commands

Fill in the blanks to run notes-api in the background on host port 8080, follow its logs, open a shell inside it, and stop it with a 30-second grace period.

# Start notes-api in the background, reachable at localhost:8080 docker run --name notes -p :3000 notes-api:dev # Follow its output docker -f notes # Open a shell inside the running container docker -it notes sh # Stop it, allowing 30 seconds to drain docker stop 30 notes