DeepSeek Docker Deployment: Compose Setup for the DeepSeek API

You want a containerised application that talks to the DeepSeek API, runs the same on your laptop and on a production VM, and does not leak your API key into a Git history. That is what this DeepSeek Docker deployment guide covers — concrete Dockerfiles, a working docker-compose.yml, and the parts that are easy to get wrong (GPU passthrough for local distills, secrets handling, health checks, and proxy patterns for cost control).
By the end you will have a reproducible setup for two scenarios: a Python service calling DeepSeek’s hosted API, and a self-hosted R1 distill model behind an OpenAI-compatible endpoint.
What you will build

Two deployments, both production-shaped:
- Scenario A — Hosted API client. A small FastAPI service in a container that calls DeepSeek’s hosted
POST /chat/completionsendpoint using the OpenAI SDK. No GPU required. Works with both current API models,deepseek-flashanddeepseek-v4-pro. - Scenario B — Self-hosted distill. A vLLM container serving a DeepSeek R1 Distill model behind an OpenAI-compatible API on your own GPU box, with the NVIDIA Container Toolkit. Useful when you need offline inference or strict data residency.
Both scenarios share the same compose file with profiles, so you can bring them up independently.
Prerequisites
- Docker Engine with the Compose v2 plugin (
docker compose, notdocker-compose). - A DeepSeek API key from the developer console — see get a DeepSeek API key if you have not generated one.
- For Scenario B: an NVIDIA GPU with at least 24 GB VRAM and the NVIDIA Container Toolkit installed on the host. The 7B distill used below ships about 15.2 GB of bf16 weights, which leaves room for the KV cache on a 24 GB card; the 14B distill’s weights are about 29.5 GB and need a larger GPU.
- Linux, macOS, or Windows with WSL2. Most of this is identical across them; GPU passthrough is Linux-first.
- Familiarity with environment variables and a basic Python or Node service. If you are new to the API surface itself, skim the DeepSeek API getting started guide first.
The model IDs you will use
DeepSeek’s API lists two models today: deepseek-flash (served by DeepSeek-V4.1-Flash-1017, the default choice for most workloads and the only current model with image input) and deepseek-v4-pro (served by DeepSeek-V4-Pro-0813). Both have a 1M-token context and up to 384K output tokens.
Thinking mode is a request parameter, not a separate model, and it is on by default. Set the effort with reasoning_effort ("low", "high" or "max") and toggle thinking with extra_body={"thinking": {"type": "enabled"}} or {"type": "disabled"}. The legacy IDs deepseek-chat and deepseek-reasoner were scheduled for retirement on 2026-07-24 at 15:59 UTC and are no longer listed in the official docs; deepseek-v4-flash is still accepted but is now served by DeepSeek-V4.1-Flash-1017. If an older container still sends one of those names, change the model value — base_url stays https://api.deepseek.com. The DeepSeek API documentation guide covers the full request shape.
Project layout
Here is the directory we will build:
deepseek-stack/
├── .env # never committed
├── .env.example
├── docker-compose.yml
├── api-client/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app.py
└── proxy/
└── litellm.config.yaml # optional, for Scenario A+
Step 1 — Write the API client image

