Skip to content

Why a manual docker exec led to an automated Ollama entrypoint setup

After the first successful start of my RAG system, I thought it was finished. Then I saw a docker exec step in the Quick Start and could not leave it alone. How a small source of friction led to a better architecture.

After the first successful start of my RAG system, I thought it was finished. Then I saw a docker exec step in the Quick Start and could not leave it alone. How a small source of friction led to a better architecture.

Contents

The Quick Start that bothered me

The system was running. Docker Compose started. Backend connected. The Chrome Extension sends metadata, Qdrant stores embeddings. First success.

Then I had Copilot write the README and stumbled over these lines:

# after first start: pull models manually
docker exec ollama_local ollama pull nomic-embed-text
docker exec ollama_local ollama pull llama3.2

Copilot wrote the lines. I committed them.

This is where most developers move on. I did not.

Not because I am pedantic. But because I eventually want to share this project publicly. And because every additional line in the Quick Start represents someone who closes the project again.

What is the actual problem?

On the surface: two extra commands after startup.

Underneath: a fundamental break in the mental model.

docker-compose up means the system is ready. Finished. It works now.

If manual steps are still needed afterward, such as pulling models, restarting services or adjusting configuration, the first command is lying. The system is not ready. It has started halfway and is waiting for the developer to finish the rest.

That is technical debt. Not in the form of bad code, but implicit knowledge documented nowhere except in the Quick Start nobody reads completely.

The entrypoint as the place for initialization

The solution was obvious once I stopped framing the problem incorrectly.

Docker containers have entrypoints. Entrypoints run at startup. If initialization logic should run at startup, why not put it there?

I created a custom ollama/Dockerfile:

FROM ollama/ollama:latest

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

The official ollama/ollama:latest image does not contain curl. Without curl, waiting for the API is not possible. So curl is installed. Three lines.

The entrypoint.sh script

#!/bin/bash
set -e

MODELS=("nomic-embed-text" "llama3.2")

# start server in background
ollama serve &
OLLAMA_PID=$!

# wait for API
echo "Waiting for Ollama API..."
until curl -sf http://localhost:11434/api/tags > /dev/null 2>&1; do
  sleep 1
done
echo "Ollama API ready."

# idempotently pull models
for model in "${MODELS[@]}"; do
  if ollama show "$model" > /dev/null 2>&1; then
    echo "Model '$model' already present. Skipping."
  else
    echo "Pulling model '$model'..."
    ollama pull "$model"
    echo "Model '$model' pulled successfully."
  fi
done

# keep main process alive
wait $OLLAMA_PID

The script has three parts. Each solves a specific problem.

Part 1: Start the server in the background. ollama serve &. The & is decisive. Without it, the process blocks and the script never reaches the models. Save the PID with OLLAMA_PID=$!. It is needed at the end.

Part 2: Wait for the API. Ollama needs a second to start. If ollama pull runs immediately, the API is not ready and the command fails. The until loop checks every second whether /api/tags responds. Only then does it continue.

Part 3: Check models idempotently. ollama show "$model" returns exit code 0 if the model exists, nonzero otherwise. No pull unless needed. On the second start, with models in the volume, the loop skips everything. Startup in seconds, not minutes.

And then: wait $OLLAMA_PID. If the script ends here, the container ends. wait keeps the container alive as long as ollama serve runs. And it forwards signals correctly: docker stop works cleanly.

Idempotence

The term sounds cumbersome. Its meaning is simple: running the same command multiple times has the same effect as running it once.

First start: no models, download.

Second start: models present, skip.

After docker-compose down without -v: models in the volume, skip.

After docker-compose down -v: volume deleted, download.

That is the expected behavior. No “I forgot to pull the models.” No “Why is it downloading again?” The system knows what it has and responds accordingly.

The result in docker-compose.override.yml

Instead of:

ollama:
  image: ollama/ollama:latest

Now:

ollama:
  build: ./ollama

One line changed. Completely different startup behavior.

The complexity lives in the container, not the documentation.

Flowchart of the Ollama entrypoint script with three phases: start the server, wait for the API and check models Figure: The script runs automatically at container startup: start the server, wait for the API, check models idempotently and download them if needed. The original illustration retains its German labels.

What this moment says about developer experience

Product development has a term: time to value. The time from first contact to the first “It works!” moment.

For developer tools, that is time to first docker-compose up.

