
A running container may not be ready
A multi-service application can start before its database or internal dependency accepts requests. The first user request then fails, while restarting the application appears to fix the problem. This tutorial uses two small services to make that race visible. The useful outcome is an explicit readiness contract. Adding an arbitrary sleep only hides the race until startup becomes slower on another machine or under load.
The Compose configuration waits for the dependency to become healthy before starting the application. This helps with initial ordering; it does not guarantee that the dependency stays available afterwards. The application must still handle connection failures and timeouts during normal operation. An unhealthy status also does not itself restart a container. Process restart policies and decisions made by the deployment platform are separate controls that need their own design.
A dependency experiment and health-check boundaries
The liveness endpoint answers without contacting another service. Readiness performs a bounded request to the dependency and returns status 503 when it cannot complete successfully. Avoid expensive transactions or data mutations in production readiness handlers. Calculate the cost of the check and its frequency, especially when many replicas share the same backend. Monitoring should not become a recurring load spike that damages the dependency it is meant to observe.
Save the four files together, inspect the configuration with docker compose config, then run docker compose up --build --wait. Execute python check_stack.py from the host. A zero exit status means that this particular readiness request succeeded. The example publishes its port on the loopback interface only. It intentionally provides a local experiment rather than a complete Internet-facing deployment with TLS, authentication and production traffic management.
After a successful response, run docker compose stop dependency in the test environment and execute the checker again. Readiness should fail even though the application process remains alive. Bring the dependency back with docker compose start dependency and repeat the check. This demonstrates why startup ordering cannot replace runtime error handling. A service can satisfy its initial dependency condition and still encounter failures during any later user request.
Complete files to save
Save all four files together. The article explains execution and verification; no code is replaced with ellipses.
1. Liveness and readiness endpoints
File: app.py. Save every line under this filename.
import json
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
ready = True
if self.path == '/ready':
try:
with urllib.request.urlopen('http://dependency:8000/', timeout=2) as response:
ready = response.status == 200
except OSError:
ready = False
elif self.path != '/live':
self.send_error(404)
return
body = json.dumps({'ready': ready}).encode()
self.send_response(200 if ready else 503)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
if __name__ == '__main__':
ThreadingHTTPServer(('0.0.0.0', 8000), Handler).serve_forever()This file belongs to the complete example above; execution order, prerequisites and limits are explained in the article.
2. An application image with a restricted user
File: Dockerfile. Save every line under this filename.
FROM python:3.14-slim
WORKDIR /app
COPY app.py /app/app.py
USER 10001:10001
EXPOSE 8000
CMD ["python", "app.py"]This file belongs to the complete example above; execution order, prerequisites and limits are explained in the article.
3. Wait for a healthy dependency
File: compose.yaml. Save every line under this filename.
services:
dependency:
image: python:3.14-slim
user: "10001:10001"
working_dir: /tmp
command: ["python", "-m", "http.server", "8000"]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/', timeout=2).read(1)"]
interval: 5s
timeout: 3s
retries: 5
start_period: 5s
app:
build: .
ports: ["127.0.0.1:8088:8000"]
depends_on:
dependency:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/ready', timeout=3).read(1)"]
interval: 5s
timeout: 4s
retries: 3
start_period: 5sThis file belongs to the complete example above; execution order, prerequisites and limits are explained in the article.
4. Check readiness from the host
File: check_stack.py. Save every line under this filename.
"""Run after docker compose up --build --wait; does not alter containers."""
import json
import sys
import urllib.error
import urllib.request
def check(url):
try:
with urllib.request.urlopen(url, timeout=5) as response:
return response.status == 200 and json.load(response)['ready'] is True
except (OSError, ValueError, KeyError):
return False
if __name__ == '__main__':
passed = check('http://127.0.0.1:8088/ready')
print(json.dumps({'ready': passed}))
sys.exit(0 if passed else 1)This file belongs to the complete example above; execution order, prerequisites and limits are explained in the article.
Run the example and observe a real failure
For deployment, record and periodically review the chosen base image version or digest in your release process. Tune health-check timings against observed startup behavior and inspect health output together with application logs. Use docker compose down to finish this experiment. Before applying the pattern to an existing service, exercise dependency loss, slow responses and recovery in an isolated environment with the actual application client and its timeout configuration.
Implementation checklist
- Validate configuration and initial readiness.
- Stop the dependency and observe readiness failing.
- Bound dependency request time.
- Keep health status separate from restart policy.
References
Editorial guidance: Liyan Knowledge.




