Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ dotnet test tests/Informedica.GenUNITS.Tests/

### Docker

The proprietary `GENPRES_URL_ID` is **not** baked into the image any more. Inject it (and `GENPRES_PASSWORD` for admin operations) at container runtime, ideally via a Docker / Kubernetes secret.
`GENPRES_URL_ID` is required at server startup, in both demo and production mode — not just for admin operations. The image also sets `GENPRES_PROD=1` by default, so the server refuses to start unless `GENPRES_PASSWORD` is also set to at least 16 characters — it's not purely an admin-operations toggle either. Neither the proprietary URL ID nor the password is baked into the image; inject both at container runtime, ideally via a Docker / Kubernetes secret. For local testing without production credentials, use the public demo sheet ID documented in `.env.example`.

- `docker build -t halcwb/genpres .`
- `docker run -it -p 8080:8085 -e GENPRES_URL_ID="your_url_id" -e GENPRES_PASSWORD="your_admin_password" halcwb/genpres`
- `dotnet run DockerRun` - Run pre-built Docker image
- `dotnet run DockerBuild` - Build the image, labelled with the version from the root `Directory.Build.props`. Override the image name with `DOCKER_IMAGE` (default `halcwb/genpres`), cross-build a different platform with `DOCKER_PLATFORM`.
- `dotnet run DockerRun` - Run the built image, reading `GENPRES_URL_ID`/`GENPRES_PASSWORD` from the current environment (source `.env` first) and failing fast if either is unset.
- Equivalent manual commands: `docker build -t halcwb/genpres .` / `docker run -it -p 8080:8085 -e GENPRES_URL_ID="your_url_id" -e GENPRES_PASSWORD="your_admin_password" halcwb/genpres`

## Key Code Locations

Expand Down
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ dotnet test tests/Informedica.GenUNITS.Tests/

### Docker

The proprietary `GENPRES_URL_ID` is **not** baked into the image any more. Inject it (and `GENPRES_PASSWORD` for admin operations) at container runtime, ideally via a Docker / Kubernetes secret.
`GENPRES_URL_ID` is required at server startup, in both demo and production mode — not just for admin operations. The image also sets `GENPRES_PROD=1` by default, so the server refuses to start unless `GENPRES_PASSWORD` is also set to at least 16 characters — it's not purely an admin-operations toggle either. Neither the proprietary URL ID nor the password is baked into the image; inject both at container runtime, ideally via a Docker / Kubernetes secret. For local testing without production credentials, use the public demo sheet ID documented in `.env.example`.

- `docker build -t halcwb/genpres .`
- `docker run -it -p 8080:8085 -e GENPRES_URL_ID="your_url_id" -e GENPRES_PASSWORD="your_admin_password" halcwb/genpres`
- `dotnet run DockerRun` - Run pre-built Docker image
- `dotnet run DockerBuild` - Build the image, labelled with the version from the root `Directory.Build.props`. Override the image name with `DOCKER_IMAGE` (default `halcwb/genpres`), cross-build a different platform with `DOCKER_PLATFORM`.
- `dotnet run DockerRun` - Run the built image, reading `GENPRES_URL_ID`/`GENPRES_PASSWORD` from the current environment (source `.env` first) and failing fast if either is unset.
- Equivalent manual commands: `docker build -t halcwb/genpres .` / `docker run -it -p 8080:8085 -e GENPRES_URL_ID="your_url_id" -e GENPRES_PASSWORD="your_admin_password" halcwb/genpres`

## Key Code Locations

Expand Down
75 changes: 74 additions & 1 deletion Build.fs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,80 @@ Target.create
)


Target.create "DockerRun" (fun _ -> run docker [ "run"; "-it"; "p"; "8080:8085"; "halcwb/genpres" ] ".")
let requireEnvVar name =
match System.Environment.GetEnvironmentVariable name with
| v when System.String.IsNullOrWhiteSpace v ->
failwithf "%s is not set. Load it from .env first (see DEVELOPMENT.md)." name
| v -> v


