mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 01:54:43 +00:00
add restart tolerance
This commit is contained in:
@@ -134,8 +134,9 @@ containers, which would end the run prematurely. See
|
|||||||
- Production logging levels
|
- Production logging levels
|
||||||
- No database clearing
|
- No database clearing
|
||||||
- Optimized build configurations
|
- Optimized build configurations
|
||||||
- Health checks enabled
|
- Health checks on every long-running service
|
||||||
- Restart policies (unless-stopped)
|
- Restart policies that survive a reboot (see
|
||||||
|
[Restart tolerance](#restart-tolerance))
|
||||||
- Security hardening
|
- Security hardening
|
||||||
- Only `frontend` publishes a port to the host, bound to `127.0.0.1` so a reverse
|
- Only `frontend` publishes a port to the host, bound to `127.0.0.1` so a reverse
|
||||||
proxy can sit in front of it; `sqlserver`, `api.core` and `seaweedfs` are
|
proxy can sit in front of it; `sqlserver`, `api.core` and `seaweedfs` are
|
||||||
@@ -170,6 +171,41 @@ Docker seeds each volume with the ownership the image expects and labels it for
|
|||||||
SELinux, so the host needs no `chown` and no `:z`/`:Z` mount options on
|
SELinux, so the host needs no `chown` and no `:z`/`:Z` mount options on
|
||||||
enforcing distributions such as RHEL, AlmaLinux and Fedora.
|
enforcing distributions such as RHEL, AlmaLinux and Fedora.
|
||||||
|
|
||||||
|
#### Restart tolerance
|
||||||
|
|
||||||
|
`sqlserver`, `api.core`, `seaweedfs` and `frontend` run under
|
||||||
|
`restart: unless-stopped`, so the stack returns on its own after a container
|
||||||
|
crash, a Docker daemon restart or a host reboot, provided Docker itself starts at
|
||||||
|
boot (`systemctl enable docker`). `database.migrations` keeps `restart: "no"`: it
|
||||||
|
is a one-shot job that runs once per deploy and stays exited across a reboot, so
|
||||||
|
the schema is migrated on deploy rather than on every boot.
|
||||||
|
|
||||||
|
The daemon replays restart policies without replaying the `depends_on` graph, so
|
||||||
|
after a reboot the containers come back in arbitrary order and each one tolerates
|
||||||
|
a dependency that arrives late. `api.core` opens its database and S3 connections
|
||||||
|
per request rather than at startup, and `frontend` answers with its own error
|
||||||
|
states while `api.core` is still coming up.
|
||||||
|
|
||||||
|
Health checks report when the stack has converged:
|
||||||
|
|
||||||
|
| Service | Probe |
|
||||||
|
| --- | --- |
|
||||||
|
| `sqlserver` | `sqlcmd -Q 'SELECT 1'`, with a 30s start period and 12 retries |
|
||||||
|
| `api.core` | `GET /health` spoken over bash's `/dev/tcp` — the .NET runtime image carries bash and no HTTP client |
|
||||||
|
| `seaweedfs` | `curl http://localhost:8333/status`, the S3 gateway, which serves once the master, volume and filer servers are up |
|
||||||
|
| `frontend` | `wget --spider http://127.0.0.1:5656/healthz`, answered by Express alone and so independent of the API |
|
||||||
|
|
||||||
|
`sqlserver` also carries `stop_grace_period: 60s`, giving a checkpoint time to
|
||||||
|
finish on shutdown instead of being recovered on the next start.
|
||||||
|
|
||||||
|
Deploy and wait for the stack to report healthy, or check convergence after a
|
||||||
|
reboot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --env-file web/.env.prod -f web/docker-compose.prod.yaml up -d --wait
|
||||||
|
docker compose --env-file web/.env.prod -f web/docker-compose.prod.yaml ps
|
||||||
|
```
|
||||||
|
|
||||||
#### Seeding production (`docker-compose.prod.seed.yaml`)
|
#### Seeding production (`docker-compose.prod.seed.yaml`)
|
||||||
|
|
||||||
`database.seed` lives in its own compose file, run on demand once the prod stack
|
`database.seed` lives in its own compose file, run on demand once the prod stack
|
||||||
@@ -266,6 +302,12 @@ graph TD
|
|||||||
In development, `api.core` also depends on `seaweedfs`
|
In development, `api.core` also depends on `seaweedfs`
|
||||||
(`condition: service_started`, not a health check) alongside `database.seed`.
|
(`condition: service_started`, not a health check) alongside `database.seed`.
|
||||||
|
|
||||||
|
In production the chain is gated on health instead: `api.core` waits for
|
||||||
|
`seaweedfs` to pass its health check and for `database.migrations` to exit
|
||||||
|
successfully, and `frontend` waits for `api.core` to report healthy. This ordering
|
||||||
|
applies to `docker compose up`; see
|
||||||
|
[Restart tolerance](#restart-tolerance) for what happens on a host reboot.
|
||||||
|
|
||||||
**Health Check Example** (SQL Server):
|
**Health Check Example** (SQL Server):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 12
|
retries: 12
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
|
restart: unless-stopped
|
||||||
|
stop_grace_period: 60s
|
||||||
networks:
|
networks:
|
||||||
- prodnet
|
- prodnet
|
||||||
|
|
||||||
@@ -54,7 +56,7 @@ services:
|
|||||||
database.migrations:
|
database.migrations:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
seaweedfs:
|
seaweedfs:
|
||||||
condition: service_started
|
condition: service_healthy
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: API/API.Core/Dockerfile
|
dockerfile: API/API.Core/Dockerfile
|
||||||
@@ -79,6 +81,20 @@ services:
|
|||||||
SEAWEEDFS_ACCESS_KEY_ID: "${SEAWEEDFS_ACCESS_KEY_ID}"
|
SEAWEEDFS_ACCESS_KEY_ID: "${SEAWEEDFS_ACCESS_KEY_ID}"
|
||||||
SEAWEEDFS_SECRET_ACCESS_KEY: "${SEAWEEDFS_SECRET_ACCESS_KEY}"
|
SEAWEEDFS_SECRET_ACCESS_KEY: "${SEAWEEDFS_SECRET_ACCESS_KEY}"
|
||||||
SEAWEEDFS_BUCKET: "${SEAWEEDFS_BUCKET}"
|
SEAWEEDFS_BUCKET: "${SEAWEEDFS_BUCKET}"
|
||||||
|
healthcheck:
|
||||||
|
# The .NET runtime image carries bash but no HTTP client, so the probe speaks
|
||||||
|
# HTTP to /health over bash's /dev/tcp rather than adding curl to the image.
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
'exec 3<>/dev/tcp/127.0.0.1/8080; printf "GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" >&3; head -1 <&3 | grep -q " 200 "',
|
||||||
|
]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- prodnet
|
- prodnet
|
||||||
@@ -97,6 +113,14 @@ services:
|
|||||||
S3_BUCKET: "${SEAWEEDFS_BUCKET}"
|
S3_BUCKET: "${SEAWEEDFS_BUCKET}"
|
||||||
volumes:
|
volumes:
|
||||||
- seaweedfsdata-prod:/data
|
- seaweedfsdata-prod:/data
|
||||||
|
healthcheck:
|
||||||
|
# Probes the S3 gateway, the one component api.core talks to; it comes up
|
||||||
|
# last, after the master, volume and filer servers are serving.
|
||||||
|
test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://localhost:8333/status"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 20s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- prodnet
|
- prodnet
|
||||||
@@ -107,7 +131,7 @@ services:
|
|||||||
container_name: prod-env-frontend
|
container_name: prod-env-frontend
|
||||||
depends_on:
|
depends_on:
|
||||||
api.core:
|
api.core:
|
||||||
condition: service_started
|
condition: service_healthy
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -118,6 +142,14 @@ services:
|
|||||||
PORT: "5656"
|
PORT: "5656"
|
||||||
API_BASE_URL: "http://api.core:8080"
|
API_BASE_URL: "http://api.core:8080"
|
||||||
SESSION_SECRET: "${SESSION_SECRET}"
|
SESSION_SECRET: "${SESSION_SECRET}"
|
||||||
|
healthcheck:
|
||||||
|
# /healthz answers from the Express server alone, so the frontend stays
|
||||||
|
# healthy while api.core is still starting and serves its own error states.
|
||||||
|
test: ["CMD", "wget", "--quiet", "--spider", "--tries=1", "http://127.0.0.1:5656/healthz"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 15s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- prodnet
|
- prodnet
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ const app = express();
|
|||||||
app.use('/assets', express.static('build/client/assets', { immutable: true, maxAge: '1y' }));
|
app.use('/assets', express.static('build/client/assets', { immutable: true, maxAge: '1y' }));
|
||||||
app.use(express.static('build/client', { maxAge: '1h' }));
|
app.use(express.static('build/client', { maxAge: '1h' }));
|
||||||
|
|
||||||
|
// Answers from Express alone so the container health check reports on this
|
||||||
|
// process only, independent of whether the API is reachable yet.
|
||||||
|
app.get('/healthz', (_req, res) => {
|
||||||
|
res.status(200).type('text/plain').send('ok');
|
||||||
|
});
|
||||||
|
|
||||||
const requestHandler = createRequestHandler({
|
const requestHandler = createRequestHandler({
|
||||||
build: () => import('./build/server/index.js'),
|
build: () => import('./build/server/index.js'),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user