Home

/

Build It in a Weekend. Run It for Years.

/

.env & Deployment Reference

.env & Deployment Reference

Appendix B
Appendix
3
min read

A working reference for one agency's single deployment: the full configuration surface, the container, the deploy command, and the permissions that keep it locked down. Everything here is illustrative — the shape of a real setup, not a copy-paste product. Placeholder values only; no real secrets appear, and none should ever appear in a file you commit.

The framing throughout is single-tenant: one container, one set of secrets, one agency's ATS credentials, one audit store. There is no "across clients" here. There is your deployment, and that's the whole picture.

The full .env.example

This is the complete configuration surface for the three agents. Every key is documented. On your laptop these load through DotNetEnv; in production not one of them lives in a file — they arrive from a managed secret store through the same IConfiguration pipeline, so the application code barely notices the difference.

.env.example is committed. .env is not. Keep .gitignore honest, and run a pre-commit secret scanner so a real key can't reach the repo in the first place.

# =============================================================================
# .env.example — copy to .env for local dev only. NEVER commit a real .env.
# In production these are mounted from GCP Secret Manager (AWS Secrets Manager
# / Azure Key Vault), not read from disk. See B.4.
# =============================================================================

# --- Runtime ----------------------------------------------------------------
ASPNETCORE_ENVIRONMENT=Development        # Development | Production
PORT=8080                                 # Cloud Run injects this; honour it
LOG_LEVEL=Information                      # Trace|Debug|Information|Warning|Error

# --- LLM provider -----------------------------------------------------------
OPENAI_API_KEY=sk-REPLACE_ME              # secret. rotate on a schedule
OPENAI_MODEL=gpt-4o                        # the screening/shortlisting model
OPENAI_MODEL_FALLBACK=gpt-4o-mini          # cheaper model for cheap steps
OPENAI_REQUEST_TIMEOUT_SECONDS=60          # fail fast; Polly handles retry
LLM_MAX_TOKENS_PER_CALL=4000               # hard ceiling per call (cost guard)

# --- Bullhorn (flagship ATS) ------------------------------------------------
# OAuth2; see Appendix C for the auth flow and endpoints.
BULLHORN_CLIENT_ID=REPLACE_ME              # secret
BULLHORN_CLIENT_SECRET=REPLACE_ME          # secret. never log this
BULLHORN_USERNAME=REPLACE_ME               # API user, least-privilege role
BULLHORN_PASSWORD=REPLACE_ME               # secret
BULLHORN_DATACENTER_REST_URL=https://rest.bullhornstaffing.com  # region-specific

# --- JobAdder (second ATS) --------------------------------------------------
# OAuth2 auth-code; see Appendix C for the auth flow and endpoints.
JOBADDER_CLIENT_ID=REPLACE_ME              # secret
JOBADDER_CLIENT_SECRET=REPLACE_ME          # secret. never log this
JOBADDER_REDIRECT_URI=https://your-host/oauth/jobadder/callback
JOBADDER_ACCESS_TOKEN=REPLACE_ME           # secret. ~60-min lifetime
JOBADDER_REFRESH_TOKEN=REPLACE_ME          # secret. ROTATES — persist the new
                                           #   one on every refresh
JOBADDER_API_BASE_URL=https://api.jobadder.com/v2  # the token response returns
                                           #   the per-account 'api' URL; store
                                           #   THAT and prefix calls with it

# --- Secrets & cloud (production) -------------------------------------------
GCP_PROJECT_ID=your-gcp-project            # AWS: account/region; Azure: vault
SECRET_STORE=gcp                           # gcp | aws | azure | env(dev only)

# --- Guardrail gateway (the enforcement layer — see Ch 9) -------------------
DLP_INSPECT_ENABLED=true                   # fail-closed if the inspector is down
DLP_FAIL_MODE=closed                       # closed = block on doubt. never 'open'
ALLOWLIST_PROFILE=screening                # which structured-field allowlist
PII_REDACTION_REQUIRED=true                # formatting agent must strip PII

# --- Observability (cloud-neutral; see Ch 11) -------------------------------
OTEL_EXPORTER_OTLP_ENDPOINT=               # blank = use cloud default exporter
OTEL_SERVICE_NAME=recruitment-agents
SERILOG_MINIMUM_LEVEL=Information
SAFE_SINK_REFUSE_RAW_PAYLOADS=true         # logger refuses raw CV/PII payloads

# --- Queue / DLQ (durable work; see Ch 10) ----------------------------------
PUBSUB_TOPIC=cv-intake                      # AWS: SQS+SNS; Azure: Service Bus
PUBSUB_DLQ_TOPIC=cv-intake-dead             # poison messages land here
PUBSUB_MAX_DELIVERY_ATTEMPTS=5             # then route to DLQ

# --- Audit store ------------------------------------------------------------
AUDIT_STORE_CONNECTION=REPLACE_ME          # secret. append-only decision log
AUDIT_RETENTION_DAYS=365                    # align to your GDPR retention policy

A note on what is not here: there is no MULTI_TENANT, no per-client key, no client routing. One deployment, one tenant. If you ever feel the urge to add a "client ID" column, that's a second product — and a different book.

The Dockerfile

Container-first and cloud-agnostic by design: the same image runs on Cloud Run, App Runner / ECS Fargate, or Azure Container Apps. Multi-stage build keeps the final image small and free of the SDK and source.

