Sign inSign up

20dumpling/ea-qms-backend

By 20dumpling

•Updated 11 days ago

Go backend for the EA QMS Change Control module — 21 CFR Part 11 compliant workflow API.

Image
API management
Developer tools
Web servers
0

325

20dumpling/ea-qms-backend repository overview

⁠EA QMS — Change Control API

Production Docker image for the backend API of the EA QMS Change Control module.


⁠Quick Pull

docker pull 20dumpling/ea-qms-backend:1.2.2
docker pull 20dumpling/ea-qms-backend:latest

⁠Running the Container

docker run -d --name ea-qms-api -p 1304:1304 \
  -e DB_URL="postgres://postgres:[email protected]:5432/ea_qms?sslmode=disable" \
  -e JWT_SECRET="your_secure_random_signing_secret" \
  -e PLATFORM="dev" \
  -e ALLOWED_ORIGINS="http://localhost:5173" \
  -e DB_MAX_OPEN_CONNS="25" \
  20dumpling/ea-qms-backend:latest

⚠ The database must already have the schema. This image is the API alone and runs no migrations, so against an empty database it starts and then fails on the first query. Either point it at a database that already has the schema, or use the Compose stack in deploy/docker/⁠, which brings up PostgreSQL, applies the Goose migrations and seeds four dev accounts before the API starts. That is the easiest way to run the whole thing.

Note the database host. Inside a container, localhost is the container itself — not your machine. Use host.docker.internal to reach a PostgreSQL server running on the host (add --add-host=host.docker.internal:host-gateway if you are on Docker Engine rather than Docker Desktop). If PostgreSQL runs in another container, use its service or container name instead.

If you pass configuration with --env-file rather than -e, do not quote the values. Docker does no quote processing on that file, so JWT_SECRET="abc" becomes a secret with literal quote characters. The -e form above is fine because the shell strips them.

⁠Health Check Endpoint

A dedicated, unauthenticated health probe is available at GET /api/healthz for Docker and orchestrator readiness/liveness checks. It actively verifies the live database connection pool (rawDB.PingContext).

curl http://localhost:1304/api/healthz
# Returns: {"status":"ok","database":"connected"}

⁠Environment Variables

VariableRequiredDescription
DB_URLYesPostgreSQL connection string
JWT_SECRETYesSecret key used to sign and verify JWT tokens. Every replica must share the same value, or a token issued by one will be rejected by another
PLATFORMNoEnvironment mode (dev, prod). Defaults to dev.
ALLOWED_ORIGINSNoComma-separated list of allowed CORS origins. See below
DB_MAX_OPEN_CONNSNoConnection-pool ceiling per process. Defaults to 10. See below
INSTANCE_IDNoIdentifies which instance served a request. Defaults to the hostname, which under Docker is the container ID
ARGON2ID_*NoTunable password hashing parameters (Memory, Iterations, etc.)
⁠Generating JWT_SECRET

It signs every access token, so it must be long and random. Anyone who knows it can mint a valid token for any user.

openssl rand -base64 64

Or, without OpenSSL:

head -c 64 /dev/urandom | base64 -w 0

Every instance must share the same value — a token signed by one is verified by another. And changing it invalidates every token in circulation, which signs everyone out.

⁠ALLOWED_ORIGINS and the silent failure

It must match the browser's origin exactly — scheme, host and port. http://localhost:5173 does not cover http://127.0.0.1:5173, and a trailing slash will not match either.

⚠ Getting it wrong looks like the API being down. Postman and curl keep working, because they are not browsers and do not enforce CORS; the browser silently blocks every call. If it is empty, the API logs a warning at startup.

A browser client served from the same origin as the API — behind a reverse proxy, for instance — needs no value at all.

⁠Running More Than One Instance

The API is stateless — configuration comes from the environment, sessions are JWTs, and nothing is held in memory between requests — so it scales horizontally without changes. Two variables matter when it does.

DB_MAX_OPEN_CONNS is per process, not per deployment. Each instance holds its own connection pool and cannot see its peers, while PostgreSQL enforces one global max_connections (default 100). Three instances at 25 is 75 connections; a fourth exceeds the budget, and the failure appears at the database rather than at the service that caused it. Size it as max_connections ÷ instances, with headroom.

INSTANCE_ID is how you tell instances apart. It is attached to every log line and returned as an X-Instance-ID response header, so a request can be traced to the process that served it and an aggregated log stream can be filtered per instance. Set it explicitly — docker run --name does not set a container's hostname, so the fallback yields a container ID rather than a readable name.

docker run -d --name ea-qms-api-1 --network qms-net \
  --env-file .env -e INSTANCE_ID=api-1 \
  20dumpling/ea-qms-backend:1.2.2

⁠Image

Multi-stage build: a static Linux binary (CGO_ENABLED=0) stripped of DWARF symbols, packed onto alpine:latest — about 7 MB. The API documentation is compiled in with go:embed, so it ships with the code it describes. Only the compiled binary crosses from the build stage; no source and no configuration reach the published image.

Published for linux/amd64 and linux/arm64 under one tag. docker pull resolves to the right variant automatically, so Apple Silicon and ARM servers run natively rather than under QEMU emulation. The build cross-compiles with GOARCH=$TARGETARCH, which costs nothing in Go — there is no emulated build step.

Tag summary

Content type

Image

Digest

sha256:d83539cba…

Size

6.8 MB

Last updated

11 days ago

docker pull 20dumpling/ea-qms-backend