Skip to content

Add Docker support: multi-stage build, provider-switchable compose, hardened env handling#1

Open
Rashik004 wants to merge 17 commits into
mainfrom
AddDockerSupport
Open

Add Docker support: multi-stage build, provider-switchable compose, hardened env handling#1
Rashik004 wants to merge 17 commits into
mainfrom
AddDockerSupport

Conversation

@Rashik004
Copy link
Copy Markdown
Owner

Summary

Adds first-class Docker support to the starter: a multi-stage Dockerfile, three provider-specific docker/compose.*.yaml stacks (Sqlite, Postgres, SqlServer), and matching init-project / select-db-provider script extensions to generate .env files and trim compose shards. Final commit (harden(docker): ...) tightens env validation, healthchecks, and secret generation based on a pre-landing review.

What changed

Image / runtime

  • Multi-stage Dockerfile — SDK image restores + publishes; aspnet runtime image hosts the published output as non-root user app.
  • Project-scoped restore (src/Host/Starter.WebApi/Starter.WebApi.csproj) — pulls only the host project + transitive module references. Skips three test projects (xunit, NetArchTest, Mvc.Testing, FluentAssertions) that never reach the runtime image.
  • Self-contained healthcheck — replaces the wget-based check (which required an apt-get install layer) with a bash /dev/tcp builtin probe of /health/live. Cuts one image layer and the apt-cache cleanup.
  • COPY --chown=app:app before USER app, with mkdir -p /data && chown -R app:app /app /data for the writable volume mount point.

Compose stacks

  • docker/compose.sqlite.yaml, compose.postgres.yaml, compose.sqlserver.yaml — one stack per supported provider, selectable at init time.
  • Required-var assertions — every secret/identity env uses ${VAR:?error message} form so missing values fail fast at docker compose up with an actionable hint (copy .env.example to .env).
  • SqlServer TCP probe — healthcheck switched from sqlcmd (whose binary path under /opt/mssql-tools*/bin/ has shifted twice across MSFT image revisions) to timeout 3 bash -c '</dev/tcp/127.0.0.1/1433'. Trade-off documented in 35d2ff8: TCP listener can come up before the engine accepts logins; EF connection retry on the api side absorbs the brief race.

Scripts

  • scripts/init-project.{ps1,sh} — generate .env with provider-appropriate keys (JWT, CORS_ORIGIN, plus POSTGRES_* or MSSQL_SA_PASSWORD).
    • Atomic create — bash uses set -C + umask 077; PowerShell uses FileMode.CreateNew + FileShare.None. Both fail loudly if a racing writer exists; neither overwrites an existing .env.
    • ACL hardening (Windows) — strips inherited rules and grants the current user FullControl only. On non-Windows PowerShell, prints a chmod 600 reminder.
    • SQL Server password — replaces the predictable Aa1!<entropy> prefix with random-position injection of one upper / lower / digit / symbol character into a base64 entropy string. Same complexity guarantee, no prefix signature.
  • scripts/select-db-provider.{ps1,sh} — when compose.yaml has already been trimmed to a single provider, detect a mismatch between the trimmed Database__Provider: and the requested provider and abort with recovery instructions instead of silently shipping the wrong DB.

Ignore lists

  • .gitignore — switch to .env.* allowlist with !.env.example exception so all real env files are blocked while the example template stays committed.
  • .dockerignore — also exclude src/tests/.

Pre-landing review notes

A /review pass produced 5 informational findings, 0 critical:

  1. Healthcheck assumes bash is present in the aspnet base image (true for default Debian variant; would break on -chiseled tags).
  2. grep -q \" 200 \" matches Kestrel's status line but is mildly brittle for non-default response shapes.
  3. SqlServer TCP probe is a weaker contract than sqlcmd (already documented in 35d2ff8).
  4. $RANDOM-modulo-position bias is negligible for the alphabets used; password entropy still comes from openssl/urandom.
  5. Provider-mismatch guard silently passes if a user manually deletes the Database__Provider: line from compose.yaml.

None block landing.

