Take the following essential steps now to secure your LangGraph AI server, adding the optional measures when relevant.
By Kevin Wells
Docker + Compose
Non-root container user
Read-only root filesystem
Date: 2025-09-30
Essential – do these now
1) Pin exact dependency versions
Pin the versions you tested. For example:
langgraph==0.6.8
langgraph-cli[inmem]==0.4.2
langgraph-checkpoint-sqlite==2.0.11
langchain-core==<your tested version>
Rebuild with docker compose build --no-cache
. This eliminates supply chain drift.
2) Add resource limits and explicit security profiles
Add these to the langgraph
service in your docker-compose.yml
:
pids_limit: 256
mem_limit: 512m
cpus: "1.0"
security_opt:
- no-new-privileges:true
- seccomp=runtime/default
- apparmor=docker-default
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
3) Remove compilers from the runtime image
After pip install
in the Dockerfile:
USER root
RUN apt-get purge -y build-essential && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
USER appuser
4) Keep the Dev API short lived
Run it only when you are using it. Stop when done:
docker compose down
Optional – apply if relevant
5) Restrict loopback access on multi-user hosts
Block other users on the same machine from calling the Dev API on 127.0.0.1:2024 with nftables
. Replace YOUR_USER
with your actual username.
sudo tee /etc/nftables.d/langgraph.nft >/dev/null <<'NFT'
table inet langgraph {
chain output {
type filter hook output priority 0; policy accept;
oif "lo" tcp dport 2024 meta skuid != "YOUR_USER" drop
}
}
NFT
if ! grep -q "/etc/nftables.d" /etc/nftables.conf; then
echo 'include "/etc/nftables.d/*.nft"' | sudo tee -a /etc/nftables.conf
fi
sudo systemctl enable --now nftables
sudo nft -f /etc/nftables.conf
6) Local reverse proxy with Basic Auth
If the Dev API stays up for long sessions, add a localhost reverse proxy that requires authentication. Example with Caddy:
services:
proxy:
image: caddy:2
container_name: langgraph-proxy
ports:
- "127.0.0.1:8443:8443"
command: ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
depends_on:
- langgraph
langgraph:
# your existing service
Caddyfile
:
https://127.0.0.1:8443 {
tls internal
route {
basicauth {
YOUR_USER YOUR_BCRYPT_HASH # generate with: caddy hash-password --plaintext YOUR_PASS
}
reverse_proxy 127.0.0.1:2024
}
}
Note: local processes can still call 127.0.0.1:2024 directly unless you also apply the nftables rule.
7) Dependency locks with hashes
For stronger supply chain integrity, use pip-tools:
python3 -m venv .lock && . .lock/bin/activate
pip install pip-tools
mv requirements.txt requirements.in
pip-compile --generate-hashes --output-file requirements.txt requirements.in
deactivate
8) Image scanning on rebuild
Scan for high and critical issues with Trivy:
trivy image --severity CRITICAL,HIGH --ignore-unfixed --exit-code 1 langgraph-lab-langgraph
9) Host hygiene
Keep Ubuntu and Docker patched. Consider rootless Docker or userns remap when convenient.
10) Operational habits
- Use a dedicated browser profile for Studio.
- Do not mount new host paths into this container.
- Before adding actionful tools, insert a human approval gate and log every invocation.
- When adding API keys, pass via environment variables at runtime and avoid logging them.
Drop-in hardened Compose
Replace your service block with this version. It includes limits, security profiles, bounded logs, and a Python-based healthcheck.
services:
langgraph:
build: .
container_name: langgraph-lab
ports:
- "127.0.0.1:2024:2024"
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,nodev
- /app/.langgraph_api:rw,noexec,nosuid,nodev
cap_drop: ["ALL"]
security_opt:
- no-new-privileges:true
- seccomp=runtime/default
- apparmor=docker-default
user: "1000:1000"
environment:
- PYTHONPATH=/app
- PYTHONDONTWRITEBYTECODE=1
- LANGSMITH_TRACING=false
volumes:
- ./app.py:/app/app.py:ro
- ./langgraph.json:/app/langgraph.json:ro
- ./state:/state:rw
pids_limit: 256
mem_limit: 512m
cpus: "1.0"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:2024/ok', timeout=2).read() else 1)"]
interval: 30s
timeout: 3s
retries: 5
restart: "no"