| |

Docker 7 🐳 Managing Container Lifecycles: Pause, Unpause, Restart, and Signal Handling (SIGTERM, SIGKILL)

A container’s lifecycle is not limited to starting and stopping. Docker provides operations for pausing a running container without terminating it, resuming a paused container, restarting a container with a fresh process, and controlling the signals that govern how a container’s main process terminates. These operations matter because they determine what happens to in-flight work, open connections, and state when a container’s life is interrupted. The difference between a graceful shutdown and an abrupt kill is the difference between a clean database commit and a corrupted file.

Signal handling is where this becomes concrete. When Docker stops a container, it sends SIGTERM to the process with PID 1 inside the container. If that process does not exit within a grace period, Docker sends SIGKILL, which cannot be caught and terminates the process immediately. Whether the application handles SIGTERM determines whether it can flush buffers, close connections, and save state before dying. Many containers run shells or wrappers that do not forward signals, causing the application to never receive SIGTERM and to be killed after the timeout. This is one of the most common operational problems with Dockerized applications.

This chapter covers pause and unpause, restart policies and the restart command, the SIGTERM/SIGKILL sequence, PID 1 and signal forwarding, the --init flag, stop signals and timeouts, and the patterns for building containers that shut down gracefully.

Key point: docker pause freezes a container’s processes using the cgroup freezer, and docker unpause resumes them. docker stop sends SIGTERM (or a custom signal) to PID 1, waits a grace period (default 10 seconds), then sends SIGKILL. The container’s main process must handle SIGTERM and forward it to children for graceful shutdown to work.


Why lifecycle management matters

The data-integrity problem. A database that receives SIGKILL mid-transaction may leave its data files inconsistent. An application that is writing a log or a file may truncate it. A message queue consumer that is processing a message may lose it. Graceful shutdown allows the application to complete the current operation, roll back partial work, and release resources before exiting.

The availability problem. When a container stops ungracefully, in-flight requests fail and connections drop. In a load-balanced environment, a container that shuts down gracefully can finish its current requests while the load balancer drains it, so no client sees an error.

The restart-loop problem. A container with a restart policy that keeps crashing will restart repeatedly. Understanding why it exits, whether the exit is clean, and what the exit code means is the difference between diagnosing a misconfiguration and watching the container restart forever.

The signal-forwarding problem. Containers run their main process as PID 1. PID 1 has special semantics in Linux: it does not receive default signal handling, and it is responsible for reaping orphaned child processes. If the main process is a shell script that spawns the application as a child, the shell may not forward SIGTERM, and the application never learns that it should shut down. This is why docker stop sometimes results in a SIGKILL after the timeout.

The resource-freezing problem. Pausing a container is not the same as stopping it. A paused container keeps its memory, open files, and network connections but is frozen by the kernel. This is useful for temporarily suspending a workload without losing its state, but it must be paired with unpause to resume.


a. Pausing and unpausing containers

docker pause suspends all processes in a container using the cgroup freezer. The processes are stopped immediately, and their state is preserved in memory.

docker pause web

The container remains in the docker ps list with the status Up (Paused). Its memory is retained, its network connections remain open, and its process tree is frozen. The container is not consuming CPU while paused.

docker unpause resumes a paused container:

docker unpause web

The processes continue from where they stopped. Because the pause is at the kernel level, the application does not need any special handling; it experiences the pause as if time had stopped.

Pausing is useful for temporarily freeing CPU without losing state, for coordinating multi-container operations, and for debugging. It is not a substitute for stopping: a paused container still holds its memory, its port bindings, and its volumes.

A paused container cannot be stopped or removed directly. It must be unpaused first, or the command must be forced. This is because the container’s processes are frozen and cannot receive the signals that stop and remove would send.


b. Restart policies and the restart command

A restart policy tells Docker what to do when a container’s main process exits. The policy is set at container creation with --restart.

PolicyBehavior
noNever restart (default)
on-failure[:max]Restart only on non-zero exit, up to max attempts
alwaysAlways restart, including on daemon restart
unless-stoppedAlways restart unless explicitly stopped

The on-failure policy restarts the container when the process exits with a non-zero code. This is useful for services that crash and should be retried. The optional :max limits the number of retries, after which the container stays stopped.

docker run -d --restart on-failure:5 --name web nginx

The always policy restarts the container whenever it exits, including when the Docker daemon restarts. This is the aggressive option; a container that crashes on startup will restart forever.

docker run -d --restart always --name web nginx

The unless-stopped policy is like always but does not restart a container that was explicitly stopped with docker stop. This is often the better choice for production services because it respects an operator’s decision to stop the container.

docker run -d --restart unless-stopped --name web nginx

The docker restart command stops and starts a container in one operation:

