Skip to content
R.

How do I restart a Compose app when its dependency is explicitly restarted?

Use long-form `depends_on.restart: true` when an explicit Compose restart of a backend should restart the dependent app too. Container restart policies solve a different problem and do not create that dependency-triggered relationship.

About 11 min readComments

When a backend is restarted on purpose, the dependent app does not always refresh with it. That can leave a Compose stack in a half-new, half-old state even though the original startup order was correct. The fix is not another sleep loop or a larger startup timeout. The fix is to make the dependency relationship explicit at restart time, not only at startup time.

Docker Compose has two separate mechanisms that are easy to blend together: container restart policy and dependency restart propagation. This article uses a minimal backend/app example to show the difference, then shows why the long-form `depends_on.restart: true` option is the one that makes an explicit `docker compose restart` of the backend restart the app as well.

The failure is not startup order; it is a later restart that leaves the app stale

A Compose stack can start in the right order and still become inconsistent later. The backend comes up first, the app comes up second, and initial readiness checks may all pass. Then an operator restarts the backend on purpose, perhaps after a configuration change, a maintenance step, or a short recovery procedure. The backend is new, but the app may still be holding an old connection, an old socket, or an old assumption about the dependency's state.

That is the problem this article solves. The issue is not whether the app waited long enough at startup. The issue is whether a later explicit Compose restart of the dependency should also force the dependent to restart so it reconnects and reinitializes its own state. Docker Compose documents that as the long-form `depends_on.restart: true` option. It is a propagation rule for an explicit Compose operation, not a generic safety net for every possible container restart.

The example in this article keeps the scope narrow on purpose. It uses one backend service and one app service. It does not cover load balancers, replicated services, database migrations, or reconnect logic inside the application. The point is to distinguish two separate controls that are easy to confuse: startup readiness and dependency-triggered restarts.

main.mjs / javascript
export function buildBackendAppCompose() {
  return {
    services: {
      backend: {
        image: 'example/backend:1',
        restart: 'unless-stopped',
      },
      app: {
        image: 'example/app:1',
        restart: 'unless-stopped',
        depends_on: {
          backend: {
            condition: 'service_started',
            restart: true,
          },
        },
      },
    },
  };
}

/**
 * Validates the configuration and operation needed for Compose's documented
 * dependency-restart feature. This is a configuration check only: it does not
 * invoke Docker or prove that a live Compose engine restarted a container.
 */
export function validateExplicitDependencyRestart(compose, command) {
  if (!compose || typeof compose !== 'object' || !compose.services) {
    throw new TypeError('compose.services is required');
  }

  if (!Array.isArray(command) || command.length !== 4) {
    throw new TypeError('command must be: docker compose restart <dependency>');
  }

  const [docker, composeCommand, restart, dependency] = command;
  if (docker !== 'docker' || composeCommand !== 'compose' || restart !== 'restart') {
    throw new TypeError('command must be: docker compose restart <dependency>');
  }

  if (!compose.services[dependency]) {
    throw new RangeError(`dependency service "${dependency}" does not exist`);
  }

  const dependents = Object.entries(compose.services)
    .filter(([, service]) => service.depends_on?.[dependency]?.restart === true)
    .map(([name]) => name);

  if (dependents.length === 0) {
    throw new Error(
      `No service declares depends_on.${dependency}.restart: true; a container restart policy alone does not configure dependency restart propagation`,
    );
  }

  return {
    operation: 'explicit-compose-restart',
    dependency,
    dependents,
    proofBoundary:
      'This validates declared Compose configuration only. Run Docker Compose against a real engine to observe container restarts.',
  };
}

SourcesDocker Docs: Control startup and shutdown order in Compose (opens a new tab)Docker Docs: Compose file services reference (opens a new tab)

`restart: true` belongs to `depends_on`, not to the container restart policy

Docker Compose gives service restart policy and dependency restart propagation different jobs. The service-level `restart` key controls what the container runtime does when a container exits. Docker documents values such as `no`, `always`, `on-failure`, and `unless-stopped` for that purpose. Those policies decide whether a terminated container should be brought back by the runtime. They do not say that a downstream app should restart when some other service is explicitly restarted from Compose.

