diff --git a/.dockerignore b/.dockerignore index a4826e7..829858b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,11 +6,12 @@ __pycache__ .git tests -# .env is deliberately NOT ignored - it carries this deployment's configuration -# and the Dockerfile copies it into the image. Excluding it here silently undid -# that: the build succeeded, the container started with no configuration at all, -# and died on the first required setting. +# .env.production carries this deployment's configuration and the Dockerfile +# copies it to /app/.env inside the image, so it must NOT be ignored here. +# Dokploy's own .env (written from the Environment tab, empty when that tab is +# blank) is ignored instead - it is what silently overwrote the committed one. # # The generated sign-in passwords must never be in the image. -SIGNIN_PASSWORDS.txt +.env .env.local +SIGNIN_PASSWORDS.txt diff --git a/.env b/.env.production similarity index 100% rename from .env rename to .env.production diff --git a/.gitignore b/.gitignore index 5a7d3e2..05969e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ -# .env is committed deliberately, at the repo owner's instruction, so the -# deployment does not depend on re-entering config in the Dokploy UI. +# .env.production is committed deliberately, at the repo owner's instruction, so +# the deployment does not depend on re-entering config in the Dokploy UI. +# +# It is NOT named .env, and that matters: Dokploy writes its own .env into the +# build context from the service's Environment tab after cloning, so a committed +# .env is silently replaced (with an empty file when that tab is blank). +# The Dockerfile copies .env.production to /app/.env inside the image. # # The two database secrets are NOT in it - they are set as Dokploy environment # variables, which override the file (settings.py calls load_dotenv() without @@ -9,7 +14,8 @@ # anyone with read access to this repository can mint an admin token, and git # history keeps it after any rotation. Regenerate with # `python scripts/make_auth_secrets.py` if that stops being acceptable. -!.env +!.env.production +.env # Local overrides and the generated sign-in passwords stay out of git. .env.local diff --git a/Dockerfile b/Dockerfile index 8cafec9..89ed8c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,18 +46,22 @@ COPY scripts ./scripts COPY data ./data COPY serve.py . -# The deployment's configuration. settings.py loads it from the backend root - -# app/infrastructure/settings.py resolves parents[2], which is /app here - so it -# has to land next to app/, not inside it. +# The deployment's configuration, landing at /app/.env because settings.py +# resolves it from the backend root - app/infrastructure/settings.py takes +# parents[2], which is /app here - so it must sit next to app/, not inside it. # -# Easy to leave out, and silent when you do: the image builds, the container -# starts with no configuration, and exits on the first required setting with the -# platform reporting only a Bad Gateway. .dockerignore must not exclude it -# either; that alone is enough to reproduce the same failure. +# The source file is named .env.production, NOT .env, and that detail is the +# whole point. Dokploy writes its own .env into the build context from the +# service's Environment tab AFTER cloning the repository. With that tab empty it +# writes an empty file, overwriting the committed one - so `COPY .env .` copied +# a zero-byte file, the container started with no configuration at all, and the +# platform reported only a Bad Gateway. The checkout showed it plainly: every +# file timestamped 08:33, and .env alone at 08:34, 0 bytes. # -# Real environment variables still win over this file (load_dotenv is called -# without override=True), so Dokploy can override any value without a rebuild. -COPY .env . +# Dokploy does not manage .env.production, so it survives. Anything set in the +# Environment tab still wins at runtime, because settings.py calls load_dotenv() +# without override=True and the process environment takes precedence. +COPY .env.production .env # Pristine copies of everything the app also WRITES to, kept at a path that is # never mounted over.