`docker compose` with sed -i: why the config changed in the file but the container ignored it — and how the inode caused an OOM loop
Published on April 28, 2026
The server rebooting from OOM — but the config was correct
A WordPress/Drupal publisher running a Docker stack on AWS was in an OOM loop. The server was rebooting repeatedly. Connecting via SSH, the uptime showed '2 minutes' — it had just come back up from the latest crash. The site was returning 502.
The server was a t4g.medium with 3.4 GB of RAM. Memory check showed:
free -h
# total used free shared buff/cache available
# Mem: 3.4Gi 3.0Gi 96Mi 12Mi 370Mi 96Mi
# Swap: 1.2Gi 1.2Gi 0Ki
docker stats --no-stream --format "table {{.Name}} {{.MemUsage}} {{.MemPerc}}"
# CONTAINER MEM USAGE / LIMIT MEM %
# cache 1.796GiB / 3.4GiB 52.38%
# php-fpm 406MiB / 3.4GiB 11.56%
# app-firewall 77MiB / 3.4GiB 2.20%
# security-agent 56MiB / 3.4GiB 1.60%
# nginx 52MiB / 3.4GiB 1.49%
# metrics-exporter 13MiB / 3.4GiB 0.38%Redis (called 'cache' in compose) consuming 1.796 GiB — 52% of total RAM. With PHP-FPM, nginx, and the other containers combined, the server was consistently over capacity. The kernel OOM killer terminated the greediest process, the container restarted, and the cycle began again.
The fix applied in the previous session — that didn't work
In the previous session, the fix had been applied via sed -i:
# Previous session: reduce maxmemory from 2048mb to 1024mb
sudo sed -i 's/--maxmemory 2048mb/--maxmemory 1024mb/' /opt/myapp/docker-compose.yml
# Immediate verification showed the file was correct:
grep maxmemory /opt/myapp/docker-compose.yml
# command: valkey-server --maxmemory 1024mb --maxmemory-policy allkeys-lruThe file was correct. The line with 1024mb was there. The fix looked applied. But the server kept rebooting from OOM.
When the file shows one thing and the container does another, the problem is usually which version of the file the container is reading — not the file's content.
The diagnosis: two inodes, two versions of the file
Checking via docker inspect revealed the problem:
# Check actual config of the cache container
docker inspect cache --format "{{.Config.Cmd}}"
# [valkey-server --maxmemory 2048mb --maxmemory-policy allkeys-lru]
# ^^^^^^ STILL 2048mb — ignoring the file!
# Check the file on the host
grep maxmemory /opt/myapp/docker-compose.yml
# command: valkey-server --maxmemory 1024mb --maxmemory-policy allkeys-lru
# ^^^^^^ 1024mb — file is correctThe container was running with --maxmemory 2048mb. The file had 1024mb. How can they differ if compose is bind-mounted?
How sed -i behaves with inodes
sed -i doesn't edit the file in-place. What it actually does internally is:
1. Read the original file (inode A)
2. Create a temporary file with the substitutions (new inode B)
3. Rename the temporary file to the original name
4. Inode A (original) still exists if anyone has a file descriptor open
In practice, after sed -i, the name docker-compose.yml points to inode B (with 1024mb). But Docker, when creating the container, opened a file descriptor for inode A (with 2048mb). The container doesn't read the file by name — it read the content at creation time and holds onto those settings.
# Demonstrating inode behavior with sed -i
# Before:
stat /opt/myapp/docker-compose.yml | grep 'Inode'
# Inode: 1234567
sudo sed -i 's/--maxmemory 2048mb/--maxmemory 1024mb/' /opt/myapp/docker-compose.yml
# After:
stat /opt/myapp/docker-compose.yml | grep 'Inode'
# Inode: 1234999 ← NEW inode! sed -i created a new file
# A container created with the original inode doesn't see the changeThis applies to any configuration passed as an argument in the compose command: — Docker uses the value at container creation time, it doesn't re-read the file on each start. nginx -s reload inside a container reads nginx.conf via bind mount (which is why it works with python3/tee in-place editing), but --maxmemory is a Redis startup argument, not a dynamically re-read config.
Why nginx -s reload wasn't enough
In another container in the same stack (the WAF running nginx), there was a similar problem: User-Agent configurations added via sed -i to the host's waf.conf file weren't appearing inside the container after nginx -s reload.
nginx re-reads its configuration files via bind mount when it receives SIGHUP (nginx -s reload). But it only works if the bind mount still points to the correct inode. When sed -i creates a new inode for waf.conf, Docker's bind mount keeps pointing to the old inode — and nginx re-reads the old file.
# WRONG pattern: sed -i + nginx -s reload (doesn't work with bind mounts)
sudo sed -i 's/old-value/new-value/' /home/developer/webserver/waf.conf
docker exec app-firewall nginx -s reload # reads old inode — change ignored
# CORRECT pattern option 1: python3 preserves the inode (writes to same file)
python3 -c "
with open('/home/developer/webserver/waf.conf', 'r') as f:
content = f.read()
content = content.replace('old-value', 'new-value')
with open('/home/developer/webserver/waf.conf', 'w') as f:
f.write(content)
"
docker exec app-firewall nginx -s reload # now works — same inode, new content
# CORRECT pattern option 2: recreate the container
docker compose up -d --force-recreate app-firewallThe definitive fix: recreate the container
The solution for Redis was simple — recreate the container so it would read the updated docker-compose.yml:
cd /opt/myapp && sudo docker compose up -d cache
# Post-recreate verification:
docker inspect cache --format "{{.Config.Cmd}}"
# [valkey-server --maxmemory 1024mb --maxmemory-policy allkeys-lru]
# ^^^^^^ now 1024mb
docker exec cache valkey-cli config get maxmemory
# 1) "maxmemory"
# 2) "1073741824" ← exactly 1 GBWith the container recreated, Redis started using 1 GB maxmemory. The memory result was immediate:
# Before/after comparison after recreating the cache container
# Metric Before After
# Redis maxmemory 2 GB 1 GB
# Redis actual usage 468 MB 458 MB
# Free RAM 370 MB 648 MB
# Available ~400 MB 985 MB
# Swap in use 1.2 GB 88 MB648 MB free against 370 MB before — a difference that means the gap between OOM crash and stable server. The reboot loop stopped immediately.
The general rule: sed -i + bind mount = change that doesn't persist
This is a principle that applies to the entire Docker stack with bind mounts:
Any tool that creates a new file when editing (sed -i, awk, cp, mv over the original) will create a new inode and Docker's bind mount will keep pointing to the original inode. The change exists on the host but is invisible to the container.
Tools that preserve the inode (edit in-place):
python3 open(path, 'w') — truncates and rewrites the same inode
tee — writes to the existing file descriptor
perl -pi -e — edits in-place preserving inode (unlike sed -i on some systems)
vim / nano — depending on config, may or may not preserve the inode
The safest and most explicit way to ensure a change reaches the container:
# Verify the change is in the host file
grep 'new-value' /path/to/config.conf
# Verify the container sees the change
docker exec <container> cat /path/to/config.conf | grep 'new-value'
# If the outputs differ: recreate the container
docker compose up -d --force-recreate <service>The full context: t4g.medium with 2 GB Redis
The original configuration of --maxmemory 2048mb on a 3.4 GB total server was unsustainable from the start. With Redis using 2 GB, only 1.4 GB remained for all the other containers (PHP-FPM, nginx, WAF, CrowdSec) plus the OS. Under normal traffic, the system was on the edge of OOM; under scraper traffic, it crossed the line.
The operational rule for sizing Redis on shared servers: Redis should not use more than 30-40% of total RAM when there are multiple containers. For a t4g.medium with 3.4 GB, the healthy limit is approximately 1 GB — which is what the fix established.
The incident had two layers: the immediate cause (Redis with 2 GB maxmemory on a 3.4 GB server) and the hidden cause (sed -i creating a new inode, fix apparently applied but never effective). The server was rebooting because the fix never reached the container. OOM diagnoses that don't check docker inspect don't reach the real cause.
Lesson: always verify docker inspect after changing container config
The correct workflow for any configuration change in Docker containers:
1. Edit the configuration file on the host (using python3/tee to preserve inode, or accepting that you'll need to recreate)
2. Verify the host file has the correct value (grep/cat)
3. Verify the container sees the correct value (docker exec + cat, or docker inspect for startup args)
4. If the values differ: recreate the container (docker compose up -d <service>)
Step 3 is what most people skip — and it's exactly what would have prevented the OOM loop in this case. The file was correct. The container was not.