Test plan

  • docker build -t starter-webapi . succeeds and produces an image without wget/apt cache layers.
  • docker compose -f docker/compose.sqlite.yaml --env-file .env up starts; healthcheck transitions to healthy within 60 s; /health/live returns 200.
  • Same for compose.postgres.yaml and compose.sqlserver.yaml (with valid .env from init-project).
  • docker compose ... up with a missing JWT_SECRET_KEY fails immediately with the :? hint message.
  • init-project.sh -Provider SqlServer (and .ps1) emit a MSSQL_SA_PASSWORD that satisfies SQL Server complexity rules (upper, lower, digit, symbol, 16+ chars) and contains no Aa1! prefix.
  • Re-running init-project against an existing .env aborts without overwriting.
  • On Windows, the generated .env ACL grants only the current user; on POSIX, mode is 0600.
  • select-db-provider against an already-trimmed compose.yaml for a different provider exits non-zero with the recovery instruction.
  • Architecture tests still pass (dotnet test src/tests/Starter.WebApi.Tests.Architecture).

Rashik004 added 11 commits May 9, 2026 15:50
Captures three defects in the pre-bootstrap docker flow and the
implementation plan to fix them. DOCKER-INVESTIGATION.md will be
removed in the final task once the flow is verified working.
The mcr.microsoft.com/dotnet/aspnet:10.0 image ships a non-root
'app' user at UID 1654 and 'ubuntu' at UID 1000. The previous
groupadd -g 1000 collided with 'ubuntu' and broke every build.
Drop the manual user creation and adopt Microsoft's documented
pattern of reusing the pre-baked app user.
context: . resolves relative to the compose file location
(docker/), but Dockerfile lives at repo root, so every build
failed with 'failed to read dockerfile'. Use context: .. so
the README invocation 'docker compose -f docker/compose.X.yaml
up -d' works without extra flags.
Compose looks for .env next to the compose file by default. With
.env at repo root and compose files under docker/, --project-directory .
is required for ${JWT_SECRET_KEY} interpolation. Also correct the
'non-root UID 1000' claim — the runtime user is whatever UID the
base image's app user has (currently 1654).
The previous change of context: . → context: .. was incorrect once
the README documented --project-directory . for .env discovery.
With --project-directory ., compose resolves context relative to
the project directory (repo root), so context: . correctly points
at the repo-root Dockerfile. context: .. would resolve to the
parent of the repo root and break the build.
Task 4 verification surfaced a logic conflict between Task 2
(context: ..) and Task 3 (--project-directory .). The compose
context revert restores the original 'context: .' which works
correctly with the documented --project-directory . invocation.
Defects documented in DOCKER-INVESTIGATION.md are fixed in
the prior commits on this branch. The README now reflects the
working invocation.
Only the SQLite docker path was end-to-end verified on this
branch. Postgres and SqlServer compose files build but full
container startup with migrations was not exercised.
The pre-existing DOCKER-PLAN.md design doc still claimed the
container would run a manually-created appuser at UID 1000.
The implementation actually reuses the base image's pre-baked
'app' user at UID 1654 because UID 1000 collides with the
'ubuntu' user shipped in mcr.microsoft.com/dotnet/aspnet:10.0.
Update lines 73 and 83 so the design doc no longer contradicts
the Dockerfile and README.
…ration

- Dockerfile: project-scoped restore (skip test projects), drop wget+apt
  layer, switch healthcheck to bash /dev/tcp builtin
- compose: add :? required-var assertions for POSTGRES_*, MSSQL_SA_PASSWORD,
  JWT_SECRET_KEY with copy-from-.env.example hints
- compose.sqlserver: replace sqlcmd healthcheck with TCP probe (avoids
  brittle /opt/mssql-tools*/bin path that has changed twice)
- init-project: atomic .env create (set -C / FileMode.CreateNew) with
  umask 077 / restricted ACL; randomize SQL Server password category-char
  positions instead of predictable Aa1! prefix
- select-db-provider: detect provider mismatch when compose.yaml already
  trimmed to a different provider than requested
- .gitignore: switch to .env.* allowlist with !.env.example exception
- .dockerignore: also exclude src/tests/
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8ff6d49f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/init-project.sh Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class Docker support to the starter Web API, aiming to make the template runnable via Docker Compose (pre- and post-bootstrap) while keeping provider trimming and bootstrap scripts in sync.

