This guide covers deploying ProjectAchilles to Render using the included Blueprint (render.yaml) or manual setup, with Elastic Cloud for analytics.
Render runs two web services from this monorepo:
| Service | Root Directory | Dockerfile | Public Domain |
|---|---|---|---|
| achilles-backend | backend/ |
backend/Dockerfile |
Yes (agents connect here) |
| achilles-frontend | frontend/ |
frontend/Dockerfile |
Yes (users visit this) |
The frontend calls the backend directly via CORS (using VITE_API_URL). Elasticsearch is handled externally by Elastic Cloud.
Note: Render's private network DNS is only available on Standard plan and above. On Starter plan, services cannot resolve each other by name, so nginx proxy mode (
BACKEND_HOST) does not work. The frontend usesVITE_API_URLto call the backend's public URL directly instead.
- Render account (render.com) — Starter plan or above (required for persistent disks)
- GitHub repo with this project pushed
- Clerk application keys (dashboard.clerk.com) — create a separate Clerk app for Render
- Elastic Cloud deployment (cloud.elastic.co)
- GitHub Personal Access Token (if using a private test repo)
The fastest path — Render reads render.yaml and creates both services automatically.
-
Go to render.com/deploy and connect your GitHub repo
-
Render detects
render.yamland shows the Blueprint preview -
Review the services, then click Apply
-
After creation, go to each service's Environment tab and set the
sync: falsevariables:Backend (
achilles-backend):CLERK_PUBLISHABLE_KEY=pk_live_... CLERK_SECRET_KEY=sk_live_... CORS_ORIGIN=https://<your-frontend>.onrender.com AGENT_SERVER_URL=https://<your-backend>.onrender.com TESTS_REPO_URL=https://github.com/your-org/f0_library.git AGENT_REPO_URL=https://github.com/your-org/ProjectAchilles.git GITHUB_TOKEN=ghp_... ELASTICSEARCH_CLOUD_ID=<from Elastic Cloud console> ELASTICSEARCH_API_KEY=<from Elastic Cloud console>Frontend (
achilles-frontend):CLERK_PUBLISHABLE_KEY=pk_live_... VITE_API_URL=https://<your-backend>.onrender.comImportant:
CORS_ORIGINandAGENT_SERVER_URLmust be full URLs with thehttps://scheme.VITE_API_URLtells the frontend where to send API calls (bypassing nginx proxy, which requires private network DNS not available on Starter plan). -
Trigger a manual deploy on both services (or push a commit)
SESSION_SECRETandENCRYPTION_SECRETare auto-generated by Render during Blueprint deploy. Do not override them unless you're migrating from another deployment.
-
In Render Dashboard, click New → Web Service
-
Connect your GitHub repo
-
Configure:
- Name:
achilles-backend - Root Directory:
backend - Runtime: Docker
- Plan: Starter ($7/mo — required for disk)
- Name:
-
Add a Disk (Settings → Disks):
Mount Path Size Purpose /root/.projectachilles1 GB SQLite database, certificates, encrypted settings -
Set environment variables (Environment tab):
Variable Value Notes NODE_ENVproductionPORT3000CLERK_PUBLISHABLE_KEYpk_live_...From Clerk dashboard CLERK_SECRET_KEYsk_live_...From Clerk dashboard SESSION_SECRET<openssl rand -base64 32>Generate a random secret ENCRYPTION_SECRET<openssl rand -base64 32>Required — see note below CLI_AUTH_SECRET<openssl rand -base64 32>Required for CLI login ( achilles login)CORS_ORIGINhttps://achilles-frontend.onrender.comYour frontend's Render URL AGENT_SERVER_URLhttps://achilles-backend.onrender.comYour backend's Render URL TESTS_REPO_URLhttps://github.com/your-org/f0_library.gitTest library repo TESTS_REPO_BRANCHmainAGENT_REPO_URLhttps://github.com/your-org/ProjectAchilles.gitAgent source for builds AGENT_REPO_BRANCHmainGITHUB_TOKENghp_...PAT with reposcopeELASTICSEARCH_CLOUD_ID<from Elastic Cloud>ELASTICSEARCH_API_KEY<from Elastic Cloud>See permissions below Elasticsearch API Key Permissions: Create the key in Kibana (Stack Management → API Keys) with these role descriptors:
{ "achilles_role": { "cluster": ["monitor"], "indices": [{ "names": ["achilles-*", "archived-*"], "privileges": ["manage", "read", "write"], "allow_restricted_indices": false }] } }ENCRYPTION_SECRETis required on Render. Without it, the backend derives a key from the container's hostname, which changes across deploys and corrupts encrypted settings.
-
Click New → Web Service
-
Connect the same GitHub repo
-
Configure:
- Name:
achilles-frontend - Root Directory:
frontend - Runtime: Docker
- Plan: Starter ($7/mo)
- Name:
-
Set environment variables:
Variable Value Notes CLERK_PUBLISHABLE_KEYpk_live_...Same publishable key as backend VITE_API_URLhttps://achilles-backend.onrender.comBackend's public URL (direct CORS)
- Wait for both services to build and deploy (first build takes 3-5 minutes)
- Visit your frontend URL — you should see the Clerk login page
- Check backend health:
curl https://achilles-backend.onrender.com/api/health - After logging in, go to Analytics → Setup and verify the Elastic Cloud connection
On Starter plan, the frontend calls the backend directly via CORS using the VITE_API_URL environment variable (set to the backend's public https://*.onrender.com URL). The docker-entrypoint.sh script injects this as window.__env__.VITE_API_URL at container start, and the axios client reads it at runtime.
On Standard plan and above, you can optionally use Render's private network for nginx proxy mode instead. Set BACKEND_HOST to the backend's service name and omit VITE_API_URL — nginx will proxy /api/* requests over the private network. This avoids exposing the backend publicly, but requires the higher-tier plan.
| Plan | Frontend → Backend | Env Var | Backend Exposure |
|---|---|---|---|
| Starter | Direct CORS | VITE_API_URL=https://<backend>.onrender.com |
Public |
| Standard+ | nginx proxy (private network) | BACKEND_HOST=<backend-service-name> |
Can be private |
The backend uses a 1 GB persistent disk at /root/.projectachilles:
| Path | Purpose |
|---|---|
agents.db |
SQLite database (agents, tokens, tasks, schedules) |
analytics.json |
Encrypted Elasticsearch connection settings |
tests.json |
Test repository configuration |
certs/ |
Code signing certificates (max 5, subdirectory per cert) |
binaries/ |
Built agent binaries organized by <os>-<arch>/ |
go-cache/mod/ |
Go module cache (persisted across redeploys) |
go-cache/build/ |
Go build cache (persisted across redeploys) |
agent-source/ |
Sparse-checkout clone of the agent Go source |
Note: Persistent disks disable Render's zero-downtime deploys. During a deploy, there will be brief downtime while the new container starts. This differs from Railway, which supports zero-downtime deploys with volumes.
Go cache persistence: The Go module and build caches (
go-cache/) are stored on the persistent disk so thatgo mod downloadand compilation results survive container redeploys. Without this, every agent build would re-download all Go dependencies and recompile from scratch, adding 1-2 minutes per build.
With GitHub integration, every push to your configured branch triggers a rebuild. The render.yaml uses buildFilter.paths to only rebuild a service when its relevant files change:
- backend rebuilds on:
src/**,package.json,Dockerfile,tsconfig.json - frontend rebuilds on:
src/**,package.json,Dockerfile,nginx.conf,docker-entrypoint.sh
A commit touching only frontend/src/ will not trigger a backend redeploy.
Three domains are needed for a production deployment: frontend, backend, and Clerk.
In each service's Settings → Custom Domains, add:
| Service | Custom Domain | Example |
|---|---|---|
| Frontend | <prefix>.yourdomain.com |
tpsgl.projectachilles.io |
| Backend | <prefix>.agent.yourdomain.com |
tpsgl.agent.projectachilles.io |
Add CNAME records in your DNS provider:
| Record | Type | Target |
|---|---|---|
<prefix>.yourdomain.com |
CNAME | <your-frontend>.onrender.com |
<prefix>.agent.yourdomain.com |
CNAME | <your-backend>.onrender.com |
clerk.<prefix>.yourdomain.com |
CNAME | frontend-api.clerk.services |
The Clerk CNAME is configured in Clerk Dashboard → Domains when adding a production instance (see Clerk Setup below). Clerk provides the exact CNAME target.
After DNS propagates and Render provisions TLS certificates:
| Variable | Service | New Value |
|---|---|---|
CORS_ORIGIN |
Backend | https://<prefix>.yourdomain.com |
AGENT_SERVER_URL |
Backend | https://<prefix>.agent.yourdomain.com |
VITE_API_URL |
Frontend | https://<prefix>.agent.yourdomain.com |
In your Clerk application's Domains settings:
- Add the frontend custom domain as an allowed origin
- If using a production instance, the Clerk-hosted sign-in pages will use
clerk.<prefix>.yourdomain.comautomatically
Create a separate Clerk application for your Render deployment. Clerk development and production instances behave differently — production requires additional OAuth configuration.
- In dashboard.clerk.com, create a new application
- Enable desired social providers (GitHub, Google, etc.)
- Copy the publishable key (
pk_test_...) and secret key (sk_test_...) to both services' environment variables
When you're ready to use custom domains instead of *.onrender.com:
- In Clerk Dashboard, go to Configure → Production
- Add your custom domain — Clerk will provide DNS records to add (a CNAME for
clerk.<your-domain>) - After DNS verification, Clerk generates production keys (
pk_live_.../sk_live_...) - Update
CLERK_PUBLISHABLE_KEYandCLERK_SECRET_KEYon both Render services with the production keys
Important: Development keys (
pk_test_) and production keys (pk_live_) are not interchangeable. After switching to production, the development keys stop working for that instance.
This step is critical. Clerk development instances use Clerk's shared OAuth credentials for social providers (GitHub, Google, etc.) — login works out of the box. Production instances require your own OAuth credentials. Without them, social login buttons will redirect to the provider with an empty client_id, resulting in a 404 error.
-
Go to github.com/settings/developers → OAuth Apps → New OAuth App
-
Configure:
Field Value Application name ProjectAchilles - Render(or your preferred name)Homepage URL https://<your-frontend-domain>Authorization callback URL https://clerk.<your-domain>/v1/oauth_callback -
After creation, generate a Client Secret
-
In Clerk Dashboard → Configure → SSO Connections → GitHub:
- Enter the Client ID and Client Secret
- Save
- Go to console.cloud.google.com/apis/credentials
- Create an OAuth 2.0 Client ID (Web application type)
- Add authorized redirect URI:
https://clerk.<your-domain>/v1/oauth_callback - In Clerk Dashboard → Configure → SSO Connections → Google:
- Enter the Client ID and Client Secret
- Save
OAuth callback URL pattern: All social providers use
https://clerk.<your-domain>/v1/oauth_callbackas the redirect URI. Replace<your-domain>with your frontend's custom domain (e.g.,clerk.tpsgl.projectachilles.io).
The backend Docker image includes Go 1.24.3, so agent cross-compilation works on Render — same as Railway. Set AGENT_REPO_URL to your ProjectAchilles repo and the backend will git-clone the agent/ subdirectory at startup (sparse checkout), then use it for Go cross-compilation.
| Variable | Value | Notes |
|---|---|---|
AGENT_REPO_URL |
https://github.com/your-org/ProjectAchilles.git |
Required for agent builds |
AGENT_REPO_BRANCH |
main |
Branch to clone from |
GITHUB_TOKEN |
ghp_... |
Required if the repo is private (same token used for test library) |
If AGENT_REPO_URL is not set, the backend disables the build feature and shows "Agent build from source is not available" in the UI. You can still upload pre-built binaries manually.
With Elastic Cloud handling analytics externally:
| Service | Est. Monthly Cost |
|---|---|
| Backend (Starter plan) | ~$7 |
| Frontend (Starter plan) | ~$7 |
| Persistent disk (1 GB) | ~$0.25 |
| Total | ~$14 |
For comparison, Railway runs the same stack for ~$10-13/mo (usage-based pricing). Render's Starter plan is a flat rate. See Render pricing for current rates.
Generate all secrets at once:
./scripts/generate-secrets.sh --target renderInteractive setup wizard:
./scripts/setup.sh # Select: PaaS → RenderInitialize Elasticsearch indices on Elastic Cloud:
./scripts/init-elasticsearch.sh --cloud-id "deploy:..." --api-key "..."On Starter plan, this means nginx can't resolve the backend hostname (private network DNS is not available). Fix:
- Set
VITE_API_URLto the backend's public URL (e.g.,https://achilles-backend.onrender.com) on the frontend service - Remove
BACKEND_HOSTandBACKEND_PORTif set — they require Standard plan or above
On Standard plan and above with nginx proxy mode:
- Verify
BACKEND_HOSTis set to the backend's service name - Both services must be in the same Render region
- The backend service must be running and healthy
The backend's CORS_ORIGIN doesn't match the frontend's origin. Verify:
CORS_ORIGINis set to the frontend's full URL with scheme (e.g.,https://achilles-frontend.onrender.com)- Do not use just the hostname — the
https://prefix is required
ENCRYPTION_SECRET is not set. Without it, the backend derives a key from the container's hostname, which changes on every deploy. Set a stable ENCRYPTION_SECRET in the Render environment variables.
Set AGENT_SERVER_URL to the backend's public Render domain (with https://), e.g., https://achilles-backend.onrender.com.
The Elastic Cloud connection is stored in the encrypted analytics.json file. If ENCRYPTION_SECRET changed, the file becomes unreadable. Reconfigure via Analytics → Setup, or set ELASTICSEARCH_CLOUD_ID and ELASTICSEARCH_API_KEY as env vars (env vars take priority over the file).
Symptom: Clicking "Sign in with GitHub" redirects to GitHub with client_id= (empty), and GitHub returns a 404 page. Google login may fail similarly.
Cause: You are using a Clerk production instance but haven't configured custom OAuth credentials. Production instances require your own GitHub/Google OAuth app credentials — unlike development instances, which use Clerk's shared credentials automatically.
Fix: Follow the "Configure OAuth Credentials" steps in the Clerk Setup section above. Every social provider enabled in Clerk needs its own Client ID and Client Secret when running in production mode.
The AGENT_REPO_URL environment variable is not set, or the Git clone failed at startup. Check:
AGENT_REPO_URLis set to the full HTTPS URL of your ProjectAchilles repoGITHUB_TOKENis set if the repo is private (the token is injected into the clone URL)- The container logs for git clone errors (e.g., authentication failures, branch not found)
The agent source is cloned once at startup via sparse checkout (only the agent/ subdirectory). If the clone fails, the build feature is disabled and the Agent tab shows "Agent build from source is not available."
Render caches Docker layers between builds. The first build takes 3-5 minutes; subsequent builds with only source code changes take ~1-2 minutes. If builds are consistently slow, check that buildFilter.paths is configured correctly so unrelated changes don't trigger rebuilds.
Go agent builds also benefit from persistent caches — the first agent build downloads all Go modules (~30s), but subsequent builds reuse the cached modules from the persistent disk.