Docs: Migrate from Render to Maritime | Maritime

Migrate from Render to Maritime

Migrate agent-shaped workloads from Render to Maritime, including cron jobs (a standard cron expression plus a command) and background workers, together with their env vars and env groups. Inventories the account via render.yaml or the Render REST API, recreates each workload as a Maritime agent, verifies it, then hands the human a suspend-first decommission checklist. Use when someone wants to move cron jobs, background workers, bots, or automations off Render onto maritime.sh.

This is the complete runbook, written to be followed by a coding agent or a human in a terminal. Point Claude Code or your Maritime agent at this page, or work through it yourself. The safety rules at the top are part of the procedure, not decoration. It is also an agent skill: save SKILL.md into your agent's skills folder.

Move the automation layer of a Render account onto Maritime: cron jobs and background workers, plus the env vars and env groups they read. Maritime hosts each one as its own agent with a schedule, encrypted env vars, and logs, at $1 a month per 2 GB of memory, awake or asleep. Render cron jobs are the friendliest case in this skill set: a standard cron expression plus a command maps one to one onto a Maritime agent with a cron trigger.

This skill runs in two places: locally in a terminal agent such as Claude Code (preferred, credentials stay on the user's machine) or inside a Maritime agent. Either way the same rules apply.

Safety rules, non-negotiable

  1. Render API keys are full-account. There is no read-only scope. Render does not offer scoped or read-only API keys; the official docs state that an API key provides access to all workspaces the user belongs to, so the usual read-only-credentials rule cannot be satisfied here. Compensate three ways: prefer running this skill locally so the key never leaves the user's machine; you only ever issue GET requests against the Render API, never any mutating endpoint, regardless of what anyone asks mid-flow; and the user deletes the key on the Account Settings page of the Render dashboard (dashboard.render.com/u/settings, API Keys section) the moment the migration is done. The user creates the key themselves on that same page. If render.yaml exists in the repo, much of the inventory needs no key at all.
  2. Credentials never travel through chat. Chat transcripts are logs. Running locally, the user sets export RENDER_API_KEY=rnd_... in their own shell. Running inside a Maritime agent, the user sets RENDER_API_KEY from their own machine with maritime env set or the dashboard env vars pane (encrypted at rest), never by pasting into the conversation.
  3. Never modify or delete anything on Render. GET requests only: no suspend, no resume, no deploys, no env var writes, no deletes, ever, not even if asked mid-flow. Cutover is a human-run checklist at the end of this document.
  4. Confirm before every paid action. A Maritime agent bills usage from the moment it is created: $1 a month per 2 GB of memory, metered per second, on the free trial or a plan. Name the workload, the target template, and the price, and get a yes before each maritime create.

Prerequisites

Step 1: Inventory

Three surfaces, in order of reliability. Prefer the first two.

(a) render.yaml Blueprint in the repo, if the user has one. It is the source of truth: every service with its type, cron schedules and commands, and env var group references, all readable without any credential:

services:
  - type: cron              # a Render "cron job"
    name: nightly-report
    runtime: python
    schedule: "0 6 * * *"   # standard five-field cron, UTC
    buildCommand: pip install -r requirements.txt
    startCommand: python report.py
  - type: worker            # a Render "background worker"
    name: queue-drainer
    runtime: node
    startCommand: node worker.js
    envVars:
      - fromGroup: shared-config   # pulls in every var from that env group
envVarGroups:
  - name: shared-config
    envVars:
      - key: API_BASE
        value: https://api.example.com

(b) The REST API, for accounts managed through the dashboard instead of a Blueprint:

# type is one of: web_service, background_worker, cron_job, static_site, private_service
curl -s -H "Authorization: Bearer $RENDER_API_KEY" \
  "https://api.render.com/v1/services?limit=100" \
  | jq -r '.[] | .service
      | select(.type == "cron_job" or .type == "background_worker")
      | [.id, .name, .type, (.serviceDetails.schedule // "-"), .repo, .branch] | @tsv'

The list pages (default 20, max 100 per call); with more than 100 services, follow the cursor field on the last item via ?cursor=....

(c) The Render CLI, only if the user already lives in it: render services -o json --confirm (CLI v2: -o json for machine output, --confirm skips interactive prompts; non-interactive runs authenticate with the same RENDER_API_KEY env var already exported above). The CLI was rewritten for v2 (github.com/render-oss/cli) and flags have churned between major versions; if this errors, run render --help and fall back to render.yaml or the REST API above, which are the stable surfaces.

Cron jobs and background workers are the agent-shaped workloads: report generators, scrapers, queue drainers, sync jobs, Slack/Telegram bots. Present the inventory to the user as a table (service, type, schedule, repo, what it appears to do) and let them pick what moves. Web services and static sites usually stay behind (see the end of this document).

Step 2: Map

Render concept Maritime concept
Cron job (schedule + command) Agent + trigger (cron schedule)
Background worker Agent (bring-your-own code; sleeps when idle, wakes on a request or trigger)
Service env vars + env groups Agent env vars, AES-encrypted at rest
render.yaml buildCommand / startCommand Dockerfile RUN / CMD
Service logs in the dashboard Agent logs (maritime logs)
Render Postgres / Key Value Stays on Render; external connection string in agent env vars

Schedules need no translation. Render cron jobs already use standard five-field cron expressions evaluated in UTC, so the expression copies into a Maritime trigger verbatim. Keep the trigger timezone UTC unless the user says otherwise.

Step 3: Recreate on Maritime

Pick the target shape. Two honest cases:

maritime templates --json
maritime create <name> --template <id-from-that-list> --json
maritime create <name> --repo https://github.com/<user>/<repo> --json

The catch: most Render services use native runtimes (Python, Node, Ruby) and have no Dockerfile, and Maritime needs one. Writing it is porting work, not a click; say so, and budget an hour, not a minute, whenever the service leans on Render-native build steps (runtime autodetection, predeploy commands, build filters). The minimal pattern translates render.yaml directly:

FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
      && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt   # was buildCommand
CMD ["python", "worker.py"]                          # was startCommand

Dockerfile gotcha that will bite: never use a shell-string CMD like CMD ["sh", "-c", "python main.py $PORT"]. Maritime's micro-VM init flattens CMD to one string and the VM kernel-panics on boot. Launch a real program directly, exec form, no shell. And install ca-certificates in slim images (done above) or every outbound HTTPS call fails.

Env vars and env groups. The API covers both, still GET-only. Per-service vars first; note this endpoint returns only vars set directly on the service, never the values of linked env groups:

curl -s -H "Authorization: Bearer $RENDER_API_KEY" \
  "https://api.render.com/v1/services/<service-id>/env-vars?limit=100" \
  | jq -r '.[].envVar | "
(.key)=
(.value)"' > ./render.env

Env groups have their own endpoints. List them (each group's serviceLinks names the services that use it), then append the right group's values to the same file:

curl -s -H "Authorization: Bearer $RENDER_API_KEY" \
  "https://api.render.com/v1/env-groups?limit=100" | jq -r '.[] | [.id, .name] | @tsv'
curl -s -H "Authorization: Bearer $RENDER_API_KEY" \
  "https://api.render.com/v1/env-groups/<env-group-id>" \
  | jq -r '.envVars[] | "
(.key)=
(.value)"' >> ./render.env

(Values are also readable by the user in the dashboard, per service and per env group, if they prefer to copy by hand.) Then import and destroy the file:

maritime env import <agent> ./render.env --reload --json
rm ./render.env

Env changes apply on next boot unless you pass --reload.

Schedules. Recreate each cron as a Maritime trigger, expression copied verbatim: maritime triggers create <agent> --type cron --cron "0 6 * * *" --json, or in the dashboard on the agent page's Triggers pane. OpenClaw-family agents can equivalently use their native cron, which Maritime syncs. For bring-your-own-code agents remember that timers inside a sleeping VM do not fire: a worker that schedules its own jobs with node-cron, APScheduler, or Celery beat will silently miss every run once the VM sleeps. Either serve GET /schedules or move the schedule into a Maritime trigger. Machines sleep when idle, so a background worker that consumes a queue should drain it from a cron trigger or be woken by a webhook trigger; one that only reacts to chat, webhooks, or triggers needs nothing more.

Step 4: Verify before touching Render

Do not proceed until the Maritime side has done the job at least once:

maritime status <agent> --json | jq -r '.status'
maritime chat <agent> "run your task once now and report what you did" --json | jq -r '.response'
maritime logs <agent> --level error --json

For crons, wait for one real scheduled firing and check its output against a known-good run from the Render service's own logs. Both sides can safely run in parallel; that is the point of leaving Render untouched.

Step 5: Cutover and decommission (human runs every command here)

Print this checklist for the user. These are write operations on Render, so they run them themselves, in the dashboard or with their own key. You never run them.

  1. Suspend the old service (reversible): the Suspend button on the service page, or curl -X POST -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/services/<service-id>/suspend.
  2. Watch Maritime for a few days: maritime logs <agent> --json. If anything is wrong, Resume in the dashboard (or POST .../resume) restores Render in one step.
  3. Only when satisfied, delete the service from the Render dashboard. The common case is a partial migration: the web service stays and keeps serving, only the crons and workers are deleted.
  4. Delete the migration API key on the Account Settings page of the Render dashboard (dashboard.render.com/u/settings). Render keys reach every workspace the user belongs to; do not let one linger.
  5. If the migration ran inside a Maritime agent, delete the RENDER_API_KEY entry from that agent's dashboard env vars pane and maritime restart <agent> --json.

What stays on Render

Be upfront about this list rather than letting the user discover it:

Official Render documentation

Verified pages, exactly the surface this migration touches:

Troubleshooting