A Beginner’s Guide to Running LangGraph Agentic AI Server in Docker

This article describes a practical, locked-down workflow for installing a LangGraph development sandbox inside Docker on Ubuntu. It uses localhost only and with minimal echo graph.  

By Kevin Wells

Tested stack – example only:

Ubuntu 24.04 LTS
Docker 28.x
Compose v2.39.x
LangGraph 0.6.8
langgraph-cli 0.4.2
langgraph-checkpoint-sqlite 2.0.11
langchain-core pinned
Date: 2025-09-30

Pin these versions for reproducibility in your environment and check for newer patches periodically.

Summary

This guide explains how to build a beginner friendly LangGraph dev environment in a Docker container.

You will have:

  • A minimal echo graph compiled and runnable.
  • A Docker Compose service with strict defaults.
  • The LangGraph Dev API reachable on 127.0.0.1:2024, with Studio connected.
No prior LangChain or LangGraph experience is needed. If you can copy files and run shell commands, then you are ready to go.

Who this is for

  • Linux admins and power users who want a controlled, reproducible sandbox.
  • Builders who do not want to risk mounting their home folder, NFS shares, or using Docker’s root socket.
  • Anyone who wants to learn how to install and configure LangGraph-based agents without risking their system being comprimised.

Architecture

Your Browser -(local)-> LangGraph Studio (web)
                              |
                              v     localhost:2024 (bound only to 127.0.0.1)
                     +----------------------------------------+
                     |         LangGraph Dev API              |
                     |            inside Docker               |
                     |                                        |
                     |  Loads graph from /app                 |
                     |  Runtime scratch in /app/.langgraph_api|
                     +-------------------+--------------------+
                                         |
                                         v
                           Echo graph - test DB at /state/graph.db

Posture: non-root user in container (UID 1000), read-only root filesystem, only two writable pockets (/state and /app/.langgraph_api), port bound to 127.0.0.1, and no mounts of home, NFS, or /var/run/docker.sock.

Project layout

$HOME/langgraph-lab/
├── app.py
├── Dockerfile
├── docker-compose.yml
├── langgraph.json
├── requirements.txt
└── state/                 # writable, owned by UID 1000
mkdir -p "$HOME/langgraph-lab/state"
sudo chown 1000:1000 "$HOME/langgraph-lab/state"
chmod 700 "$HOME/langgraph-lab/state"

Files & configuration

requirements.txt

langgraph==0.6.8
langgraph-cli[inmem]==0.4.2
langgraph-checkpoint-sqlite==2.0.11
langchain-core==<pin your tested version>

Dockerfile

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    LANG=C.UTF-8

# Non-root user
RUN useradd -m -u 1000 appuser
WORKDIR /app

# Minimal OS deps for build
RUN apt-get update && apt-get install -y --no-install-recommends \
      build-essential sqlite3 libsqlite3-dev ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Python deps
COPY requirements.txt /tmp/requirements.txt
RUN python -m pip install -U pip && pip install -r /tmp/requirements.txt

# Remove compilers from runtime
USER root
RUN apt-get purge -y build-essential && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
USER appuser

# App code
COPY app.py /app/app.py

# Writable state dir
USER root
RUN mkdir -p /state && chown -R appuser:appuser /state
USER appuser

# Idle by default - dev server started by compose command
CMD ["bash","-lc","sleep infinity"]

docker-compose.yml

services:
  langgraph:
    build: .
    container_name: langgraph-lab
    ports:
      - "127.0.0.1:2024:2024"    # localhost only
    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  # tracing off by default
    volumes:
      - ./app.py:/app/app.py:ro
      - ./langgraph.json:/app/langgraph.json:ro
      - ./state:/state:rw
    # Resource and log controls
    pids_limit: 256
    mem_limit: 512m
    cpus: "1.0"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    # Healthcheck uses Python - no wget needed
    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"

langgraph.json

{
  "dependencies": ["."],
  "graphs": {
    "echo": "/app/app.py:app"
  }
}

app.py

from typing import TypedDict, Annotated, List
from operator import add