docker restart web

The command sends SIGTERM, waits for the grace period, then starts the container again. The -t flag sets the timeout:

docker restart -t 30 web

A restart is useful for applying configuration changes that are read at startup, or for recovering from a transient failure.

The restart policy can be changed on a running container with docker update:

docker update --restart unless-stopped web

This is useful for adopting a restart policy on containers that were created without one.


c. The SIGTERM and SIGKILL sequence

When docker stop is called, Docker sends a signal to the container’s main process. The default signal is SIGTERM, which is a request to terminate gracefully. The process can catch it, perform cleanup, and exit on its own schedule.

docker stop web

After sending SIGTERM, Docker waits for a grace period. The default is 10 seconds, configurable with -t:

docker stop -t 60 web

If the process has not exited when the grace period expires, Docker sends SIGKILL:

SIGTERM → wait (10 seconds) → SIGKILL → wait → container stops

SIGKILL cannot be caught, blocked, or ignored. The process is terminated immediately by the kernel, with no opportunity for cleanup. This is the last resort.

The sequence is the same for docker restart and for the termination phase of docker rm -f, except that docker rm -f may skip the grace period depending on the version.

A container that takes 10 seconds to stop is a container whose main process does not handle SIGTERM. A container that stops immediately is one that does. The exit code reflects what happened: exit code 0 means a clean exit, 143 means terminated by SIGTERM (128 + 15), and 137 means terminated by SIGKILL (128 + 9).

docker ps -a --format "table {{.Names}}\t{{.Status}}"
# web   Exited (0) 2 minutes ago        → clean exit
# api   Exited (143) 2 minutes ago      → SIGTERM, handled or default
# db    Exited (137) 2 minutes ago      → SIGKILL, forced

d. PID 1, signal forwarding, and the init process

Inside a container, the main process runs as PID 1. PID 1 has special semantics: the kernel does not apply default signal handling to it, so a PID 1 process that does not explicitly install a handler for a signal will ignore it.

This matters for shell scripts and wrappers. A common Dockerfile pattern is:

CMD /app/start.sh

where start.sh runs the actual application:

#!/bin/sh
nginx -g 'daemon off;'

When Docker sends SIGTERM, it goes to the shell script (PID 1), not to nginx. The shell does not forward the signal, so nginx never learns to shut down. After the timeout, Docker sends SIGKILL, and the container dies abruptly.

The fix is either to run the application directly as PID 1:

CMD ["nginx", "-g", "daemon off;"]

or to use the exec form in a shell script:

#!/bin/sh
exec nginx -g 'daemon off;'

The exec replaces the shell process with nginx, so nginx becomes PID 1 and receives the signals.

For cases where a wrapper is necessary, Docker provides the --init flag, which inserts a tiny init process (tini) as PID 1:

docker run -d --init --name web nginx

The init process forwards signals to the child and reaps orphaned processes. This is the recommended approach when the application cannot be run directly as PID 1.


e. Custom stop signals and timeouts

Some applications expect a different signal. A Node.js application might listen for SIGINT. A process might expect SIGHUP to reload configuration. The --stop-signal flag sets the signal that docker stop sends:

docker run -d --stop-signal SIGINT --name app myapp

The signal can be a name (SIGINT, SIGTERM) or a number (2, 15). The docker stop -s flag overrides it for a single stop:

docker stop -s SIGHUP web

The --stop-timeout flag sets the default grace period for a container:

docker run -d --stop-timeout 30 --name db postgres

The -t flag on docker stop overrides it for a single operation:

docker stop -t 5 web

A short timeout is appropriate for containers that shut down quickly; a long timeout is appropriate for databases and services that need time to flush data.


Complete Example Session

# ============================================
# PART 1: RUN A CONTAINER WITH A RESTART POLICY
# ============================================
docker run -d --name web --restart unless-stopped -p 8080:80 nginx
# ============================================
# PART 2: PAUSE THE CONTAINER
# ============================================
docker pause web
docker ps --filter name=web
# Status: Up X seconds (Paused)
# ============================================
# PART 3: UNPAUSE THE CONTAINER
# ============================================
docker unpause web
docker ps --filter name=web
# Status: Up X seconds
# ============================================
# PART 4: RESTART THE CONTAINER
# ============================================
docker restart web
# ============================================
# PART 5: INSPECT THE RESTART POLICY
# ============================================
docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' web
# unless-stopped
# ============================================
# PART 6: CHANGE THE RESTART POLICY
# ============================================
docker update --restart always web
docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' web
# always
# ============================================
# PART 7: STOP WITH A CUSTOM SIGNAL
# ============================================
docker stop -s SIGINT web
# ============================================
# PART 8: STOP WITH A TIMEOUT
# ============================================
docker start web
docker stop -t 30 web
# ============================================
# PART 9: RUN WITH INIT AND STOP SIGNAL
# ============================================
docker run -d --name app --init --stop-signal SIGINT --stop-timeout 20 myapp
# ============================================
# PART 10: CHECK EXIT CODE
# ============================================
docker ps -a --format "table {{.Names}}\t{{.Status}}"
# web   Exited (0) 2 minutes ago
# app   Exited (143) 1 minute ago

