sed for Emergency Debugging: When vim and nano Are Not an Option
It was 11 PM. I was debugging self-hosted PostgreSQL HA setup and vim wasn't available in the container. This is the story of how sed became my go-to tool for quick fixes in production environments.
It was 11 PM on a weekday. I was debugging an issue with our company's PostgreSQL HA setup running in Docker containers. The error logs pointed to a misconfigured pg_hba.conf, and I needed to add a new IP to the authentication rules.
I SSH'd into the container, ran which vim... nothing. which nano... nothing. I sat there for a second, heart rate rising, and then I remembered: sed is always there.
This is the story of how I stopped fearing sed and started using it for every quick fix in production.
The Problem: Minimal Images Don't Have Editors
Production container images are built for size, not developer convenience. Alpine, Debian slim — they ship with the essentials, not vim or nano.
# Check what you actually have inside a container
docker exec postgres-primary which vim
# (nothing)
docker exec postgres-primary which nano
# (nothing)
docker exec postgres-container which sed
/usr/bin/sedThis isn't a bug. It's a deliberate choice. But when you need to make a quick change at 11 PM while the team is waiting, you work with what you have.
What sed Actually Does
sed works like a conveyor belt — it reads each line, applies your instruction, and moves on. No GUI, no interactive mode, just stream processing.
sed 's/old/new/' file
# Reads line 1 → applies s/old/new/ → prints result
# Reads line 2 → applies s/old/new/ → prints result
# ...continues until endOnce I understood this, everything clicked. sed isn't a text editor — it's a line transformer. And that's exactly what you need in production at 11 PM.
The Commands I Actually Use
After months of emergency debugging at work, I've narrowed it down to five commands I use repeatedly. These handle 95% of what I need.
1. Substitute — s/old/new/
Find and replace text on matching lines:
# Change max_connections from 100 to 500
sed '/max_connections/s/100/500/' postgresql.conf
# Make it global (all occurrences per line)
sed 's/100/500/g' postgresql.conf
# Print only what changed (dry-run)
sed -n 's/100/500/p' postgresql.confThe pattern I use most: sed '/PATTERN/s/old/new/' file
- First, find lines containing PATTERN
- Then, replace old with new on those lines only
2. Delete — d
Remove lines by pattern, number, or range:
# Delete empty lines
sed '/^$/d' file
# Delete lines containing "debug"
sed '/debug/d' file
# Delete lines NOT matching (keep only matches)
sed '/ERROR/!d' file
# Delete specific line number
sed '42d' file
# Delete a range
sed '10,20d' file3. Insert Before — i\
Add a line before matching lines:
# Insert a comment before max_connections
sed '/max_connections/i\# Modified by SRE' postgresql.conf
# Insert at specific line number
sed '5i\# Configuration header' file4. Append After — a\
Add a line after matching lines:
# Add new IP to pg_hba.conf after localhost entry
sed '/^host.*all.*all.*127.0.0.1\/32/a host all all 10.20.30.40/32 md5' pg_hba.conf
# Append after line matching listen_addresses
sed '/listen_addresses/a\port = 5433' postgresql.conf5. Change Line — c\
Replace the entire matching line with new content:
# Replace max_connections line entirely
sed '/^max_connections/c max_connections = 500 # Tuned' postgresql.conf⚠️ Watch out: Unlike s/ which edits text, c\ replaces the entire line. Use pattern matching to be safe.
Targeting Specific Lines: Address Types
Without an address, sed applies to ALL lines. Here's how I target exactly what I need:
# Line number only (line 42)
sed '42s/old/new/' file
# Line range (lines 10 to 20)
sed '10,20s/old/new/' file
# Pattern match (lines with "max_connections")
sed '/max_connections/s/old/new/' file
# From line 50 to end of file
sed '50,$s/old/new/' file
# Range between patterns
sed '/^# Authentication/,/^$/s/old/new/' fileIn-Place Editing: The -i Flag
By default, sed prints to screen and leaves the file unchanged. Use -i to modify the file directly:
# ⚠️ DANGEROUS - no backup
sed -i 's/old/new/' file
# ✅ RECOMMENDED - creates .bak backup
sed -i.bak 's/old/new/' file
# If things go wrong, rollback instantly
mv file.bak fileThis is the first thing I tell anyone: Always use -i.bak. Never plain -i. The backup is your safety net.
Multiple Changes at Once
Real configs need multiple fixes. Chain commands with -e:
sed -i.bak \
-e 's/max_connections = 100/max_connections = 500/' \
-e '/listen_addresses/a\port = 5433' \
-e '/WARNING/s/^/# /' \
file.confI use this all the time when patching a config. One command, one backup, all changes applied atomically.
Special Characters: The Trap That Cost Me an Hour
Dots (.) match ANY character in sed patterns. For literal dots, escape them:
# Wrong: dots in IP match anything
sed 's/10.0.0.1/192.168.1.1/' pg_hba.conf
# Matches "10X0Y0Z1" too!
# Correct: escaped dots match literally
sed 's/10\.0\.0\.1/192.168.1.1/' pg_hba.confWhen patterns contain /, use alternate delimiters:
# Instead of escaping every /
sed 's|/etc/postgresql|/opt/postgresql|' file
# Or use | (pipe)
sed 's|path/to/file|new/path|' fileThe First-Occurrence Trick: 0,/pattern/
By default, sed changes ALL matching lines. But sometimes you only want the first:
# Change only the first "trust" to "md5"
sed '0,/trust/s/trust/md5/' pg_hba.confThe 0,/pattern/ syntax means: "Find the first line matching PATTERN, apply the substitution, then stop."
Why My Changes Didn't Persist After Restart (The Story)
This cost me an hour of debugging at work one night:
docker exec postgres sed -i 's/max_connections = 100/max_connections = 500/' /var/lib/postgresql/data/postgresql.conf
docker restart postgres
# Check config... still 100?!The problem: Many Docker images regenerate their configs at startup from environment variables. My sed change was there, but the startup script overwrote it.
The fix depends on your situation:
-
Use environment variables (if the image supports them):
environment: POSTGRES_MAX_CONNECTIONS: 500 -
Mount custom config from host:
volumes: - ./custom.conf:/etc/postgresql/custom.conf:ro -
Use init scripts for one-time setup.
Knowing this saved me from many hours of "why isn't this working?"
Reloading Config Without Restart
Some PostgreSQL settings can be reloaded without restarting:
# Reload PostgreSQL config
docker exec postgres psql -U postgres -c "SELECT pg_reload_conf();"Settings that reload without restart: log_line_prefix, log_statement, listen_addresses.
Settings that require restart: max_connections, shared_buffers, port, wal_level.
My Go-To Patterns
After months of emergency debugging at work, these are the commands I keep in my muscle memory:
# Comment out a line
sed '/PATTERN/s/^/# /' file
# Uncomment a line (remove leading #)
sed '/^# PATTERN/s/^# //' file
# Change a specific value
sed "s/^PATTERN = .*/PATTERN = NEW_VALUE/" file
# Add new line after pattern
sed '/PATTERN/a\NEW_LINE' file
# Delete lines matching pattern
sed '/PATTERN/d' file
# Replace entire line
sed '/PATTERN/c\REPLACEMENT' fileSafety Checklist
Before editing any production file:
- Always backup:
sed -i.bak '...' file - Dry-run first:
sed -n 's/old/new/p' file(see what changes) - Verify:
grep "new_value" file - Rollback if needed:
mv file.bak file
When sed is Not the Answer
| Use Case | Better Tool |
|---|---|
| JSON files | jq |
| YAML files | yq |
| Complex multi-line | perl |
| Structured config management | ansible |
For quick emergency fixes inside containers, sed is perfect. For repeatable infrastructure, use proper tooling like Ansible.
The Bottom Line
Every developer and SRE should know sed. It's available on virtually every Unix system, works without text editors, and handles the quick fixes you need when you're debugging production at 11 PM.
The five commands I use most: s/ (substitute), d (delete), i\ (insert before), a\ (append after), c\ (change). That's 95% of what I need.
Next time you're stuck inside a container with no editors available, remember: sed is your friend.