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.
- 1Run it in the background
- 2See what’s running and what it’s saying
- 3Stopping: SIGTERM, then SIGKILL
- 4Restart policies and log growth
- 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
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-ddetaches, so the container runs in the background and Docker prints its ID.--name notesgives it a name you can type instead of the ID.-p 8080:3000maps 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.-esets environment variables. Use--env-file .env.localwhen there are many.--memoryand--cpusset 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
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:
docker exec -it notes shexec 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.
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
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 diskYou 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:3000maps host port 8080 to container port 3000; the left side is your machine, the right side is the app.- Pass configuration with
-eor--env-fileat run time so one image serves every environment. - Containers should log to stdout and stderr;
docker logsand every orchestrator collect from there. docker stopsends 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.