These ten parts cover restart policies, pause and unpause, restart, inspecting and changing policies, custom stop signals, timeouts, the --init flag, and reading exit codes.


Quick Reference

Lifecycle Commands

CommandPurpose
docker pauseFreeze container processes
docker unpauseResume frozen processes
docker restartStop and start
docker stopGraceful shutdown
docker killImmediate SIGKILL
docker updateChange restart policy

Restart Policies

PolicyRestarts On
noNever
on-failure[:n]Non-zero exit, up to n times
alwaysAny exit, including daemon restart
unless-stoppedAny exit except explicit stop

Signal Sequence

StepSignalEffect
1SIGTERM (default)Request graceful shutdown
2Grace period (10s default)Wait for process to exit
3SIGKILLForce termination
4Container stopsExit code recorded

Exit Codes

CodeMeaning
0Clean exit
137SIGKILL (128 + 9)
143SIGTERM (128 + 15)
1General error
126Command not executable
127Command not found

Signal Flags

FlagPurpose
--stop-signalSignal sent by docker stop
--stop-timeoutGrace period before SIGKILL
docker stop -sOverride signal for one stop
docker stop -tOverride timeout for one stop
--initInsert tini as PID 1

Best Practices

✅ Do This:

docker run -d --restart unless-stopped --name web nginx    # Respect explicit stop
docker run -d --init --name app myapp                       # Signal forwarding
CMD ["nginx", "-g", "daemon off;"]                          # Exec form: app is PID 1
docker stop -t 30 db                                        # Long timeout for DB
docker update --restart always web                          # Change policy

❌ Don’t Do This:

CMD /app/start.sh                                           # ❌ Shell as PID 1
docker stop -t 0 web                                        # ❌ No grace period
docker kill web                                             # ❌ Skips graceful shutdown
docker run --restart always --name db postgres              # ❌ Restarts even after stop

Common Pitfalls

PitfallWhy It HappensFix
Container takes 10s to stopPID 1 does not handle SIGTERMUse exec form or --init
Exit code 137Process was SIGKILLedHandle SIGTERM; increase timeout
Container restarts after stopalways policyUse unless-stopped
Cannot remove paused containerProcesses frozendocker unpause first
Data lost on restartProcess killed before flushHandle SIGTERM; increase timeout
Shell script ignores signalShell is PID 1Use exec or --init

Real-World Examples

1. Database with Long Timeout

docker run -d --name db --stop-timeout 60 postgres

2. Web Server with Restart Policy

docker run -d --name web --restart unless-stopped nginx

3. Application with Init

docker run -d --init --name app myapp

4. Custom Signal for Node.js

docker run -d --stop-signal SIGINT --name node node:20

5. Pause for Maintenance

docker pause web
# perform maintenance
docker unpause web

6. Change Policy on Running Container

docker update --restart on-failure:3 api

7. Graceful Stop with Timeout

docker stop -t 45 db

8. Immediate Kill

docker kill -s SIGKILL web

9. Restart with Custom Timeout

docker restart -t 20 web

10. Exec Form in Dockerfile

CMD ["node", "server.js"]

Visual

Signal Sequence on docker stop

┌──────────────────────────────────────────────────────────────┐
│  docker stop web                                             │
│       │                                                      │
│       ▼                                                      │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  SIGTERM sent to PID 1                                 │  │
│  └────────────────────────┬───────────────────────────────┘  │
│                           │                                   │
│                           ▼                                   │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  Process handles signal:                               │  │
│  │  - flush buffers                                       │  │
│  │  - close connections                                   │  │
│  │  - release resources                                   │  │
│  │  - exit(0)                                             │  │
│  └────────────────────────┬───────────────────────────────┘  │
│                           │                                   │
│              ┌────────────┴────────────┐                      │
│              ▼                         ▼                      │
│  ┌────────────────────┐    ┌────────────────────┐            │
│  │  Exits within      │    │  Exits after       │            │
│  │  grace period      │    │  grace period      │            │
│  └─────────┬──────────┘    └─────────┬──────────┘            │
│            │                          │                      │
│            ▼                          ▼                      │
│  ┌────────────────────┐    ┌────────────────────┐            │
│  │  Exit code 0       │    │  SIGKILL sent      │            │
│  │  Clean shutdown    │    │  Exit code 137     │            │
│  └────────────────────┘    └────────────────────┘            │
└──────────────────────────────────────────────────────────────┘

