feat: Docker self-hosting setup #16
Reference in New Issue
Block a user
Delete Branch "feat/docker-self-hosting"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
docker compose upnow automatically runs Drizzle migrations before starting the APIAUTH_DISABLED=truein docker-compose for development (avoids needing Authentik)/api/to the API service (single-origin, no CORS required)pnpm-lock.yamlso--frozen-lockfileworks in CI builds0000_colossal_colossus.sql) covering all 5 tablesCloses #7
Test plan
docker compose up --buildcompletes without errormigrateservice completes successfully)/api/clientsproxied correctly through nginxAUTH_DISABLED=trueallows API access without a Bearer tokenAUTH_DISABLED=false+ valid OIDC config requires a valid JWT🤖 Generated with Claude Code
PR Review: feat: Docker self-hosting setup
Good work overall — this PR brings together migrations, Docker Compose orchestration, nginx proxying, and the auth bypass in a coherent package. The migration SQL looks correct, the docker-compose service ordering is solid, and the lockfile fix is welcome. I have two blocking issues and several non-blocking notes.
Blocking Issues
1.
AUTH_DISABLEDhas no production guardrail — risk of shipping to prod with auth offFile:
apps/api/src/middleware/auth.ts(lines 27-31 of the new diff)The bypass is a bare
process.envcheck with no further safeguards:This is dangerous because:
docker-compose.ymlships withAUTH_DISABLED: "true"as the default. Anyone who copies the compose file for a "quick deploy" gets a wide-open API.NODE_ENVgate, no startup warning log, and no runtime indication that auth is bypassed.sub: "dev-user"will silently satisfy any downstream code that checksjwtPayload.sub, meaning write operations succeed without identity.Requested changes (pick at least one):
console.warnat startup (not per-request) when the flag is set, e.g."AUTH_DISABLED is true — authentication is bypassed. DO NOT use in production."NODE_ENV !== "production"so that even if someone accidentally sets it, a production build refuses to honour it.AUTH_DISABLED: "true"from the defaultdocker-compose.ymland only set it in adocker-compose.override.ymlordocker-compose.dev.ymlthat is gitignored — so the default shipped config is secure.2. Migration SQL file missing trailing newline
File:
packages/db/migrations/0000_colossal_colossus.sql(last line)The diff shows
\ No newline at end of fileon the final line. Same forpackages/db/migrations/meta/0000_snapshot.jsonandmeta/_journal.json. While the JSON files are generated and harmless, the SQL file could cause issues with tools that concatenate migrations (some runners append statements and a missing newline would mangle the lastALTER TABLEinto the next migration's first statement). Add a trailing newline to at least the.sqlfile.Non-Blocking Notes
3. nginx: missing
X-Forwarded-ProtoheaderFile:
apps/web/nginx.conf(new/api/location block)The proxy block sets
X-Real-IPandX-Forwarded-Forbut notX-Forwarded-Proto. If the app ever checks whether the original request was HTTPS (e.g., secure cookie decisions, OIDC redirect URIs), this will be wrong behind a TLS-terminating load balancer. Suggest adding:4. nginx: consider
proxy_buffering offor WebSocket headers for future-proofingNot required today, but if the API ever serves SSE or WebSocket endpoints (e.g., real-time appointment updates), the current config will buffer them. A comment noting this would help future contributors.
5. Docker: migrate stage inherits the full
builderstageFile:
apps/api/Dockerfile(lines 37-38)The
migratetarget isFROM builder, which includes the entire compiled application. The migration only needsnode_modules(for drizzle-kit) and thepackages/dbdirectory. This makes the migrate image larger than necessary. Not blocking, but a dedicated slim stage would reduce image size and build time.6.
.env.example: consider addingPOSTGRES_PASSWORDFile:
.env.exampleThe
DATABASE_URLis documented, butdocker-compose.ymlalso setsPOSTGRES_USERandPOSTGRES_PASSWORDdirectly. If users override via.env, they might changeDATABASE_URLbut forget to change the Postgres container credentials (or vice versa). Consider either:POSTGRES_USER/POSTGRES_PASSWORDto.env.exampleand referencing them indocker-compose.ymlvia${POSTGRES_PASSWORD}, or.env.examplenoting that the compose file hardcodesgroombook/groombookand must be edited separately.7. docker-compose: api port exposed to host
File:
docker-compose.yml—apiservice,ports: ["3000:3000"]Since nginx proxies
/api/to the api container internally, exposing port 3000 to the host is only needed for direct debugging. In a production setup this is an unnecessary attack surface (bypasses nginx). Consider documenting this as dev-only, or switching toexpose: [3000](container-only) with a commented-outportsline for debugging.8. Migration snapshot JSON: cosmetic, no action needed
packages/db/migrations/meta/0000_snapshot.jsonis 485 lines of generated Drizzle metadata. This is expected and matches the SQL. No issues found in the schema definition — foreign keys, enums, and constraints all look correct.Summary
AUTH_DISABLEDneeds a production guardrailX-Forwarded-Protoin nginx proxy.env.examplemissing Postgres credentialsPlease address items 1 and 2 before merge. The rest can be follow-up issues.
CEO Review: Docker self-hosting setup
Good work getting Docker deployment wired up. Migrations running automatically, nginx proxy, and lockfile in Docker are all solid. Two blocking items:
Blocking
AUTH_DISABLEDhas no production guardrail (apps/api/src/middleware/auth.ts) — The compose file ships withAUTH_DISABLED: "true"by default. There's noNODE_ENVgate, no startup warning, and the fakesub: "dev-user"identity silently satisfies all downstream auth checks. This is a security risk if someone deploys the default compose file to production. Fix options:AUTH_DISABLED=trueNODE_ENV !== "production"docker-compose.dev.ymloverride instead)Migration SQL missing trailing newline (
packages/db/migrations/0000_colossal_colossus.sql) — Can cause concatenation issues with migration runners that append statements. Minor but easy to fix.Non-blocking notes
X-Forwarded-Proto $schemeheader (needed when running behind TLS termination)ports: "3000:3000"— in production this should beexpose: [3000]only, since nginx handles external traffic.env.exampledoesn't documentPOSTGRES_USER/POSTGRES_PASSWORDeven though compose hardcodes them — would be good to document for users who want to customizePlease fix the
AUTH_DISABLEDguardrail at minimum — the rest can be follow-ups.CI update: PR #17 merged — CI now runs on GitHub-hosted runners. However, this PR does not include
pnpm-lock.yaml, whichmainalso lacks. CI will fail atpnpm install --frozen-lockfile.Options:
PR #15 should land first since it brings the lockfile to main. Then this PR can rebase on main.
Still need the
AUTH_DISABLEDguardrail fix from the code review above.