Scenario A is a Python 3.12 service. Create api-client/requirements.txt with three pinned dependencies (the latest releases on PyPI as of October 2026):
openai==3.37.0
fastapi==0.162.0
uvicorn[standard]==0.54.0
Then api-client/app.py — a minimal endpoint that forwards a prompt to DeepSeek. Note the explicit base_url; chat requests hit POST /chat/completions, the OpenAI-compatible endpoint. The key is read from a Docker secret file if one is mounted, otherwise from the DEEPSEEK_API_KEY environment variable:
import os
from pathlib import Path
from fastapi import FastAPI, HTTPException
from openai import OpenAI
from pydantic import BaseModel
def load_api_key() -> str:
secret = Path("/run/secrets/deepseek_api_key") # Docker secret (Step 2)
if secret.exists():
return secret.read_text().strip()
return os.environ["DEEPSEEK_API_KEY"]
client = OpenAI(
api_key=load_api_key(),
base_url="https://api.deepseek.com",
)
MODEL = os.environ.get("DEEPSEEK_MODEL", "deepseek-flash")
app = FastAPI()
class Ask(BaseModel):
prompt: str
thinking: bool = False
@app.get("/healthz")
def healthz():
return {"ok": True}
@app.post("/ask")
def ask(body: Ask):
kwargs = {
"model": MODEL,
"messages": [{"role": "user", "content": body.prompt}],
"max_tokens": 8192,
}
if body.thinking:
kwargs["reasoning_effort"] = "high"
kwargs["extra_body"] = {"thinking": {"type": "enabled"}}
else:
# Thinking is on by default, so switch it off explicitly.
kwargs["extra_body"] = {"thinking": {"type": "disabled"}}
kwargs["temperature"] = 1.3 # DeepSeek's guidance for general conversation
try:
resp = client.chat.completions.create(**kwargs)
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
msg = resp.choices[0].message
return {
"content": msg.content,
"reasoning_content": getattr(msg, "reasoning_content", None),
}
The API is stateless — each request must carry the full messages array if you want multi-turn context. The web chat keeps session history; the API does not. When thinking is enabled, the response returns reasoning_content alongside the final content. In thinking mode temperature is accepted but has no effect, which is why the sample sets it only on the non-thinking path.
Now api-client/Dockerfile. Use a slim base, run as non-root, and copy requirements.txt before the source so layer caching survives code edits:
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
RUN useradd --create-home --uid 1000 app
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
RUN chown -R app:app /app
USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz')" || exit 1
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Step 2 — Handle secrets properly
Never bake API keys into images. Two patterns work; pick one:
- Environment variables via
.env(development). Create.env.examplecommitted to Git, and a real.envin.gitignore:# .env.example DEEPSEEK_API_KEY=sk-replace-me DEEPSEEK_MODEL=deepseek-flash # only needed for the optional LiteLLM proxy (Step 5) LITELLM_MASTER_KEY=sk-replace-with-a-long-random-value - Docker secrets (production). Compose mounts each secret as a file at
/run/secrets/<secret_name>, which is the pathload_api_key()checks first. With Compose v2 this works on a single host without Swarm, using afile:source:services: api-client: secrets: - deepseek_api_key secrets: deepseek_api_key: file: ./secrets/deepseek_api_key.txt # keep this file out of Git
For Kubernetes, use a Secret mounted as an env var or projected file. Whatever you do, audit your image with docker history <image> before pushing — keys in build args are visible there.
Step 3 — Compose the stack
Here is the docker-compose.yml that ties everything together. Profiles let you bring up only the API client by default and opt in to the local GPU service when needed. The vLLM service follows vLLM’s own Docker instructions (ipc: host, a mounted Hugging Face cache, an optional HF_TOKEN), and --reasoning-parser deepseek_r1 splits the distill’s reasoning from its answer:
name: deepseek-stack
services:
api-client:
build: ./api-client
image: deepseek-api-client:latest
env_file: .env
ports:
- "8000:8000"
restart: unless-stopped
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz')"]
interval: 30s
timeout: 5s
retries: 3
litellm:
image: ghcr.io/berriai/litellm:v1.106.3
profiles: ["proxy"]
env_file: .env
command: ["--config", "/app/config.yaml", "--port", "4000"]
ports:
- "4000:4000"
volumes:
- ./proxy/litellm.config.yaml:/app/config.yaml:ro
restart: unless-stopped
vllm-distill:
image: vllm/vllm-openai:v0.32.0
profiles: ["gpu"]
ipc: host
ports:
- "8001:8000"
volumes:
- hf-cache:/root/.cache/huggingface
environment:
- HF_TOKEN=${HF_TOKEN:-}
command: >
--model deepseek-ai/DeepSeek-R1-Distill-Qwen-7B
--dtype bfloat16
--max-model-len 32768
--gpu-memory-utilization 0.90
--reasoning-parser deepseek_r1
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
restart: unless-stopped
volumes:
hf-cache:
Bring it up with docker compose up -d --build for Scenario A, or docker compose --profile gpu up -d to add the local distill (Scenario B). The read_only root filesystem and no-new-privileges flag are cheap hardening wins; few applications need to write outside /tmp. Image tags are pinned to the current releases (LiteLLM v1.106.3 and vLLM v0.32.0 as of October 2026); LiteLLM’s deployment docs recommend pinning a version rather than a moving tag.
Step 4 — Verify it worked
Probe the health endpoint and run a real call:
# Health
curl -s http://localhost:8000/healthz
# Round-trip a prompt
curl -s -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"prompt": "Summarise Docker layer caching in two sentences.", "thinking": false}'
For the GPU service, hit it directly — it speaks OpenAI-compatible JSON on port 8001. With the reasoning parser enabled, vLLM returns the distill’s reasoning in a field named reasoning (it used to be called reasoning_content), next to the final content:
curl -s http://localhost:8001/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B","messages":[{"role":"user","content":"hi"}]}'
If you want a deeper local-only path without Docker, the running DeepSeek on Ollama walkthrough covers the same R1 Distill family on a different runtime.
Step 5 — Add a proxy for cost control (optional)
Once you have more than one service calling DeepSeek, route them through an internal proxy. LiteLLM is a small Python service that exposes an OpenAI-compatible API and forwards to multiple backends; its DeepSeek provider takes any DeepSeek model name behind a deepseek/ prefix. A minimal proxy/litellm.config.yaml:
model_list:
- model_name: chat-default
litellm_params:
model: deepseek/deepseek-flash
api_base: https://api.deepseek.com
api_key: os.environ/DEEPSEEK_API_KEY
- model_name: chat-pro
litellm_params:
model: deepseek/deepseek-v4-pro
api_base: https://api.deepseek.com
api_key: os.environ/DEEPSEEK_API_KEY
litellm_settings:
drop_params: true
cache: true
cache_params:
type: local # in-memory; without this LiteLLM expects Redis
Bring it up with docker compose --profile proxy up -d. Now your other services point at http://litellm:4000 instead of https://api.deepseek.com and authenticate with the LITELLM_MASTER_KEY (or a virtual key issued from it), and you get centralised logging, retries, and per-team budgets without changing call sites. An in-memory cache lives inside one worker process; LiteLLM’s caching docs recommend Redis once you run more than one worker.
Cost math you should run before deploying

