Bare python in a Docker entrypoint picks the wrong interpreter
The knowledge seed failed on every container start although the script was in the image. Bare python resolved to the system interpreter.
TL;DR
Seeder gagal karena entrypoint memanggil python sistem yang tak punya paket proyek, bukan interpreter venv. Solusinya satu baris: panggil interpreter venv dengan path lengkap dan set PYTHONPATH ke root proyek, tetap pakai fallback agar kegagalan seed tidak menghentikan container. Aturannya simpel: kalau ada venv, selalu panggil interpreter venv langsung, jangan python polos.
The CI log said knowledge seed skipped (failed) every time the container started. The script was there, I could see it baked into the image at /app/api/scripts/seed_knowledge.py. And the line in .docker/entrypoint.sh that ran it looked harmless:
python /app/api/scripts/seed_knowledge.py
How can a script that exists in the image fail with ModuleNotFoundError? My first guess was a typo in the path. Nope. The real issue was simpler and dumber: python meant the system interpreter. Not the project's venv, but the base image's /usr/bin/python, which has zero packages from the project installed. So when the seeder tried to import the app's own modules, the system interpreter had no idea where they were. No site-packages from the venv, nothing from the project root on the search path. Just a clean, empty Python, failing cleanly.
Why bare `python` behaves this way
When you run a script file, the interpreter calculates sys.path using a fixed recipe: the script's own directory gets added first, then the standard library and the site-packages of the Python installation in use [2]. In this case that installation was the system one. The scripts folder ended up on sys.path because that's where the script lives, but the project root did not. And the venv's packages were invisible because the venv's own interpreter was never called [1].
CWD doesn't help either. The docs are clear: the current working directory only goes on sys.path when you're in the interactive interpreter or using the dash c and dash m options [3]. Running a script file by path skips CWD entirely. So my cd /app/api before the call was doing nothing for module resolution.
And no, you can't just pip install -e . on the system Python to patch this up. PEP 668 marks externally-managed base environments (like the ones in Debian/Ubuntu Docker images) as off-limits for bare pip installs [4]. The system Python wants you in a venv. Fair enough, but then you actually have to use the venv's interpreter.
The fix
One line change in .docker/entrypoint.sh:
cd /app/api && PYTHONPATH=/app/api /app/api/.venv/bin/python scripts/seed_knowledge.py || echo skipped
Full path to the venv interpreter. Explicit PYTHONPATH pointing at the project root so imports resolve even if the script's directory isn't /app/api itself. The || echo skipped stays, because a seed failure should not crash the container on startup.
I considered an activation step instead, something like sourcing the venv's activate script before the call. Dropped that idea. Activation is just environment surgery inside one shell, and entrypoint scripts often run in shells where that trick is not guaranteed to work. Calling the venv interpreter by its full path needs none of that ceremony and behaves the same everywhere.
The lazier alternative would be dropping the seed step from the entrypoint entirely. I kept it, because the seed is optional and the failure-silent pattern with || echo skipped matches that: a failed seed logs a line and moves on, it never blocks the container from starting.
The broader pattern
This is one of those Docker Python traps that's easy to hit and hard to debug, because nothing explodes loudly. You get a ModuleNotFoundError tucked behind an echo, and the container starts fine otherwise.
The rule is simple: in any container where a venv exists, never type bare python. Always call the venv interpreter directly. And if the script needs imports from a directory that isn't its own, set PYTHONPATH explicitly rather than hoping the shell's working directory covers it. The docs are specific about when the working directory matters, and running a script file is squarely in the "doesn't" column [3].