Running Unsloth in Docker with OpenAI-Compatible API Relay and Unbuffered SSE
When testing unslothai/unsloth for local LLM inference and fine-tuning, I needed a production-grade Docker deployment that could:
- Run the Unsloth container with GPU pass-through
- Expose an OpenAI-compatible API for downstream tools (Claude Code, Codex, etc.)
- Integrate third-party API relays (OpenRouter, DeepSeek) as fallback routes
- Prevent SSE (Server-Sent Events) buffering at the reverse proxy layer
This post documents the exact configs that worked in my homelab.
Docker Compose with Health Checks and GPU Support
Here's the minimal docker-compose.yml for running Unsloth with NVIDIA GPU runtime:
version: '3.8'
services:
unsloth:
image: unsloth/unsloth:latest
container_name: unsloth
restart: unless-stopped
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=all
- UNSLOTH_API_KEY=${UNSLOTH_API_KEY}
- OPENAI_BASE_URL=http://localhost:8000/v1
- OPENAI_API_KEY=${RELAY_API_KEY}
ports:
- "8000:8000"
volumes:
- ./models:/root/.cache/huggingface
- ./config:/app/config
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
Key points:
-
runtime: nvidia+NVIDIA_VISIBLE_DEVICES=allensures GPU access - Health check probes
/healthendpoint (adjust if Unsloth uses a different path) - Volumes persist model weights and config across restarts
-
OPENAI_BASE_URLallows chaining to external relays (e.g., OpenRouter)
Caddy Reverse Proxy with Unbuffered SSE
To prevent response buffering (critical for streaming LLM outputs), here's the Caddyfile:
unsloth.local {
reverse_proxy localhost:8000 {
flush_interval -1
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
}
}
Why flush_interval -1 matters:
By default, reverse proxies buffer responses to optimize throughput. For SSE endpoints (/v1/chat/completions with stream=true), buffering causes 10–30 second delays before the first token appears. Setting flush_interval -1 disables buffering.
Nginx Alternative (if you prefer Nginx)
server {
listen 80;
server_name unsloth.local;
location / {
proxy_pass http://localhost:8000;
proxy_buffering off;
proxy_set_header X-Accel-Buffering no;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
}
}
Critical directives:
proxy_buffering off;X-Accel-Buffering: no;-
chunked_transfer_encoding off;(prevents Nginx from rechunking the stream)
Verifying Unbuffered Streaming from Terminal
Once deployed, test that tokens stream immediately:
curl -N -X POST http://unsloth.local/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $UNSLOTH_API_KEY" \
-d '{
"model": "unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL",
"messages": [{"role": "user", "content": "Count to 10 slowly"}],
"stream": true
}'
Expected behavior:
You should see data: {"choices":[...]} lines appear in real-time, not all at once after 10+ seconds.
If responses are buffered, double-check:
- Caddy/Nginx flush settings
- Docker
healthcheckisn't blocking traffic during startup - Upstream relay (if used) supports streaming
Integrating Third-Party API Relays
To route requests through OpenRouter or similar gateways, set OPENAI_BASE_URL in the container:
OPENAI_BASE_URL=https://openrouter.ai/api/v1
OPENAI_API_KEY=sk-or-v1-...
Unsloth's OpenAI-compatible server will forward requests to the specified base URL, allowing you to:
- Use models not available locally (Claude, GPT-4, etc.)
- Offload compute during peak load
- A/B test local vs. cloud inference
Lessons Learned
-
Health checks prevent premature traffic routing – Without
start_period: 60s, Caddy routes requests before model loading completes, causing 502 errors. -
SSE buffering is silent – Your logs will show 200 OK, but the client waits forever. Always test with
curl -N. - GPU memory fragmentation – Restart the container daily if running multiple models; VRAM doesn't always release cleanly.
Top comments (0)