Every Quick Start line after up extends time to value. Every manual step is an obstacle. Not because developers are lazy, but because obstacles accumulate. Two extra commands, a restart and an unclear error message turn a five-minute setup into a thirty-minute puzzle.

I am building this system for myself. But I build it as if it were for others. Because in three months, “I” will also be someone who no longer remembers what I was thinking.

The underlying principle

Make the right thing easy and the wrong thing hard.

Here, the right thing is for the system to run completely on the first docker-compose up --build. The wrong thing is having to intervene manually after startup.

By moving initialization logic into the container, the right thing becomes automatic. No developer can forget to pull the models because there is no step to forget.

This is not overengineering. It is respect for other people’s time.

All articles in the series

  1. Vision and system overview: Chrome Extension, RAG architecture and project background: Read article
  2. Building a RAG system: Qdrant, embeddings and cosine similarity in TypeScript: Read article
  3. AI provider abstraction: Ollama vs. OpenAI, interface design and no vendor lock-in: Read article
  4. Chrome Extension MV3: Three isolated runtime contexts, message passing and Strategy Pattern: Read article
  5. Docker Compose strategy: The override pattern, from local development to Azure: Read article
  6. Ollama locally vs. Docker: The decision and its consequences: Read article
  7. Ollama auto-pull entrypoint: Automated model setup at container startup: (this article)
  8. tsconfig and Vite: Node16 vs. bundler, and why Vite has its own rules: Read article
  9. Load complete Instagram captions with MutationObserver: Read article
  10. Chrome Extension foundation with health indicator and retry queue: Read article
  11. Phase 2 features: Shadow DOM overlay, Tailwind v4 and duplicate detection: Read article
  12. Race condition in platform detection: How a UI event breaks Instagram detection: Read article
  13. Extracting PostId in two Instagram layouts: querySelector vs. ancestor traversal: Read article
  14. Capture a complete Instagram carousel with MutationObserver: Lazy loading, observer-before-click and timeout fallback: Read article
  15. Notes and tags when saving screenshots: Read article
  16. Instagram keyboard shortcuts block Chrome Extension input: Read article
  17. Lowercase normalization and duplicate detection in tag input: Read article
  18. Zitadel Login V2 in Docker Compose with three hidden errors: Read article
  19. PKCE OAuth in a Chrome MV3 extension: Read article
  20. React frontend with react-oidc-context and Zitadel: Read article
  21. Vite build-time environment variables in Docker: Read article
  22. Event-driven ingestion with BullMQ and Redis: Read article
  23. MinIO instead of Azurite: S3-compatible object storage locally and on Hetzner: Read article
  24. access_token, id_token and the Userinfo endpoint: Which belongs where: Read article
  25. Qdrant multi-tenancy with one collection per user: Read article
  26. When backend and frontend know different types: Read article
  27. Removing Zitadel bootstrap: Host header bug and manual setup: Read article
  28. Backend code review with six problems fixed before launch: Read article
  29. Traefik instead of NGINX: Reverse proxy for a growing Docker Compose stack: Read article
  30. Two-layer rate limiting: Traefik and express-rate-limit with Redis: Read article
  31. Implementing GDPR Article 17 correctly: Promise.allSettled and export batching: Read article
  32. Embedding model lock-in: Why mxbai-embed-large is a permanent production decision: Read article
  33. Docker volumes in production: Named volumes, bind mounts and the Hetzner volume approach: Read article
  34. Two vulnerabilities before launch: Redis without authentication and an exposed Qdrant admin port: Read article
  35. Traefik as the single entry point in the Docker Compose stack: Read article
  36. Connecting Zitadel behind Traefik with issuer, JWKS and Login V2: Read article
  37. Fixing the frontend when the nginx healthcheck fails on localhost: Read article
  38. Observability for my Docker Compose stack with Bull Board and Dozzle: Read article
  39. Open the Qdrant dashboard securely with local Traefik and an SSH tunnel: Read article
  40. Diagnosis of chunking failures despite tokenization: Read article
  41. Why I set the chunk size to 1500 tokens: Read article
  42. Splitting the embedding workflow into clear steps: Read article
  43. Bringing chunking, storage and search back together: Read article
  44. Capture Intake as a separate module, less orchestration in the Express endpoint: Read article

Building a system with local AI inference and want to minimize setup effort for new developers? Let us assess it together.

Related posts

What should your portal do?

Describe your workflow. Together, we will identify the right next step.

Discuss your workflow