The long-form `depends_on` syntax adds a separate `restart` field. Docker documents that this field restarts the dependent service after Compose updates the dependency service. The important limitation is in the wording: it applies to an explicit restart controlled by a Compose operation, such as `docker compose restart`, and it excludes automated restart by the container runtime after the container dies. That boundary matters because it tells you exactly what this feature does not do.

The practical consequence is simple. If you only add `restart: always` to both services, you are still telling each container how to behave after its own termination. You are not telling Compose to restart the app whenever the backend is intentionally restarted. For this scenario, the dependency edge needs the long-form `depends_on` option with `restart: true`.

SourcesDocker Docs: Control startup and shutdown order in Compose (opens a new tab)Docker Docs: Compose file services reference (opens a new tab)

The minimal example proves the declared relationship, not a live Docker engine

The supplied example is intentionally small. The `buildBackendAppCompose()` function returns a Compose services object with two services: `backend` and `app`. The backend has a service-level restart policy of `unless-stopped`. The app also has a service-level restart policy of `unless-stopped`, but its `depends_on.backend` entry includes `condition: 'service_started'` and `restart: true`. That means the app is waiting for the backend to exist before the app starts, and it is also asking Compose to restart the app when the backend is explicitly restarted later.

The validation function, `validateExplicitDependencyRestart()`, checks the configuration and the command shape. It accepts a Compose object and a command array like `['docker', 'compose', 'restart', 'backend']`. If the target service exists and at least one service declares `depends_on.backend.restart === true`, the function returns the dependency name, the affected dependents, and a proof boundary string that says this is configuration validation only. It does not call Docker, and it does not claim to observe a real container restart.

That proof boundary is worth keeping visible. A pure Node test can prove that the configuration is declared correctly and that the command is the right one to issue. It cannot prove Docker's live runtime behavior on your machine. The article therefore treats the code as a configuration checker and the Docker docs as the source of truth for the runtime contract.

SourcesDocker Docs: Control startup and shutdown order in Compose (opens a new tab)Docker Docs: Compose file services reference (opens a new tab)

The tests show the distinction between propagation and policy

The first test proves the positive case. Given the composed backend/app configuration, validating `docker compose restart backend` returns `operation: 'explicit-compose-restart'`, identifies `backend` as the dependency, and identifies `app` as the dependent that should be restarted because its long-form dependency entry says `restart: true`. That is the exact behavior the article is about: a later explicit Compose restart should reach the dependent.

The second test proves the negative case that often causes confusion. It creates a Compose object where both services have `restart: 'always'`, but the app's dependency entry omits `restart: true`. The validator throws with a message stating that a container restart policy alone does not configure dependency restart propagation. That negative case matters because many readers assume the service restart policy is enough. Docker's docs say otherwise, and the test keeps that distinction from drifting.

Two more tests keep the boundary honest. One ensures the command must target an existing dependency service; another rejects a non-Compose command shape. Those checks are not extra ceremony. They prevent the article from quietly conflating an explicit Compose restart with an unrelated container runtime restart or a mistyped command. The test suite proves the configuration relationship and the command form, but it does not prove a live daemon's container lifecycle. That remains a separate operational check.

main.test.mjs / javascript
import test from 'node:test';
import assert from 'node:assert/strict';
import {
  buildBackendAppCompose,
  validateExplicitDependencyRestart,
} from './main.mjs';

test('docker compose restart backend is configured to propagate to app', () => {
  const compose = buildBackendAppCompose();

  const result = validateExplicitDependencyRestart(compose, [
    'docker',
    'compose',
    'restart',
    'backend',
  ]);

  assert.deepEqual(result, {
    operation: 'explicit-compose-restart',
    dependency: 'backend',
    dependents: ['app'],
    proofBoundary:
      'This validates declared Compose configuration only. Run Docker Compose against a real engine to observe container restarts.',
  });
});