Container orchestration does not change pricing — it changes how often you make calls. Before you ship, estimate spend from the three token buckets DeepSeek bills separately: input on a cache hit, input on a cache miss, and output. As of October 2026 the official pricing page lists, per 1M tokens, deepseek-flash at $0.006 cache hit, $0.30 cache miss and $1.20 output in peak hours, and half that off-peak ($0.003, $0.15 and $0.60). deepseek-v4-pro is $0.044, $1.32 and $3.96 at peak, and $0.022, $0.66 and $1.98 off-peak. Peak hours are 01:00–04:00 and 06:00–10:00 UTC, Monday to Friday, excluding Chinese public holidays; every other hour is off-peak.
Two container-specific habits move the bill most: a restart or retry loop that resends a long prompt, and batch jobs scheduled inside peak hours when they could run off-peak. LiteLLM’s DeepSeek docs say its cost tracking applies the off-peak rate automatically. The DeepSeek API pricing reference has worked examples, and the DeepSeek context caching guide explains how cache hits are detected.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
401 Unauthorized from DeepSeek |
Empty DEEPSEEK_API_KEY at runtime |
Run docker compose config to confirm the variable is interpolated; check .env sits next to docker-compose.yml |
| Container exits immediately | Healthcheck or app crash on import | docker compose logs api-client; pin SDK versions; confirm Python version |
could not select device driver "nvidia" |
NVIDIA Container Toolkit not installed | Install nvidia-container-toolkit on the host and restart Docker |
| Empty content from JSON mode | Truncation or missing schema hint | Set max_tokens high; include the word “json” plus a short example schema in the prompt — see DeepSeek API JSON mode |
| Streaming stalls behind a load balancer | Proxy buffering server-sent events | Disable response buffering on the proxy; for nginx, set proxy_buffering off |
| Out-of-memory on vllm-distill | Context length too high for VRAM | Lower --max-model-len or pick a smaller distill |
Production hardening checklist
- Pin image digests (
image: vllm/vllm-openai@sha256:...) for reproducible deploys. - Set resource limits with
deploy.resources.limitsso a runaway container cannot starve the host. - Log structured JSON from your application; ship logs with the Docker JSON-file driver or a sidecar.
- Rotate API keys regularly and use separate keys per environment.
- Monitor token spend with the LiteLLM proxy or your own middleware — a retry loop can multiply calls far faster than real users do.
- Back up the Hugging Face cache volume if you self-host; redownloading model weights takes time you do not want during an incident.
Where this fits in your stack
If you are building a chatbot, the same image pattern wraps cleanly into a DeepSeek Discord bot service. For retrieval-augmented workflows, point the container at a vector store and follow the DeepSeek RAG tutorial. Teams shipping internal tools should also read the DeepSeek API best practices notes on retries, idempotency, and timeouts before going live.
Next steps

