Restart Policies and Multi-Service Stacks
Table of Contents
- Restart Condition, in practice
- batch-job-none-restart
- multi-service-stack
- Deploying the two-service stack
- Beginner Pitfalls
1. Restart Condition, in practice
A stack's Policy & Resource tab (see Part 1) sets Restart Condition to one of three values:
- Any — restart no matter how the process exited (success or
failure). Right for a long-running server (e.g.
docker-hello-world) — if it crashes, or exits for any other reason, Run App brings it back. - On failure — restart only on a non-zero exit code. Useful for a job you want retried a few times on transient failure, but not re-run after it succeeds. Combine with Restart Max Attempts so it doesn't retry forever.
- None — never restart, regardless of exit code. Right for a one-off container that's supposed to run once and stop, like a database migration or import job. Leaving this on Any for a job like that turns it into an infinite loop, since Run App keeps re-running it every time it exits.
2. batch-job-none-restart
A container that does a fixed amount of "work" and then exits — standing in for a one-off job like a migration or data import:
const http = require("http");
const port = process.env.PORT || 3000;
const jobName = process.env.JOB_NAME || "demo-migration";
const workSeconds = Number(process.env.WORK_SECONDS || 10);
const shouldFail = process.env.FAIL === "true";
let status = "running";
const startedAt = Date.now();
const server = http.createServer((req, res) => {
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify({
job: jobName,
status,
uptimeSeconds: Math.floor((Date.now() - startedAt) / 1000),
})
);
});
server.listen(port, () => {
console.log(`[${jobName}] listening on port ${port}, working for ${workSeconds}s`);
});
setTimeout(() => {
if (shouldFail) {
console.error(`[${jobName}] failed`);
status = "failed";
process.exitCode = 1;
server.close(() => process.exit(1));
return;
}
console.log(`[${jobName}] done`);
status = "done";
server.close(() => process.exit(0));
}, workSeconds * 1000);
It still serves one route (GET /, returning { job, status, uptimeSeconds }) while it "works" — Container Port needs something
listening so the stack has a status to check via its domain/port before
the job exits. After WORK_SECONDS, the process exits 0 normally, or
1 if FAIL=true.
Locally, docker ps -a after running it shows the container as Exited (0) — it doesn't come back on its own. Docker's own --restart flag
isn't set here; Run App's Restart Policy is what this example is about,
and that only applies once the image is deployed as a Run App stack.
Deploy it, set Restart Condition to None, then CREATE & DEPLOY
— watch the service run once and stop in
Part 3's
service view. Then try redeploying the same stack with Restart
Condition set to Any instead, and FAIL=true, to see the
crash-loop behavior this option is meant to avoid for a job like this.
3. multi-service-stack
Two small Express services — api (returns a JSON list) and web
(fetches from api and renders it as HTML) — deployed as one Run App
stack with two services. This is the example for the ADD SERVICE
button and the Depend On field from
Part 1,
rather than the single-service flow docker-hello-world uses.
api/—GET /api/items,GET /health. ReadsPORT(default4000).web/—GET /(renders items fromapi),GET /health. ReadsPORT(default3000) andAPI_URL(defaulthttp://localhost:4000), which it uses to reach theapiservice.
Locally, Docker Compose gives each service a container hostname
automatically, so web reaches api at http://api:4000:
docker compose up --build
Visit http://localhost:3000 — it should show three items fetched from
the api service.
4. Deploying the two-service stack
-
Build both images for
linux/amd64and push them to a registry:docker login docker buildx build --platform linux/amd64 \ -t <your-dockerhub-username>/run-app-multi-service-api:latest \ --push ./api docker buildx build --platform linux/amd64 \ -t <your-dockerhub-username>/run-app-multi-service-web:latest \ --push ./web -
Create one stack containing both services:
- Add the
apiservice first (Service Info → yourrun-app-multi-service-apiimage, container port4000). - Click ADD SERVICE and add
web(yourrun-app-multi-service-webimage, container port3000), setting Depend On to theapiservice.
- Add the
-
Once
apiis deployed, open its Service Info tab (see Part 3) and copy its Service Network value. SetAPI_URLfor thewebservice tohttp://<that value>:4000. This value is specific to your own stack (it embeds a random uid), so it can't be hardcoded in this example — copy it from your dashboard afterapiis deployed.
5. Beginner Pitfalls
- Leaving Restart Condition on Any for a one-off job. It turns a job that should run once into an infinite crash/restart loop.
- Hardcoding
API_URLbetween services. The Service Network value embeds a random account-specific uid — it must be copied from your own dashboard after the depended-on service is deployed, not reused from an example. - Forgetting to set Depend On. Without it,
webmay start beforeapiis reachable — though Run App doesn't block startup on it, so a missing dependency shows up as failed requests, not a deploy error. - Testing
docker-compose.yml'sAPI_URLagainst a real Run App stack.http://api:4000only resolves inside Compose's own network — once deployed to Run App,API_URLmust use the actual Service Network value instead.
Buy Me a Coffee
Found this tutorial helpful? Consider buying me a coffee to support more free content!