# ---- build stage -----------------------------------------------------------
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.sln .
COPY src/ ./src/
RUN dotnet restore
RUN dotnet publish src/RecruitmentAgents.Api -c Release -o /app \
    --no-restore /p:PublishTrimmed=false

# ---- runtime stage ---------------------------------------------------------
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app .

# Run as a non-root user — least privilege at the process level too.
RUN adduser --disabled-password --gecos "" appuser
USER appuser

# Cloud Run sets PORT; bind to it. Stateless: durable work lives in the queue.
ENV ASPNETCORE_URLS=http://0.0.0.0:8080
EXPOSE 8080

ENTRYPOINT ["dotnet", "RecruitmentAgents.Api.dll"]

No secrets in the image. No .env copied in. A secret baked into a layer is a secret leaked forever — layers are cached, pushed, and pulled.

The service is stateless on purpose. Scale-to-zero means an idle instance gets reclaimed; if in-flight work lived in memory, it would vanish with the instance. It doesn't — durable work lives in the queue and the ATS, so a reclaimed instance loses nothing. (the reliability chapter covers the cold-start and warming trade-offs that come with this.)

Deploying to Cloud Run (GCP first)

The canonical runtime is Cloud Run (AWS App Runner / ECS Fargate / Azure Container Apps), chosen because it scales to zero — when no CVs are arriving, you're running nothing and paying for nothing.

# 1. Build and push the image to Artifact Registry
gcloud builds submit \
  --tag europe-west2-docker.pkg.dev/$GCP_PROJECT_ID/agents/recruitment:latest

# 2. Store each secret once (do this from a trusted machine, not CI logs)
printf '%s' "$BULLHORN_CLIENT_SECRET" | \
  gcloud secrets create bullhorn-client-secret --data-file=-
# ...repeat for openai-api-key, jobadder-client-secret, audit-store-connection, etc.

# 3. Deploy, mounting secrets at runtime (NOT baked into the image)
gcloud run deploy recruitment-agents \
  --image europe-west2-docker.pkg.dev/$GCP_PROJECT_ID/agents/recruitment:latest \
  --region europe-west2 \
  --no-allow-unauthenticated \
  --service-account agents-runtime@$GCP_PROJECT_ID.iam.gserviceaccount.com \
  --min-instances 0 --max-instances 5 \
  --cpu 1 --memory 1Gi --timeout 300 \
  --set-env-vars ASPNETCORE_ENVIRONMENT=Production,SECRET_STORE=gcp \
  --set-secrets \
OPENAI_API_KEY=openai-api-key:latest,\
BULLHORN_CLIENT_SECRET=bullhorn-client-secret:latest,\
JOBADDER_CLIENT_SECRET=jobadder-client-secret:latest,\
AUDIT_STORE_CONNECTION=audit-store-connection:latest

The load-bearing flags:

  • --no-allow-unauthenticated — the service is private. It is invoked by Pub/Sub or Cloud Scheduler with an authenticated identity, never exposed to the open internet.
  • --service-account — the service runs as its own identity, not the project default. This is what makes least-privilege IAM possible.
  • --set-secrets — secrets are mounted at runtime from Secret Manager. They are never passed as plaintext env vars and never appear in a build layer.
  • --min-instances 0 — scale to zero. Cold starts are the cost of this.
Deploy flow: an Artifact Registry image plus Secret Manager secrets feed a private Cloud Run service with its own service account, invoked by Pub/Sub and Cloud Scheduler.

AWS / Azure equivalents

The shape is identical; only the nouns change.

StepGCPAWSAzure
Image registryArtifact RegistryECRAzure Container Registry
RuntimeCloud RunApp Runner / ECS FargateContainer Apps
Secret storeSecret ManagerSecrets ManagerKey Vault
Secret mount--set-secretstask-def secrets: from Secrets ManagerKey Vault reference / CSI driver
Runtime identityservice accountIAM task rolemanaged identity
Private invoke--no-allow-unauthenticated + IAMprivate ALB / IAM authinternal ingress + RBAC
the-math-no-recruiter-can-win-by-hand
what-an-ai-agent-actually-is
the-leash
the-toolkit
the-model-small-capable-swappable
talking-to-your-ats
use-case-1-resume-screening-against-a-job
the-shape-of-the-loop
running-it-thought-action-observation
use-case-2-cv-formatting-redacting-for-clients
reformatting-into-your-branded-template
resume-shortlisting
that-was-easy
security-compliance
keeping-pii-out-of-the-llm
exceptions-reliability
silent-api-drift-the-ats-changes-under-you
when-it-fails-anyway-dead-letter-and-the-leash
monitoring-observability
maintenance-the-lifecycle
the-scorecard-success-metrics-kpis
build-vs-buy-vs-managed
what-an-engineer-actually-costs
what-the-wider-data-says-happens-next
conclusion-how-this-gets-run-for-you
the-promises-behind-the-service
fuller-code-listings
one-full-screening-react-loop-semantic-kernel
env-deployment-reference
secrets-in-dev-vs-production
bullhorn-jobadder-endpoint-cheat-sheets
sources-further-reading
compliance-primary-law-sources

Download the full PDF for free?

Download full PDF
build-it-in-a-weekend.pdf
Oops! Something went wrong while submitting the form.
Related Chapters