Skip to content

Commit 64b2234

Browse files
Merge pull request #3 from numbersprotocol/omni/mainnet-explorer-hardening-20260521
Merged after review. The only failing check is unrelated CI setup: erlef/setup-beam missing otp-version before project code is built.
2 parents bc4930b + e5f6c8e commit 64b2234

6 files changed

Lines changed: 129 additions & 1 deletion

File tree

docker-compose/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ You can adjust BlockScout environment variables:
6767
- for visualizer in `./envs/common-visualizer.env`
6868
- for user-ops-indexer in `./envs/common-user-ops-indexer.env`
6969

70+
For production resource limits, health checks, log rotation, and monitoring
71+
requirements, see [`production-hardening.md`](./production-hardening.md).
72+
7073
Descriptions of the ENVs are available
7174

7275
- for [backend](https://docs.blockscout.com/setup/env-variables)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Production Hardening
2+
3+
This checklist keeps the public Docker Compose files safe to publish while
4+
covering the failure mode observed on the Numbers mainnet explorer: long-running
5+
containers consumed nearly all VM memory, then the VM kept receiving packets but
6+
stopped returning responses.
7+
8+
## Resource Guards
9+
10+
The compose service files define non-secret defaults that can be overridden by
11+
the production environment:
12+
13+
```sh
14+
BACKEND_MEM_LIMIT=4g
15+
BACKEND_MEMSWAP_LIMIT=4g
16+
DB_MEM_LIMIT=6g
17+
DB_MEMSWAP_LIMIT=6g
18+
STATS_MEM_LIMIT=1g
19+
STATS_MEMSWAP_LIMIT=1g
20+
STATS_DB_MEM_LIMIT=1g
21+
STATS_DB_MEMSWAP_LIMIT=1g
22+
REDIS_MEM_LIMIT=512m
23+
REDIS_MEMSWAP_LIMIT=512m
24+
```
25+
26+
Keep total container limits below host memory so the OS, nginx, Docker, and the
27+
monitoring agent have headroom. On a 16 GiB VM, reserve at least 3 GiB for the
28+
host.
29+
30+
## Health Checks
31+
32+
The backend container now exposes a Docker healthcheck against:
33+
34+
```text
35+
http://localhost:$${PORT:-4000}/api/v2/main-page/indexing-status
36+
```
37+
38+
Production monitoring should also check the public endpoint:
39+
40+
```sh
41+
curl -fsS --max-time 10 \
42+
https://mainnet.num.network/api/v2/main-page/indexing-status
43+
```
44+
45+
Alert if this endpoint is non-200 or exceeds the expected latency for multiple
46+
consecutive checks.
47+
48+
## Log Rotation
49+
50+
Docker `json-file` log rotation is enabled for the high-volume services. The
51+
defaults are intentionally conservative and can be adjusted without changing the
52+
compose files:
53+
54+
```sh
55+
BACKEND_LOG_MAX_SIZE=50m
56+
BACKEND_LOG_MAX_FILE=5
57+
DB_LOG_MAX_SIZE=50m
58+
DB_LOG_MAX_FILE=5
59+
STATS_LOG_MAX_SIZE=50m
60+
STATS_LOG_MAX_FILE=5
61+
STATS_DB_LOG_MAX_SIZE=25m
62+
STATS_DB_LOG_MAX_FILE=5
63+
REDIS_LOG_MAX_SIZE=25m
64+
REDIS_LOG_MAX_FILE=5
65+
```
66+
67+
## Monitoring
68+
69+
Required production alerts:
70+
71+
- VM memory used > 85% for 10 minutes.
72+
- VM outbound bytes = 0 while inbound bytes > 0 for 5 minutes.
73+
- Public explorer health endpoint returns non-200 for 3 consecutive checks.
74+
- Docker container health is `unhealthy` for backend.
75+
76+
Enable process-level memory metrics on the VM. Without process RSS history, an
77+
incident can prove memory exhaustion but cannot identify which process caused it.
78+
Do not exclude these Ops Agent metrics in production:
79+
80+
```yaml
81+
agent.googleapis.com/processes/*
82+
agent.googleapis.com/swap/*
83+
```

docker-compose/services/backend.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ services:
55
image: blockscout/${DOCKER_REPO:-blockscout}:${DOCKER_TAG:-latest}
66
pull_policy: always
77
restart: always
8+
mem_limit: ${BACKEND_MEM_LIMIT:-4g}
9+
memswap_limit: ${BACKEND_MEMSWAP_LIMIT:-4g}
810
stop_grace_period: 5m
911
container_name: 'backend'
1012
command: sh -c "bin/blockscout eval \"Elixir.Explorer.ReleaseTasks.create_and_migrate()\" && bin/blockscout start"
@@ -14,4 +16,15 @@ services:
1416
- ../envs/common-blockscout.env
1517
volumes:
1618
- ./logs/:/app/logs/
17-
- ./dets/:/app/dets/
19+
- ./dets/:/app/dets/
20+
healthcheck:
21+
test: ["CMD-SHELL", "curl -fsS http://localhost:$${PORT:-4000}/api/v2/main-page/indexing-status >/dev/null || exit 1"]
22+
interval: 30s
23+
timeout: 10s
24+
retries: 3
25+
start_period: 2m
26+
logging:
27+
driver: json-file
28+
options:
29+
max-size: ${BACKEND_LOG_MAX_SIZE:-50m}
30+
max-file: ${BACKEND_LOG_MAX_FILE:-5}

docker-compose/services/db.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ services:
1616
user: 2000:2000
1717
shm_size: 256m
1818
restart: always
19+
mem_limit: ${DB_MEM_LIMIT:-6g}
20+
memswap_limit: ${DB_MEMSWAP_LIMIT:-6g}
1921
container_name: 'db'
2022
command: postgres -c 'max_connections=200' -c 'client_connection_check_interval=60000'
2123
environment:
@@ -33,3 +35,8 @@ services:
3335
timeout: 5s
3436
retries: 5
3537
start_period: 10s
38+
logging:
39+
driver: json-file
40+
options:
41+
max-size: ${DB_LOG_MAX_SIZE:-50m}
42+
max-file: ${DB_LOG_MAX_FILE:-5}

docker-compose/services/redis.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,13 @@ services:
55
image: 'redis:alpine'
66
container_name: redis-db
77
command: redis-server
8+
restart: always
9+
mem_limit: ${REDIS_MEM_LIMIT:-512m}
10+
memswap_limit: ${REDIS_MEMSWAP_LIMIT:-512m}
811
volumes:
912
- ./redis-data:/data
13+
logging:
14+
driver: json-file
15+
options:
16+
max-size: ${REDIS_LOG_MAX_SIZE:-25m}
17+
max-file: ${REDIS_LOG_MAX_FILE:-5}

docker-compose/services/stats.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ services:
1616
user: 2000:2000
1717
shm_size: 256m
1818
restart: always
19+
mem_limit: ${STATS_DB_MEM_LIMIT:-1g}
20+
memswap_limit: ${STATS_DB_MEMSWAP_LIMIT:-1g}
1921
container_name: 'stats-db'
2022
command: postgres -c 'max_connections=200'
2123
environment:
@@ -33,12 +35,19 @@ services:
3335
timeout: 5s
3436
retries: 5
3537
start_period: 10s
38+
logging:
39+
driver: json-file
40+
options:
41+
max-size: ${STATS_DB_LOG_MAX_SIZE:-25m}
42+
max-file: ${STATS_DB_LOG_MAX_FILE:-5}
3643

3744
stats:
3845
image: ghcr.io/blockscout/stats:${STATS_DOCKER_TAG:-latest}
3946
pull_policy: always
4047
platform: linux/amd64
4148
restart: always
49+
mem_limit: ${STATS_MEM_LIMIT:-1g}
50+
memswap_limit: ${STATS_MEMSWAP_LIMIT:-1g}
4251
container_name: 'stats'
4352
extra_hosts:
4453
- 'host.docker.internal:host-gateway'
@@ -49,3 +58,8 @@ services:
4958
- STATS__BLOCKSCOUT_DB_URL=${STATS__BLOCKSCOUT_DB_URL:-postgresql://blockscout:ceWb1MeLBEeOIfk65gU8EjF8@db:5432/blockscout}
5059
- STATS__CREATE_DATABASE=${STATS__CREATE_DATABASE:-true}
5160
- STATS__RUN_MIGRATIONS=${STATS__RUN_MIGRATIONS:-true}
61+
logging:
62+
driver: json-file
63+
options:
64+
max-size: ${STATS_LOG_MAX_SIZE:-50m}
65+
max-file: ${STATS_LOG_MAX_FILE:-5}

0 commit comments

Comments
 (0)