test('a container restart policy does not replace depends_on restart: true', () => {
  const compose = {
    services: {
      backend: { image: 'example/backend:1', restart: 'always' },
      app: {
        image: 'example/app:1',
        restart: 'always',
        depends_on: { backend: { condition: 'service_started' } },
      },
    },
  };

  assert.throws(
    () => validateExplicitDependencyRestart(compose, ['docker', 'compose', 'restart', 'backend']),
    /does not configure dependency restart propagation/,
  );
});

test('the explicit restart command must target an existing dependency service', () => {
  const compose = buildBackendAppCompose();

  assert.throws(
    () => validateExplicitDependencyRestart(compose, ['docker', 'compose', 'restart', 'database']),
    /dependency service "database" does not exist/,
  );
});

test('a non-Compose restart command is outside the explicit Compose operation', () => {
  const compose = buildBackendAppCompose();

  assert.throws(
    () => validateExplicitDependencyRestart(compose, ['docker', 'restart', 'backend']),
    /command must be: docker compose restart <dependency>/,
  );
});

SourcesDocker Docs: Compose file services reference (opens a new tab)Docker Docs: Control startup and shutdown order in Compose (opens a new tab)

`service_healthy` solves one timing problem; `restart: true` solves a different ownership problem

Compose startup conditions answer a question about the first boot. The docs explain that Compose does not wait for a container to be ready unless you use a dependency condition such as `service_healthy`. That is useful when a database process is running but not yet accepting connections. It makes the dependent wait until the dependency is ready enough for startup. But it does not by itself create a later restart relationship.

That difference is easy to miss if you only think in terms of 'wait for the database.' Waiting at startup answers, 'When may the app begin?' Dependency restart propagation answers, 'What should happen when the backend is later restarted on purpose?' The second question matters when the app needs to rebuild a connection pool, reread credentials, refresh a cached endpoint, or otherwise re-establish its own process state after the dependency has been replaced.

The article deliberately keeps those responsibilities separate. The startup condition controls the first creation order. The restart propagation controls a later operator action. When both are needed, both belong in the Compose file, but they do not mean the same thing and one does not substitute for the other. That separation is the main reason the example uses `condition: 'service_started'` rather than pretending the article is about readiness gating alone.

SourcesDocker Docs: Control startup and shutdown order in Compose (opens a new tab)Docker Docs: Compose file services reference (opens a new tab)

The pattern is useful, but it is not a cure for stale state in the app

This Compose feature is useful when you want a dependent to restart whenever the dependency is explicitly restarted from Compose. It is not a substitute for making the application resilient. If the app can reconnect cleanly without a restart, that is usually better than depending on a coordinated restart. The article's example does not claim that a restart is always required; it only shows how to make that restart relationship explicit when you do need it.

There are also clear boundaries. The documented behavior excludes automated restart by the container runtime after the container dies. So if the backend crashes and the runtime restarts it under `restart: always`, that is not the same event as a deliberate `docker compose restart backend`. The long-form `depends_on.restart: true` is not a general crash propagation mechanism. It is an operator-triggered dependency rule.

Finally, this pattern only helps when Compose owns the operation. If the backend is restarted outside Compose, or if another process manipulates containers directly, the propagation rule is not guaranteed to apply. That is why the article's recommendation stays modest: use the feature when a deliberate Compose restart should also refresh dependents, and keep the application's own reconnect behavior as the real long-term defense.

SourcesDocker Docs: Control startup and shutdown order in Compose (opens a new tab)Docker Docs: Compose file services reference (opens a new tab)

Share LinkedIn Email Subscribe

Discussion

Leave a comment

Comments appear after review. No email needed.

Image preview

Follow the blog

Choose email updates or your favorite RSS reader.

The weekly email

New articles on code, design, and AI. Fridays, only when there is something new.

By subscribing, you agree to weekly blog emails.
Confirm in your inbox. Unsubscribe anytime. Privacy

Use OpenRSS

Preview the feed, then choose a reader to subscribe.

Open in OpenRSS (opens a new tab)

Already have a reader?

Paste this link into your reader's Add feed option.

Prefer a reader? RSS does not require an email address. View XML feed