PID 1 Signal Forwarding Problem

┌──────────────────────────────────────────────────────────────┐
│  WITHOUT exec                   WITH exec                    │
│                                                              │
│  PID 1: /bin/sh                 PID 1: nginx                 │
│  └── PID 2: nginx               └── (receives SIGTERM)       │
│                                                              │
│  SIGTERM → shell (ignored)      SIGTERM → nginx (handled)    │
│  nginx never notified           nginx shuts down gracefully  │
│  SIGKILL after 10s              exits with code 0            │
│                                                              │
│  Fix: exec nginx -g 'daemon off;' in the script              │
│  Or: --init flag inserts tini as PID 1                       │
└──────────────────────────────────────────────────────────────┘

Restart Policy Decision

┌──────────────────────────────────────────────────────────────┐
│  RESTART POLICY                                              │
│                                                              │
│  Should the container restart after a crash?                 │
│       │                                                      │
│       ├── No ──▶ --restart no                                │
│       │                                                      │
│       └── Yes ──▶ Should it restart after explicit stop?     │
│                      │                                       │
│                      ├── No ──▶ --restart unless-stopped     │
│                      │                                       │
│                      └── Yes ──▶ --restart always            │
│                                                              │
│  Retry limited times only? ──▶ --restart on-failure:N        │
└──────────────────────────────────────────────────────────────┘

Pause vs Stop

┌──────────────────────────────────────────────────────────────┐
│  PAUSE                         STOP                          │
│                                                              │
│  Processes frozen              Processes terminated          │
│  Memory retained               Memory freed                  │
│  State preserved               State lost (unless volume)    │
│  Ports held                    Ports released                │
│  Status: Up (Paused)           Status: Exited (code)         │
│  Resume: unpause               Resume: start                 │
│                                                              │
│  Use for temporary             Use for ending the            │
│  suspension.                   container's life.             │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
docker pauseFreeze processes via cgroup freezer
docker unpauseResume frozen processes
docker restartStop and start
docker stopSIGTERM, grace period, SIGKILL
Default grace period10 seconds
Default signalSIGTERM (signal 15)
Forced signalSIGKILL (signal 9)
Exit code 0Clean exit
Exit code 143Terminated by SIGTERM
Exit code 137Terminated by SIGKILL
Restart policiesno, on-failure, always, unless-stopped
--initInsert tini as PID 1 for signal forwarding
--stop-signalCustom signal for docker stop
--stop-timeoutCustom grace period

Key takeaways:

  • docker pause freezes a container without terminating it. Memory and state are retained, and docker unpause resumes exactly where it stopped. A paused container holds its ports and volumes.
  • docker stop sends SIGTERM, then SIGKILL after a grace period. The default grace period is 10 seconds, configurable with -t or --stop-timeout. The exit code reveals which signal ended the process.
  • PID 1 does not receive default signal handling. A container whose main process is a shell script may ignore SIGTERM, causing Docker to SIGKILL after the timeout. Use the exec form or --init to fix this.
  • The --init flag inserts tini as PID 1. It forwards signals to the child process and reaps orphaned processes, which is the recommended approach when the application cannot be PID 1 directly.
  • Restart policies control what happens after exit. no never restarts, on-failure restarts on non-zero exit, always always restarts, and unless-stopped restarts except after an explicit stop.
  • unless-stopped is usually the right choice for production. It respects an operator’s decision to stop the container while still recovering from crashes and daemon restarts.
  • Custom stop signals accommodate applications that expect them. --stop-signal SIGINT is appropriate for Node.js applications that handle SIGINT for shutdown.
  • Graceful shutdown requires the application to handle the signal. The container’s main process must install a handler for SIGTERM that flushes buffers, closes connections, and exits cleanly. Without it, the process is killed abruptly after the timeout.

Remember: A container’s lifecycle is not binary. Between running and removed are the states of paused, stopped, and restarting, and each transition is governed by signals and policies. docker pause freezes without losing state; docker stop requests graceful termination and escalates to force if the request is ignored. The escalation is the key mechanic: SIGTERM is a request, SIGKILL is a command, and the grace period is the window in which the application decides which one it will receive. Whether the application can use that window depends on whether its main process handles signals and whether it is PID 1. A shell script as PID 1 will ignore SIGTERM; an exec-ed application or one wrapped with --init will not. Restart policies determine what happens next: whether a crashed container is retried, whether a stopped container stays stopped, and whether a daemon restart brings containers back. When these pieces are configured correctly, containers stop cleanly, data is preserved, and services recover from failures without manual intervention.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!