from langgraph.graph import StateGraph, END
from langchain_core.messages import HumanMessage, AIMessage, BaseMessage

# State
class State(TypedDict):
    messages: Annotated[List[BaseMessage], add]

# Node
def model_node(state: State):
    last = next((m.content for m in reversed(state["messages"]) if isinstance(m, HumanMessage)), "")
    return {"messages": [AIMessage(content=f"ECHO: {last}")]}

# Graph factory
def build_graph():
    g = StateGraph(State)
    g.add_node("model", model_node)
    g.set_entry_point("model")
    g.add_edge("model", END)
    return g

# Exported graph for Dev API - no custom checkpointer
app = build_graph().compile()

if __name__ == "__main__":
    # Local test runner with SQLite checkpointer
    import sqlite3
    from langgraph.checkpoint.sqlite import SqliteSaver
    conn = sqlite3.connect("/state/graph.db", check_same_thread=False)
    app_local = build_graph().compile(checkpointer=SqliteSaver(conn))
    cfg = {"configurable": {"thread_id": "demo"}}
    out = app_local.invoke({"messages": [HumanMessage(content="Hello, graph.")]}, cfg)
    print(out["messages"][-1].content)

Build & boot

cd "$HOME/langgraph-lab"
docker compose up -d --build

# One-off script sanity test
docker compose exec -T langgraph bash -lc 'python /app/app.py'
# Expect: ECHO: Hello, graph.

Start the Dev API

Note: the server binds inside the container to 0.0.0.0, but Docker publishes it only on 127.0.0.1 due to the port mapping. Do not expose the server beyond localhost.

Interactive session

docker compose exec -T langgraph bash -lc \
  'cd /app && langgraph dev --host 0.0.0.0 --port 2024 --no-browser'

Verification

curl -sS http://127.0.0.1:2024/ok
curl -sS http://127.0.0.1:2024/info

Use it

Studio UI

https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024

Create a Thread, select assistant echo, send a message. The LangSmith banner is informational. Tracing is off by default here.

REST API

BASE=http://127.0.0.1:2024
THREAD_JSON=$(curl -sS -X POST "$BASE/threads" -H 'content-type: application/json' -d '{}')
THREAD_ID=$(echo "$THREAD_JSON" | sed -n 's/.*"thread_id":"\([^"]*\)".*/\1/p')

RUN_JSON=$(curl -sS -X POST "$BASE/threads/$THREAD_ID/runs" \
  -H 'content-type: application/json' \
  -d '{"assistant_id":"echo","input":{"messages":[{"role":"user","content":"Hello"}]}}')

RUN_ID=$(echo "$RUN_JSON" | sed -n 's/.*"run_id":"\([^"]*\)".*/\1/p')
curl -sS "$BASE/threads/$THREAD_ID/runs/$RUN_ID/join"

Troubleshooting

  • ModuleNotFoundError: langgraph.checkpoint.sqlite – ensure langgraph-checkpoint-sqlite is installed and rebuild.
  • langgraph: command not found – ensure langgraph-cli[inmem] is installed and rebuild.
  • sqlite3.OperationalError: unable to open database file – fix ownership and permissions on ./state, then recreate the container.
  • ValueError: custom checkpointer not allowed – export app = build_graph().compile() without a custom saver. Keep SQLite only in __main__.
  • Address already in use after Ctrl-C – docker compose down then up -d.

Security rules

  • Do not expose the Dev API beyond 127.0.0.1. If you must, then put a reverse proxy with auth and TLS in front.
  • Keep read_only: true, and restrict writable paths to /state and /app/.langgraph_api.
  • Do not mount any additional host paths. Never mount /var/run/docker.sock.
  • Before adding action-based tools, add a human approval gate and default all AI-agent decisions to deny.

 

Maintenance

Start and stop

docker compose up -d
docker compose logs -f
docker compose down

Clean reset

docker compose down -v

Disclaimer: This guide is provided as-is for educational purposes. Always validate all commands in a non-production environment and review security settings for your own risk profile.