When logs aren’t enough - you need to actually look at the filesystem, hit the database, or run a one-off command in the service’s environment - the CLI gives you two surfaces: render ssh for live instances, and render psql for the database. Both work great interactively and even better in scripts.
render ssh: a shell on a running instance
SRV=$(render-id api)render ssh "$SRV"You land in a shell on the actual running instance. Same filesystem, same env vars, same PATH. Anything you do affects production traffic - exit when you’re done.
Run a single command and exit
For scripts, pass the command as the final argument(s):
render ssh "$SRV" -- env | grep DATABASE_URLrender ssh "$SRV" -- ls -la /tmpThe -- separator tells the CLI “anything after this is the remote command.” It’s exactly like ssh user@host -- command.
Quick recipes
# What env vars does the running service actually see?render ssh "$SRV" -- env
# What version of Node/Python/Ruby is actually running?render ssh "$SRV" -- node --version
# Disk usage - helpful when a persistent disk is filling uprender ssh "$SRV" -- df -h
# Heap snapshot for Node (writes to the disk that gets discarded on next deploy)render ssh "$SRV" -- node -e 'require("v8").writeHeapSnapshot("/tmp/heap.heapsnapshot")'The last one is in the “carefully” category - you’re running a real command in the live process’s environment. Fine for inspection, off-limits for cleanup-style mutations.
render ssh --ephemeral: the safe playground
--ephemeral (or -e) launches a fresh container, isolated from production traffic, with the service’s image and env vars but no incoming connections. Spin one up, mess around, exit, and it’s gone.
render ssh "$SRV" -eThis is the right surface for:
| Use it for | Why |
|---|---|
| Running a one-off database migration | Production traffic isn’t affected if it goes wrong; you exit and nothing persists |
| Reproducing a bug with a debugger attached | You can take your time without holding up real users |
| Testing a new dependency or binary version | Install, test, exit - the image isn’t permanently changed |
Running an rspec / pytest / smoke check against prod env vars | Same environment as the real service, but you can break it freely |
The shell exits and the container is destroyed. No cleanup, no lingering state.
Pre-deploy migration check pattern
A real-world example - run pending migrations in an isolated container before flipping a deploy:
render ssh "$SRV" -e -- npm run migrate:statusIf the output is what you expected, run the real migration in CI as part of a build step. If it isn’t, you’ve caught the surprise before production saw it.
render psql: one-shot SQL
render psql opens a psql session against a Render Postgres database - same auth, internal/external routing handled, no copy-pasted connection strings. Interactive use is exactly like psql:
DB=$(render-id app-db)render psql "$DB"You’re in the psql REPL. \q to exit.
One-shot queries
The power-user form takes -c for a single query, with -o json (or -o text -- --csv) for parseable output:
render psql "$DB" -c "SELECT NOW();" -o jsonrender psql "$DB" -c "SELECT count(*) FROM users WHERE created_at > NOW() - INTERVAL '24 hours';" -o jsonThe -o json flag wraps the result as a JSON array of row objects - directly pipeable to jq.
CSV output
For exports, hand flags through to the underlying psql with the -- separator:
render psql "$DB" -c "SELECT id, email, created_at FROM users LIMIT 100;" -o text -- --csv \ > users-sample.csv-- --csv says “after this, pass the rest as flags to psql itself.” psql’s --csv mode emits proper RFC 4180 CSV.
Run a file
For multi-statement scripts:
render psql "$DB" -- -f migrations/2026-05-21-add-index.sql-- -f file.sql is identical to psql -f file.sql - the -- lets you pass any psql flag through.
Scripted data checks
Two patterns you’ll reach for constantly.
Daily count snapshot
#!/usr/bin/env bashset -euo pipefailexport RENDER_API_KEY="${RENDER_API_KEY:?must be set}"export RENDER_OUTPUT=jsonDB="${RENDER_DB_ID:?must be set}"
render psql "$DB" -c " SELECT (SELECT count(*) FROM users) AS users_total, (SELECT count(*) FROM users WHERE created_at::date = CURRENT_DATE) AS users_today, (SELECT count(*) FROM orders WHERE created_at::date = CURRENT_DATE) AS orders_today;" | jq '.[0]'Pipe the result into Slack, a webhook, your inbox - whatever your team reads.
Pre-migration row-count check
#!/usr/bin/env bashset -euo pipefailexport RENDER_API_KEY="${RENDER_API_KEY:?must be set}"export RENDER_OUTPUT=jsonDB="${RENDER_DB_ID:?must be set}"
threshold=1000000n=$(render psql "$DB" -c "SELECT count(*) FROM events;" | jq -r '.[0].count')
if (( n > threshold )); then echo "events has $n rows (> $threshold). Migration may be slow; consider running off-hours." >&2 exit 1fi
echo "events has $n rows. Safe to migrate."render psql -c "SELECT count(*)..." -o json returns [{ "count": 1234 }]. The jq -r '.[0].count' extracts just the number.
Combining ssh and psql
Sometimes you want a SQL session as the service itself - same DATABASE_URL, same network position:
render ssh "$SRV" -e -- psql "$DATABASE_URL"This is the right choice for:
- Running a query that depends on a Postgres extension only installed inside the container
- Reproducing connection pool behavior from the service’s perspective
- Testing IP allowlists by going through the service’s network path
The -e (ephemeral) makes it safe to experiment. Exit when you’re done.
When to use which
flowchart TD q["What do you want?"] l["Read service logs"] inspect["Inspect live state (env, files, processes)"] experiment["Run a risky command"] query["Run a SQL query"] scriptq["Run a SQL query from a script"] logs["render logs"] ssh["render ssh"] e["render ssh --ephemeral"] psql["render psql"] scripted["render psql -c... -o json"] q --> l --> logs q --> inspect --> ssh q --> experiment --> e q --> query --> psql q --> scriptq --> scripted
Pick the smallest tool that answers the question. The cost of using render logs is nothing; the cost of using render ssh is “be careful.” The cost of --ephemeral is “a few extra seconds to spin up a container.” The cost of psql in the middle of a busy database is exactly what you’d expect.
Common mistakes
| Mistake | Fix |
|---|---|
Running destructive commands in render ssh (no --ephemeral) | Use --ephemeral for anything riskier than ls and env |
Forgetting -c and getting an interactive psql in a script | Always pass -c "SQL" for scripts; reserve interactive psql for humans |
Parsing psql text output with grep | Use -o json and jq, or -o text -- --csv for CSV |
render ssh hanging in CI | SSH isn’t for CI - use render deploys create and pre-deploy hooks instead |
What you learned
- `render ssh SRV` puts you in the live container; use `-- command` to run one thing and exit
- `render ssh SRV --ephemeral` is the safe playground - fresh container, same env, gone when you exit
- `render psql DB -c "SQL" -o json` is the scripting form; pipe to `jq` for whatever you need
- `-- --csv` and `-- -f file.sql` pass flags through to the underlying `psql`
- Pick the smallest tool that answers the question - logs before ssh, ssh -e before ssh, scripted psql before interactive