// Override via DOCKER_IMAGE if you're pushing to your own registry/namespace
// rather than the project's `halcwb/genpres`.
let dockerImage =
match System.Environment.GetEnvironmentVariable "DOCKER_IMAGE" with
| null
| "" -> "halcwb/genpres"
| image -> image


Target.create
"DockerBuild"
(fun _ ->
let version =
System.Xml.Linq.XDocument.Load("Directory.Build.props").Descendants(System.Xml.Linq.XName.Get "Version")
|> Seq.map (fun e -> e.Value)
|> Seq.tryHead
|> Option.defaultWith (fun () -> failwith "Directory.Build.props: <Version> element not found")

// Cross-build for a different target platform, e.g. amd64 from Apple
// Silicon, via: DOCKER_PLATFORM=linux/amd64 dotnet run DockerBuild
let platformArgs =
match System.Environment.GetEnvironmentVariable "DOCKER_PLATFORM" with
| null
| "" -> []
| platform -> [ "--platform"; platform ]

run
docker
([ "build" ]
@ platformArgs
@ [
"--build-arg"
$"APP_VERSION={version}"
"-t"
dockerImage
"."
])
"."
)


Target.create
"DockerRun"
(fun _ ->
// Fail fast with a clear message, but don't pass the values as `-e NAME=value` args: `createProcess`
// (Helpers.fs) renders the full argument list into its failure message on any non-zero docker exit,
// which would leak GENPRES_URL_ID/GENPRES_PASSWORD as plain text. `-e NAME` (no `=value`) makes docker
// forward the variable from its own environment instead, so the secrets never appear in the args.
requireEnvVar "GENPRES_URL_ID" |> ignore
requireEnvVar "GENPRES_PASSWORD" |> ignore

run
docker
[
"run"
"-it"
"--rm"
"-p"
"8080:8085"
"-e"
"GENPRES_URL_ID"
"-e"
"GENPRES_PASSWORD"
dockerImage
]
"."
)


open Fake.Core.TargetOperators
Expand Down
45 changes: 23 additions & 22 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ packages for the Fable/Vite dev server).
| `dotnet run TestHeadless` | `TestHeadless` | Build and run tests without launching a browser |
| `dotnet run WatchTests` | `WatchTests` | Run tests in watch mode (re-runs on file changes) |
| `dotnet run Format` | `Format` | Format all F# source files using Fantomas |
| `dotnet run DockerRun` | `DockerRun` | Run the pre-built Docker image locally |
| `dotnet run DockerBuild` | `DockerBuild` | Build the production image (`halcwb/genpres` by default, override with `DOCKER_IMAGE`), labelling it with the version from the root `Directory.Build.props` |
| `dotnet run DockerRun` | `DockerRun` | Run the built image locally, using `GENPRES_URL_ID`/`GENPRES_PASSWORD` from the current environment (source `.env` first) |

#### Target Dependency Chains

Expand Down Expand Up @@ -237,38 +238,38 @@ dotnet run

##### Docker wrappers

