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:
- Login to your LocalOps account with
ops login - Run
ops logs,ops connectorops proxyagainst an environment and service by name
kubectl at the environment
instead:
- Login to your LocalOps account with
ops login - Set the Kubernetes context of
kubectlto point at the environment’s cluster withops update-kubeconfig - Then use
kubectlto 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:
/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:
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:
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 throughops.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
DATABASE_URL in its environment.
Open a psql shell
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.Use a local GUI client (TablePlus, DataGrip, pgAdmin)
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:
Advanced workflows
The commands above cover the day-to-day work without leaving the CLI. Reach forkubectl 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.env-id, visit your environment dashboard and see “Shell” tab.
Use Kubectl
Afterupdate-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:
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 withops update-kubeconfig, simply launch k9s:
ops update-kubeconfig. To launch k9s directly in the
app-services namespace where your services run:
- 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
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:
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 withk9s -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.
Connect to a database with kubectl
These recipes do the same work asops 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:
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:
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:
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:
kubectl port-forward -n app-services pod/<db-pod> 5432:5432
reaches them directly.