- Browse other DeepSeek tutorials for adjacent integrations (LangChain, LlamaIndex, VS Code).
- If you would rather skip Docker for local development, the install DeepSeek locally guide covers bare-metal options.
Frequently asked questions
How do I pass my DeepSeek API key into a Docker container safely?
Use an .env file referenced by env_file: in your compose file for development, and Docker secrets or a Kubernetes Secret in production. Never bake the key into the image with ENV or build args — it stays visible in docker history. For full setup steps see the get a DeepSeek API key guide and the DeepSeek API authentication reference.
What model ID should I use in my Dockerised app today?
Use deepseek-flash (DeepSeek-V4.1-Flash-1017) as the default; it is also the only current API model that accepts images. deepseek-v4-pro (DeepSeek-V4-Pro-0813) is the second option and costs 3.3× as much per output token. The legacy IDs deepseek-chat and deepseek-reasoner were scheduled for retirement on 2026-07-24 at 15:59 UTC, so any image still sending them needs its model value changed. The DeepSeek V4-Flash page covers the Flash line in detail.
Can I run DeepSeek V4 itself inside a Docker container on my own GPU?
Not on a single workstation. The open weights of DeepSeek-V4.1-Flash-1017 are about 510 GB and those of DeepSeek-V4-Pro-0813 about 893 GB on Hugging Face, and vLLM’s recipe for V4.1-Flash lists roughly 614 GB of GPU memory as the minimum. For self-hosted Docker deployments on one GPU, use the smaller DeepSeek R1 Distill family (1.5B to 70B) served by vLLM, and use the hosted API for the current models. The DeepSeek offline setup guide covers the multi-GPU options, and the DeepSeek system requirements page lists VRAM needs by model size.
Does Docker change how DeepSeek’s API behaves at runtime?
No. The container is just a network client. The API is still stateless — your code must resend the full messages array on every POST /chat/completions call to maintain multi-turn context, regardless of where the client runs. For request-shape details see the DeepSeek API documentation and DeepSeek OpenAI SDK compatibility notes.
Why use a LiteLLM proxy container instead of calling DeepSeek directly?
A proxy gives you one place for retries, caching, key rotation, per-team budgets, and structured logs — without editing every service that calls the API. It also makes it simple to fail over to a self-hosted distill or another provider during incidents. For more on retry and timeout patterns, see the DeepSeek API best practices guide alongside other step-by-step DeepSeek guides.
Sources & Methodology
Primary sources
- Model cardDeepSeek on Hugging FaceWeights, license, usage notes per modelLast checked: October 25, 2026
- Official documentationDeepSeek API docs — Your First API CallModel names deepseek-flash / deepseek-v4-pro, base_url, thinking and reasoning_effort fields, key from DEEPSEEK_API_KEYLast checked: October 25, 2026
- Official documentationDeepSeek API docs — Thinking ModeThinking on by default, extra_body toggle, reasoning_content, temperature has no effect in thinking modeLast checked: October 25, 2026
- Official documentationDeepSeek pricing pagePeak and off-peak per-1M-token rates, peak hours, 1M context / 384K outputLast checked: October 25, 2026
Technical references
- Technical reportDeepSeek-R1 paper (arXiv:2501.12948)R1 reasoning approach, distillation resultsLast checked: October 25, 2026
- Runtime documentationvLLM docs — Using Dockervllm/vllm-openai image, --ipc=host, HF_TOKEN, Hugging Face cache mountLast checked: October 25, 2026
- Runtime documentationvLLM docs — Reasoning Outputs--reasoning-parser deepseek_r1; reasoning field (formerly reasoning_content)Last checked: October 25, 2026
- Tool documentationLiteLLM docs — DeepSeek providerdeepseek/ prefix, deepseek-flash and deepseek-v4-pro support, off-peak cost trackingLast checked: October 25, 2026
- Tool documentationLiteLLM docs — Cachingcache: true defaults to Redis; cache_params type localLast checked: October 25, 2026
- Tool documentationLiteLLM docs — Production deploymentghcr.io/berriai/litellm image, pin a version tagLast checked: October 25, 2026
- Tool documentationDocker docs — Secrets in ComposeSecrets mounted at /run/secrets/<name>, file: sourceLast checked: October 25, 2026
- Tool documentationDocker docs — GPU support in Composedeploy.resources.reservations.devices syntaxLast checked: October 25, 2026
- Model cardDeepSeek-R1-Distill-Qwen-7B (Hugging Face)Weights size (15.2 GB safetensors), MIT licenceLast checked: October 25, 2026
Methodology
Information in this article was checked against the official DeepSeek documentation, the article's cited primary sources, and the deepseekai.guide pricing dataset on the date shown above. Where third-party numbers are quoted, the originating source is linked inline so the reader can verify and re-check.
Data confidence
High for facts cited inline with primary-source links; medium for context narrative.
Editorial note
AI tooling moves quickly. If you spot an outdated number or a broken link, the contact link in the footer is the fastest way to flag it for the editorial team.
