Post

Job Hunter Agent: Infrastructure and Environments

Two environments for Job Hunter Agent, a laptop and the homelab platform cluster, plus the configuration, secrets, DNS and capacity that connect them.

Job Hunter Agent: Infrastructure and Environments

Job Hunter Agent has two environments and no staging, and that’s a decision rather than an omission. The laptop runs everything with fakes or the real model on demand; production is one pod on the homelab platform cluster. The thing a staging environment would protect, the LinkedIn account, can’t be duplicated anyway.

Development environment

My laptop: make dev serves the API and UI on 127.0.0.1:8000 with in-memory storage and sign-in off; the LLM is the same LiteLLM gateway as production, with its own virtual key. Optional extras are a local Supabase in Docker (make supabase-start) and a local LinkedIn MCP server with its own browser session on 127.0.0.1:8765. Details in Local development setup.

Test or QA environment

CI is the test environment: GitHub-hosted runners run the unit tests, start a throwaway local Supabase with every migration, and build the image. It’s created and destroyed on every push, so it can’t drift.

Staging environment

None, deliberately. A staging copy would need a second LinkedIn session (a second account breaks LinkedIn’s terms; sharing one doubles the traffic) and a second Supabase project. The risk staging would catch (a bad deploy) is covered by fast rollback: revert the digest commit in homelab-gitops and Argo CD goes back. Schema changes are the exception, and they’re additive so the old image keeps working.

Production environment

PieceWhere
App + LinkedIn sidecarplatform k3d cluster, namespace job-hunter, one pod
URLhttps://jobs.platform.home.cloudutsuk.com (tailnet)
LLMLiteLLM on the ai cluster
Identity, secretsKeycloak and OpenBao on the core cluster
DatabaseSupabase cloud, free tier
RegistryZot on core, platform/job-hunter-agent

Infrastructure as code

  • homelab-gitops apps/job-hunter/: Deployment, PVC, Service, Ingress, NetworkPolicy, ExternalSecret, registry pull secret, Kustomization with the image digest; clusters/platform/job-hunter.yaml is the Argo CD Application.
  • infra-local: the Keycloak client (configure-realm.sh) and host/job-hunter/configure.sh, which fills OpenBao (Keycloak secret, session key, LiteLLM key, Supabase, Slack webhook) without printing anything.
  • job-hunter-agent: supabase/migrations/ for the schema.

The one manual step is pasting new migrations into the Supabase SQL editor.

Networking and DNS

Traefik ingress with TLS from Let’s Encrypt; ExternalDNS publishes the name to the homelab’s internal DNS. cert-manager uses Cloudflare DNS-01, and getting the first certificate needed one fix: with split DNS, cert-manager’s own lookups saw the internal zone and failed, so it now checks DNS-01 records against public resolvers only (--dns01-recursive-nameservers-only). Inside the pod, the app reaches the sidecar on 127.0.0.1:8080; nothing else can.

Compute, storage, and databases

ContainerRequestsLimitNotes
app50m CPU, 192 MiB768 MiBRead-only root filesystem, non-root
linkedin100m CPU, 1 GiB3 GiBChromium; /dev/shm is a 1 GiB memory volume, counted in the limit
init home-dir10m, 16 MiB64 MiBCreates the browser’s home folder on the volume

The LinkedIn container was OOMKilled at 2 GiB during its first real search; 3 GiB has held since. Storage is a 2 GiB local-path PVC for the browser profile. The database is Supabase Postgres, outside the cluster.

Configuration and secret management

Non-secret settings are plain env vars in the Deployment (model, URLs, schedule default, timezone, LinkedIn limits). Secrets come from OpenBao kv/platform/job-hunter through one ExternalSecret, refreshed hourly. One gotcha: env vars from a Secret are read at container start, so a new or rotated secret needs a pod restart after the ExternalSecret syncs.

Environment parity

AspectLaptopProduction
Code and imageSourceSame code, signed image
LLMSame gateway and modelSame
StorageMemory (or local Supabase)Supabase cloud
Sign-inOffKeycloak
LinkedInOff by defaultSidecar
ScheduleOffPer profile

The biggest gap is storage; the CI Supabase test closes most of it.

Capacity and quota planning

The limits that matter aren’t CPU: LinkedIn’s tolerance (about 6 searches and up to 30 job pages a day across three profiles), the local model’s throughput (one request at a time is fine), and Supabase’s free tier (database size and inactivity rules).

The Supabase free plan, as of October 2026:

LimitFree planWhat it means here
Database size500 MBThe real ceiling: postings and drafts are text, a few MB per hundred jobs; retention keeps it flat
Egress5 GB a month (plus 5 GB cached)The UI and the nightly export; far below
Active projects2This is one
PausingAfter a week without activityThe daily upkeep job queries it every day
Backups, point-in-time recoveryNoneHence the app’s own nightly export

I check the database size on the Supabase dashboard once a month. Once it passes 400 MB, retention gets shorter, or postings that were never drafted get a retention period of their own.

Checklist

  • No-staging decision recorded, with rollback as the safety net
  • Every manifest change in homelab-gitops through a PR
  • Secrets only in OpenBao; pod restarted after a rotation
  • Memory limits reviewed after any MCP server upgrade
  • Supabase usage checked monthly
  • Migrations applied in Supabase before the code that needs them merges

This post is licensed under CC BY 4.0 by the author.