Skip to main content
LocalOps CLI ops can be used to read logs from your services, get shell access to them and to their databases, forward their ports to your machine, and reach the underlying Kubernetes cluster of an environment.

How it works

For the things you reach for most often - reading logs, opening a shell, forwarding a port, connecting to a database - ops logs, ops connect and ops proxy talk to your environment directly. You need neither kubectl nor the pod and namespace names:
  1. Login to your LocalOps account with ops login
  2. Run ops logs, ops connect or ops proxy against an environment and service by name
To inspect anything else in the cluster - deployments, jobs, secrets, events - point kubectl at the environment instead:
  1. Login to your LocalOps account with ops login
  2. Set the Kubernetes context of kubectl to point at the environment’s cluster with ops update-kubeconfig
  3. Then use kubectl to inspect the cluster

Login to LocalOps

Basic usage

Reading logs, opening a shell and forwarding a port - each of these takes an environment and a service by name.

Stream logs from a service

ops logs streams logs from a service straight to your terminal:
--env and --svc take the environment and service names shown in your dashboard, or their ids. --svc is required; --env falls back to the ENV environment variable when you leave it out. The stream stays open and prints new lines as they arrive - press Ctrl-C to stop. Pass --follow=false for a snapshot that prints and exits. When a service runs more than one pod, all of them are read at once and every line is prefixed with the pod it came from, so you can tell replicas apart:
--tail and --since apply to each pod, not to the service as a whole - --tail 100 against a service with three pods can print up to 300 lines. Lines are printed as they arrive rather than merged into one chronological order, so add --timestamps when ordering across replicas matters.

Shell into a service

ops connect opens a shell inside a pod of your service, without looking up pod or namespace names:
That gives you /bin/sh on the first ready pod. To use a different shell, or to run a single command and exit, put it after --:
Both commands take the default pod selection silently so they stay usable in scripts. Add -i when you want to choose instead. On a service with several pods it shows a list of them, and the first entry keeps the default behaviour:
ops connect puts you on a live container serving traffic. Anything you change inside it is lost the next time the pod restarts or is replaced by a deployment - use it to inspect and debug, and make lasting changes through your repo instead.

Forward a service port to localhost

ops proxy opens a tunnel from your machine to a service running in an environment, so you can reach an internal service as though it were running locally:
It runs in the foreground and closes the tunnel when you stop it with Ctrl-C. By default it binds the same port locally that the service listens on, taken from the service’s configuration.
The tunnel attaches to a single pod, so it does not load balance across replicas. That is usually what you want when debugging, but it means restarting that pod ends the tunnel.

Connect to your databases or cache

Resources provisioned through ops.json - RDS instances and ElastiCache clusters - live in the private subnets of your environment’s VPC and only accept connections from inside it. There is no public endpoint to point psql at from your laptop, by design. So you connect from a throwaway pod inside the cluster instead. ops connect --dep and ops proxy --dep do that for you: they start a short-lived pod in your environment, connect it to the database, and clean it up when you exit.
These commands only reach resources you provisioned through ops.json - for example an RDS instance or an ElastiCache cluster. LocalOps creates those inside your environment’s VPC and injects their credentials into your service, which is what --dep reads. A database you host or manage yourself is not reachable this way.

First, export $dsn

Export $dsn in your ops.json. It is a complete, ready-to-use connection URL that LocalOps assembles for you - host, port, username, password and database name already filled in and correctly escaped. It is also what the --dep commands below look for, so they will not find a database without it:
ops.json
Deploy once, and every container of that service has DATABASE_URL in its environment.
$dsn embeds the database password, so handle the value exactly as you would a password. Don’t paste it into tickets, Slack messages or shared docs, don’t commit it, and don’t save it into a GUI client’s connection profile - read it from a pod each time you need it. When managed_password is true, RDS rotates the password on its own schedule, so a copied URL is both a leaked secret and a value with an expiry date.

Open a psql shell

That starts a postgres:17-alpine pod in your environment, hands it the same database credential your service uses, and drops you at a psql prompt. Exit the shell and the pod is deleted. Bare --dep picks the dependency for you when the service has exactly one database. Name it when there are several - either by its ops.json id or by the variable it exports:
Only Postgres is supported today. MySQL and Redis are coming soon. --db-image overrides the client image if you need a specific psql version.
The credential is never read by the CLI or printed to your terminal. The client pod is given the same Kubernetes Secret reference your service has, so the password is injected by the cluster. It stays out of your shell history and out of your scrollback.

Use a local GUI client (TablePlus, DataGrip, pgAdmin)

That relays the private database endpoint to your machine and prints the connection details to use:
Point your GUI client at localhost on that port with those credentials, and leave the command running while you work. Ctrl-C closes the tunnel and removes the relay pod. Pass --local-port if 5432 is already taken locally:
Use SSL mode require rather than verify-full. The database’s certificate is issued for its real hostname, not localhost, so certificate verification through the tunnel will fail. For psql that means PGSSLMODE=require, since psql has no sslmode flag.
Every recipe on this page connects you to a real database. On production environments, prefer read-only queries and keep in mind that anything you run has immediate effect - there is no staging step between your prompt and your customers’ data.