Changes:

  • Introduces a multi-stage Dockerfile (non-root runtime, HTTP-only on 8080, healthcheck).
  • Adds provider-specific Compose stacks (docker/compose.{sqlite,postgres,sqlserver}.yaml) and extends select-db-provider to trim to a single docker/compose.yaml.
  • Extends init-project to optionally generate a hardened repo-root .env, and updates ignores / docs / architecture tests accordingly.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
Dockerfile Multi-stage build/publish and runtime hardening (non-root, healthcheck).
docker/compose.sqlite.yaml SQLite compose stack using repo-root .env vars and a named volume for /data.
docker/compose.postgres.yaml Postgres stack with healthcheck + required env var assertions.
docker/compose.sqlserver.yaml SQL Server stack with TCP health probe + required env var assertions.
scripts/select-db-provider.sh Adds compose-shard trimming/rename step (to docker/compose.yaml).
scripts/select-db-provider.ps1 Same compose trimming logic for PowerShell.
scripts/init-project.sh Adds --no-env-file and .env generation for Docker Compose.
scripts/init-project.ps1 Adds -NoEnvFile and .env generation + ACL hardening notes.
scripts/rename-project.sh Ensures Docker/YAML files participate in rename find/replace.
scripts/rename-project.ps1 Same: include YAML + Dockerfile in rename content pass.
src/tests/Starter.WebApi.Tests.Architecture/DockerArtifactsTests.cs Architecture-level checks that Docker artifacts exist.
README.md Documents pre-/post-bootstrap Docker flows and endpoints.
.gitignore Ignores .env* files while explicitly keeping .env.example.
.env.example Template for required Docker Compose environment variables.
.dockerignore Shrinks build context; excludes secrets/tests/etc.
DOCKER-PLAN.md Captures implementation plan/constraints for Docker support.
docs/superpowers/plans/2026-05-09-fix-docker-pre-bootstrap.md Detailed plan doc for pre-bootstrap Docker flow fixes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/init-project.sh Outdated
Comment thread scripts/init-project.ps1
Comment on lines +245 to +250
$extras = @(
(Pick-Char 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
(Pick-Char 'abcdefghijklmnopqrstuvwxyz')
(Pick-Char '0123456789')
(Pick-Char '!@#%^*-_+=')
)
Comment thread scripts/init-project.sh Outdated
Comment thread scripts/init-project.ps1 Outdated
Comment thread README.md Outdated
Comment thread scripts/select-db-provider.sh Outdated
Comment on lines +462 to +467
git_tracked() { git -C "$ROOT_DIR" ls-files --error-unmatch "$1" >/dev/null 2>&1; }

if [[ ! -d "$COMPOSE_DIR" ]]; then
tag SKIP "docker/ directory absent — skipping compose trim"
elif [[ ! -f "$KEEP_PATH" ]]; then
# Idempotency: if compose.yaml exists and no shards remain, already trimmed
Comment thread scripts/select-db-provider.ps1 Outdated
Rashik004 and others added 5 commits May 9, 2026 17:51
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@Rashik004
Copy link
Copy Markdown
Owner Author

@copilot apply changes based on the comments in this thread

…hs in select-db-provider

Agent-Logs-Url: https://github.com/Rashik004/starter-web-api/sessions/9a65981d-6f12-4696-939b-03ffc2a17a0c

Co-authored-by: Rashik004 <4425665+Rashik004@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI commented May 9, 2026

@copilot apply changes based on the comments in this thread

Applied all unresolved review feedback in commit 8591d7f:

  1. init-project.ps1 — removed # from the SQL Server password symbol set ('!@#%^*-_+=''!@%^*-_+='), matching the fix already in the .sh variant.

  2. select-db-provider.sh — updated git_tracked() to strip the $ROOT_DIR/ prefix before passing the path to git ("${1#$ROOT_DIR/}"), and updated the git rm/git mv calls to use the same relative-path form. Git pathspecs are now always repo-relative.

  3. select-db-provider.ps1 — extracted a Get-RepoRelativePath helper (absorbing the conversion logic that was inline in Test-GitTracked) and applied it at both the git rm and git mv call sites so all three git invocations use consistent repo-relative paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants