Skip to content

Conversation

@shawnfultz
Copy link

Summary

This PR fixes the issue where environment variables configured in the Trigger.dev portal were not being injected into runner pods/containers in self-hosted deployments.

Fixes #2792

Root Cause

Environment variables were only being fetched during the startRunAttempt phase, which occurs AFTER the runner pod/container has already been created. This meant that pods were created without access to portal-configured environment variables, causing tasks to fail immediately with "Missing required environment variables" errors.

Solution

Move environment variable fetching from the startRunAttempt phase to the dequeue phase, ensuring env vars are available when the supervisor creates the runner workload.

Changes

  1. Schema (packages/core/src/v3/schemas/runEngine.ts)

    • Added optional envVars field to DequeuedMessage schema
  2. Webapp Service (apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts)

    • Modified AuthenticatedWorkerInstance.dequeue() to fetch and include environment variables for each dequeued message
    • Reuses existing getEnvVars() helper method
  3. Supervisor Types (apps/supervisor/src/workloadManager/types.ts)

    • Added envVars field to WorkloadManagerCreateOptions interface
  4. Supervisor (apps/supervisor/src/index.ts)

    • Updated workload creation to pass envVars from dequeued message
  5. Workload Managers

    • Kubernetes (apps/supervisor/src/workloadManager/kubernetes.ts): Inject envVars into pod environment
    • Docker (apps/supervisor/src/workloadManager/docker.ts): Inject envVars into container environment

Testing

  • ✅ Tested in self-hosted Kubernetes deployment on AWS EKS
  • ✅ Verified environment variables configured in portal appear in runner pods
  • ✅ Confirmed tasks execute successfully with portal-configured env vars
  • ✅ TypeScript compilation validated

Backward Compatibility

This change is backward compatible:

  • The envVars field is optional in the schema
  • If envVars is undefined/empty, workload creation proceeds as before
  • No database migrations required
  • No breaking changes to existing APIs

Related Documentation

…loyments

This commit fixes the issue where environment variables configured in the
Trigger.dev portal were not being injected into runner pods/containers in
self-hosted deployments (GitHub issue triggerdotdev#2792).

Root Cause:
Environment variables were only being fetched during the 'startRunAttempt'
phase, which occurs AFTER the runner pod/container has already been created.
This meant that pods were created without access to portal-configured
environment variables, causing tasks to fail immediately.

Solution:
Move environment variable fetching from the 'startRunAttempt' phase to the
'dequeue' phase, so that env vars are available when the supervisor creates
the runner workload.

Changes:
1. Added optional 'envVars' field to DequeuedMessage schema
2. Modified AuthenticatedWorkerInstance.dequeue() to fetch and include
   environment variables for each dequeued message
3. Added 'envVars' field to WorkloadManagerCreateOptions interface
4. Updated supervisor to pass envVars when creating workloads
5. Modified KubernetesWorkloadManager to inject envVars into pod spec
6. Modified DockerWorkloadManager to inject envVars into container

Testing:
- Tested in self-hosted Kubernetes deployment (EKS)
- Verified environment variables appear in runner pods
- Confirmed tasks execute successfully with portal-configured env vars

Fixes triggerdotdev#2792
@changeset-bot
Copy link

changeset-bot bot commented Dec 17, 2025

⚠️ No Changeset found

Latest commit: b2cdc64

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 17, 2025

Walkthrough

This change implements environment variable injection throughout the workload creation pipeline. A new optional envVars field is added to the DequeuedMessage schema and WorkloadManagerCreateOptions type. The dequeue service enriches each message with computed environment variables resolved from the runtime environment context. The supervisor passes these variables through to the workload manager, which injects them into both Docker containers and Kubernetes pods during workload creation.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

  • apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts — The dequeue enrichment logic involves RuntimeEnvironment resolution and getEnvVars computation; verify correctness of context parameters (run/task/machine) and fallback behavior.
  • apps/supervisor/src/workloadManager/docker.ts and apps/supervisor/src/workloadManager/kubernetes.ts — Confirm that environment variable iteration and concatenation handle empty/undefined opts.envVars correctly and maintain proper ordering with existing variables.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the primary change: injecting environment variables at dequeue time for self-hosted deployments.
Description check ✅ Passed The PR description is comprehensive, includes the linked issue reference, testing details, backward compatibility notes, and follows the template structure with a detailed changelog section.
Linked Issues check ✅ Passed The PR fully addresses the requirements from issue #2792 by fetching environment variables at dequeue time and injecting them into runner pods across all workload managers.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to the objective of moving environment variable fetching from startRunAttempt to dequeue phase; no extraneous modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts (1)

381-408: Consider performance implications of N+1 database queries.

For each dequeued message, the code fetches the runtime environment from the database (lines 385-392). When multiple messages are dequeued in a batch, this results in N separate database queries. While this may be acceptable for self-hosted deployments with lower dequeue rates, consider batching environment fetches if performance becomes a concern.

If needed, environments could be fetched in a single query:

// Fetch all unique environments in one query
const envIds = [...new Set(messages.map(m => m.environment.id))];
const environments = await this._prisma.runtimeEnvironment.findMany({
  where: { id: { in: envIds } },
  include: { parentEnvironment: true },
});
const envMap = new Map(environments.map(e => [e.id, e]));

// Then look up from map instead of querying per message
const environment = envMap.get(message.environment.id);
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff80742 and b2cdc64.

📒 Files selected for processing (6)
  • apps/supervisor/src/index.ts (1 hunks)
  • apps/supervisor/src/workloadManager/docker.ts (1 hunks)
  • apps/supervisor/src/workloadManager/kubernetes.ts (1 hunks)
  • apps/supervisor/src/workloadManager/types.ts (1 hunks)
  • apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts (1 hunks)
  • packages/core/src/v3/schemas/runEngine.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

Files:

  • packages/core/src/v3/schemas/runEngine.ts
  • apps/supervisor/src/workloadManager/docker.ts
  • apps/supervisor/src/index.ts
  • apps/supervisor/src/workloadManager/types.ts
  • apps/supervisor/src/workloadManager/kubernetes.ts
  • apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • packages/core/src/v3/schemas/runEngine.ts
  • apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • packages/core/src/v3/schemas/runEngine.ts
  • apps/supervisor/src/workloadManager/docker.ts
  • apps/supervisor/src/index.ts
  • apps/supervisor/src/workloadManager/types.ts
  • apps/supervisor/src/workloadManager/kubernetes.ts
  • apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts
**/*.{js,ts,jsx,tsx,json,md,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier

Files:

  • packages/core/src/v3/schemas/runEngine.ts
  • apps/supervisor/src/workloadManager/docker.ts
  • apps/supervisor/src/index.ts
  • apps/supervisor/src/workloadManager/types.ts
  • apps/supervisor/src/workloadManager/kubernetes.ts
  • apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

Files:

  • apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Files:

  • apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts
apps/webapp/app/v3/services/**/*.server.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Organize services in the webapp following the pattern app/v3/services/*/*.server.ts

Files:

  • apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: julienvanbeveren
Repo: triggerdotdev/trigger.dev PR: 2417
File: apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.import.ts:56-61
Timestamp: 2025-08-19T09:49:07.011Z
Learning: In the Trigger.dev codebase, environment variables should default to `isSecret: false` when not explicitly marked as secrets in the syncEnvVars functionality. This is the intended behavior for both regular variables and parent variables.
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/app/**/*.{ts,tsx} : Access all environment variables through the `env` export of `env.server.ts` instead of directly accessing `process.env` in the Trigger.dev webapp
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/app/**/*.{ts,tsx} : Access all environment variables through the `env` export of `env.server.ts` instead of directly accessing `process.env` in the Trigger.dev webapp

Applied to files:

  • apps/supervisor/src/workloadManager/docker.ts
  • apps/supervisor/src/index.ts
  • apps/supervisor/src/workloadManager/kubernetes.ts
🧬 Code graph analysis (1)
apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts (2)
internal-packages/run-engine/src/run-queue/index.ts (5)
  • message (1458-1506)
  • message (1763-1818)
  • message (1859-1907)
  • message (1909-1937)
  • message (1958-1960)
apps/webapp/app/v3/machinePresets.server.ts (1)
  • machinePresetFromName (27-32)
🔇 Additional comments (5)
apps/supervisor/src/workloadManager/types.ts (1)

27-27: LGTM! Clean type addition.

The optional envVars field is well-placed and appropriately typed for carrying per-message environment variables through the workload creation pipeline.

apps/supervisor/src/index.ts (1)

270-270: LGTM! Proper forwarding of environment variables.

The envVars are correctly passed from the dequeued message to the workload manager, completing the pipeline from webapp to container/pod creation.

apps/supervisor/src/workloadManager/docker.ts (1)

113-117: LGTM! Consistent environment variable injection.

The implementation mirrors the existing additionalEnvVars pattern. Note that opts.envVars will override additionalEnvVars if there are duplicate keys, which appears intentional (portal-configured variables should take precedence over global configuration).

packages/core/src/v3/schemas/runEngine.ts (1)

273-273: LGTM! Backward-compatible schema extension.

The optional envVars field properly extends the DequeuedMessage schema with the correct Zod type, enabling environment variable propagation without breaking existing consumers.

apps/supervisor/src/workloadManager/kubernetes.ts (1)

228-233: LGTM! Consistent Kubernetes environment variable injection.

The implementation mirrors the additionalEnvVars pattern and maintains consistency with the Docker workload manager. Portal-configured variables will override global configuration if keys conflict, which aligns with expected behavior.

Comment on lines +381 to +408
const messagesWithEnvVars = await Promise.all(
messages.map(async (message) => {
const defaultMachinePreset = machinePresetFromName(defaultMachine);

const environment = await this._prisma.runtimeEnvironment.findFirst({
where: {
id: message.environment.id,
},
include: {
parentEnvironment: true,
},
});

const envVars = environment
? await this.getEnvVars(
environment,
message.run.id,
message.run.machine ?? defaultMachinePreset,
environment.parentEnvironment ?? undefined
)
: {};

return {
...message,
envVars,
};
})
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing taskEventStore parameter in getEnvVars call.

The getEnvVars method expects an optional taskEventStore parameter (line 582), which is used to set OTEL_RESOURCE_ATTRIBUTES for observability (lines 599-610). The startRunAttempt method correctly passes this parameter (line 481), but the dequeue enrichment does not.

Apply this diff to include the taskEventStore parameter:

         const envVars = environment
           ? await this.getEnvVars(
               environment,
               message.run.id,
               message.run.machine ?? defaultMachinePreset,
-              environment.parentEnvironment ?? undefined
+              environment.parentEnvironment ?? undefined,
+              message.run.taskEventStore ?? undefined
             )
           : {};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const messagesWithEnvVars = await Promise.all(
messages.map(async (message) => {
const defaultMachinePreset = machinePresetFromName(defaultMachine);
const environment = await this._prisma.runtimeEnvironment.findFirst({
where: {
id: message.environment.id,
},
include: {
parentEnvironment: true,
},
});
const envVars = environment
? await this.getEnvVars(
environment,
message.run.id,
message.run.machine ?? defaultMachinePreset,
environment.parentEnvironment ?? undefined
)
: {};
return {
...message,
envVars,
};
})
);
const messagesWithEnvVars = await Promise.all(
messages.map(async (message) => {
const defaultMachinePreset = machinePresetFromName(defaultMachine);
const environment = await this._prisma.runtimeEnvironment.findFirst({
where: {
id: message.environment.id,
},
include: {
parentEnvironment: true,
},
});
const envVars = environment
? await this.getEnvVars(
environment,
message.run.id,
message.run.machine ?? defaultMachinePreset,
environment.parentEnvironment ?? undefined,
message.run.taskEventStore ?? undefined
)
: {};
return {
...message,
envVars,
};
})
);
🤖 Prompt for AI Agents
In apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts around
lines 381-408, the call to getEnvVars is missing the optional taskEventStore
parameter; update the call to pass the same taskEventStore used elsewhere in
this class (the same variable passed in startRunAttempt at ~line 481) — e.g. add
, taskEventStore (or this._taskEventStore if it's a class member) as the last
argument so getEnvVars(environment, message.run.id, message.run.machine ??
defaultMachinePreset, environment.parentEnvironment ?? undefined,
taskEventStore).

@shawnfultz
Copy link
Author

Testing & Verification

Process

We've successfully tested this fix in a production-like environment:

  1. Built Custom Docker Images

    • Built patched versions of both and from the branch
    • Tagged as and pushed to Docker Hub
  2. Deployed to Development Kubernetes Cluster

    • Self-hosted Trigger.dev v4.0.4 on AWS EKS
    • Updated Helm deployment to use patched images:
      • docker.io/maceinnovations/trigger-supervisor:v4.0.4-patched
      • docker.io/maceinnovations/trigger-webapp:v4.0.4-patched
  3. Verified the Fix

    • ✅ Environment variables (including SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY) are now correctly injected into runner pods at creation time
    • ✅ Queued tasks are now executing successfully with access to portal-configured environment variables
    • ✅ Previously failing tasks now complete successfully

Root Cause Confirmed

The issue was that environment variables were being fetched in startRunAttempt() after the runner pod was already created, making them unavailable during task execution. This fix moves env var fetching to the dequeue() phase, ensuring they're included in the DequeuedMessage and injected into pods at creation time.

Impact

This fix resolves the critical issue where environment variables configured in the Trigger.dev portal were not being injected into self-hosted runner containers, causing tasks to fail when accessing required secrets and configuration.


🤖 Tested and verified in production environment

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.

[Bug] Environment Variables Not Injected into Runner Pods (Self-Hosted v4.0.4)

1 participant