Advanced workflows

The commands above cover the day-to-day work without leaving the CLI. Reach for kubectl and k9s when you need to inspect anything else in the cluster - deployments, jobs, secrets, events - or to reach a pod that is not one of your own services.

Set kubernetes context

Use this commnd updates your local current context to point at the environment’s kubernetes cluster.
To get env-id, visit your environment dashboard and see “Shell” tab.

Use Kubectl

After update-kubeconfig command succeeds, you can use any kubectl command to inspect pods, deployments, jobs and everything else running in your LocalOps environment. All your services are deployed in the app-services namespace. To see only your service pods:
To see all cluster resources across all namespaces:

Monitor pods faster than kubectl with k9s

k9s is a terminal-based UI that makes it easy to monitor and manage Kubernetes resources. After setting up your kubeconfig with ops update-kubeconfig, simply launch k9s:
k9s will automatically use the current Kubernetes context set by ops update-kubeconfig. To launch k9s directly in the app-services namespace where your services run:
From the k9s interface you can:
  • View all pods and their status in real time
  • Watch logs by selecting a pod and pressing l
  • Filter resources by namespace using / to search
  • Delete, describe, or edit resources interactively
Install k9s via Homebrew with brew install derailed/k9s/k9s on macOS, or see the k9s installation guide for other platforms.

Shell into a pod

ops connect is the shortest way to get a shell of one of your own services. Reach for kubectl when you need a shell on a pod that is not a LocalOps service - a database pod, or something you installed into the cluster yourself. To get a shell inside a running pod, first identify the pod name in the app-services namespace:
Then exec into the pod:
Some containers may use /bin/bash instead of /bin/sh. If /bin/sh fails, try /bin/bash.

Shell into a pod using k9s

k9s gets you the same shell in fewer keystrokes, and it saves you from having to know the pod name up front. Launch it against your services with k9s -n app-services, press / and type part of your service name to filter the list, move to the pod you want with the arrow keys, and press s. k9s opens an interactive shell inside that pod - the equivalent of the kubectl exec command above. If the pod runs more than one container (say your service plus a sidecar), press Enter on the pod first to list its containers, highlight the one you want, and press s there instead. When you are done, type exit or press Ctrl-d to close the shell and drop back into the k9s pod list, and Esc to back out of any k9s view. Unlike a hand-written kubectl exec, k9s tries a few common shells rather than a single hard-coded path, so the /bin/sh versus /bin/bash guesswork above usually doesn’t come up.
A shell opened this way is a shell on a live container serving traffic. Anything you change inside it is lost the next time the pod restarts or is replaced by a deployment - use it to inspect and debug, and make lasting changes through your repo instead.

Connect to a database with kubectl

These recipes do the same work as ops connect --dep and ops proxy --dep by hand. Reach for them when you need something --dep does not cover yet - a MySQL shell, or a dependency that exports discrete fields rather than $dsn. Use DATABASE_URL instead of typing out <USERNAME>, <PASSWORD> and <HOST> yourself. Hand-assembling the URL means looking up four separate values, escaping any special characters in the password correctly, and leaving credentials in your shell history - and it breaks silently the moment RDS rotates the password. Reading $dsn off a pod avoids all of that. List your pods, then read the variable off any pod belonging to that service:
Start a one-shot client pod in the same namespace, passing the DSN straight from the running pod into the new one. The URL never lands in your shell history, and the client pod is deleted the moment you exit:
MySQL is the same command with a different image and client. Use mysqlsh, which ships in the official mysql:8 image and accepts the connection URL as-is - --sql starts it in SQL mode so it behaves like the classic mysql prompt:
Reach for mysqlsh rather than the classic mysql client here, which takes discrete -h / -u / -p flags and no URL. Pick a client image whose major version matches your server too - an older client against a newer server can fail on newer authentication methods. If you haven’t exported $dsn yet, you can still connect by spelling the credentials out and reading each value from kubectl exec -n app-services <pod-name> -- printenv | grep -i db, but adding $dsn to exports is a one-line change that makes every command above shorter and safer. Or relay the endpoint by hand, with a TCP forwarder pod and a port-forward to it:
Leave that running and point your GUI client at localhost:5432. Take the username, password and database name from the DATABASE_URL you read above rather than looking them up separately - most clients let you paste the whole URL and fill the fields in for you. Use SSL mode require rather than verify-full, since the certificate is issued for the database’s real hostname, not localhost. Delete the forwarder when you’re done:
Ephemeral preview databases run as pods in the cluster rather than as RDS instances, so they need no forwarder at all - kubectl port-forward -n app-services pod/<db-pod> 5432:5432 reaches them directly.