None of these wrappers bake `GENPRES_URL_ID` into the image — that constraint is enforced by the `Dockerfile` itself and described in [Environment Configuration](#environment-configuration).
Building and running the image no longer needs a hand-copied shell script: the `DockerBuild` and `DockerRun` FAKE targets (see [FAKE Build Targets Reference](#fake-build-targets-reference)) cover both, work identically from PowerShell, Git Bash, or any POSIX shell, and are tracked in `Build.fs` rather than living only as documentation. Neither target bakes `GENPRES_URL_ID` into the image — that constraint is enforced by the `Dockerfile` itself and described in [Environment Configuration](#environment-configuration).

**`docker-local.sh`** — build for the local processor architecture (Apple Silicon → arm64; Intel/Linux → amd64). Save at the repo root:
**Build** — `dotnet run DockerBuild` reads the app's single curated version number from the root `Directory.Build.props` and passes it to `docker build --build-arg APP_VERSION=...`, so the image's `org.opencontainers.image.version` label always matches what was built. To cross-build for a different platform set `DOCKER_PLATFORM`; to tag/push under your own name instead of the project's `halcwb/genpres` default, set `DOCKER_IMAGE` (both `DockerBuild` and `DockerRun` read it).

```bash
#!/usr/bin/env bash
docker build -t halcwb/genpres .
```
# local architecture
dotnet run DockerBuild

**`docker-amd64.sh`** — cross-build an amd64 image on Apple Silicon for deployment to a typical Linux host. Save at the repo root:
# cross-build amd64
DOCKER_PLATFORM=linux/amd64 dotnet run DockerBuild
```

```bash
#!/usr/bin/env bash
docker build --platform linux/amd64 -t halcwb/genpres .
```powershell
# cross-build amd64 (PowerShell)
$env:DOCKER_PLATFORM = "linux/amd64"
dotnet run DockerBuild
```

**`docker-run.sh`** — source `.env`, validate that both `GENPRES_URL_ID` and `GENPRES_PASSWORD` are set (the `:` parameter expansion fails fast if either is missing), then run the container with the right `-e` flags. Save at the repo root:
**Run** — `dotnet run DockerRun` reads `GENPRES_URL_ID` and `GENPRES_PASSWORD` from the current environment and fails fast with an error if either is missing, rather than starting an unauthenticated container that the in-server `validateProductionPassword` would refuse later. Source `.env` first (single source of truth — same as `prod.sh` / `debug.sh`):

```bash
#!/usr/bin/env bash
# Load env vars from .env (single source of truth — same as prod.sh / debug.sh).
set -a; source .env; set +a
dotnet run DockerRun
```

# Fail fast if .env did not provide the secrets, so we don't start an
# unauthenticated container that the in-server validateProductionPassword
# would refuse later anyway.
: "${GENPRES_URL_ID:?GENPRES_URL_ID is not set in .env}"
: "${GENPRES_PASSWORD:?GENPRES_PASSWORD is not set in .env}"

docker run -e GENPRES_URL_ID="${GENPRES_URL_ID}" \
-e GENPRES_PASSWORD="${GENPRES_PASSWORD}" \
-p 8080:8085 halcwb/genpres
```powershell
Get-Content .env | ForEach-Object {
if ($_ -match '^\s*([^#=]+)=(.*)$') {
[Environment]::SetEnvironmentVariable($Matches[1].Trim(), $Matches[2].Trim())
}
}
dotnet run DockerRun
```

If you find yourself wanting to commit one of these local scripts (e.g. because the team agrees it should be standardized), add a `!`-prefixed allow-line for the file to `.gitignore` in the same PR — otherwise the opt-in strategy will silently keep it untracked.
Expand Down
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ RUN dotnet tool restore
COPY .paket .paket
COPY paket.references paket.references
COPY paket.dependencies paket.lock ./
# Each library's own Directory.Build.props imports this root file (via
# GetPathOfFileAbove) to share the single curated <Version>. Without it
# present at /workspace, that Import resolves to an empty path and MSBuild
# fails with MSB4020.
COPY Directory.Build.props .

FROM build AS app-build

Expand Down Expand Up @@ -34,6 +39,14 @@ RUN dotnet run bundle


FROM mcr.microsoft.com/dotnet/aspnet:10.0

# Curated single version number for the whole app (server, client, libraries).
# Sourced from the root Directory.Build.props by the caller (see the
# `DockerBuild` FAKE target in Build.fs / DEVELOPMENT.md) so the image label
# always matches what was actually built, without duplicating the version here.
ARG APP_VERSION=0.0.0
LABEL org.opencontainers.image.version="${APP_VERSION}"

COPY --from=app-build /workspace/deploy /app

ENV GENPRES_LOG=0
Expand Down