# Run a command inside a service

Sometimes you need to be inside the service rather than looking at it from outside: to check what a config file actually contains, to run a migration, to reach a database from the machine that can reach it. There are three ways in, and they differ in whether you get a prompt and whether the command touches a replica that is serving traffic.

Where each action lives:

| Surface | Terminal, command and SSH actions |
| --- | --- |
| Product App | **Service → Terminal** opens the browser shell. **Run command** at the foot of the service panel runs a one-off command. SSH keys are registered with **Add key** under the environment's **Settings → SSH keys**. |
| CLI | Every action on this page: the CLI renders the same operation catalog as MCP. [Run it from your own terminal](/interfaces/cli). |
| MCP | Every action on this page. Tool names are linked per section. |
| REST | Every action on this page, under `/deployment/…`. [Browse the endpoints](/reference/openapi/deployment). |

## Choose the way in

| Way in | What it gives you | Touches a serving replica | Service must be running |
|---|---|---|---|
| The terminal | An interactive shell in the browser, inside a running replica. | Yes | Yes |
| A one-off command, in a running replica | A single command, output returned when it finishes. | Yes | Yes |
| A one-off command, as a separate run **(Recommended for anything that changes state)** | A single command in a fresh container from the service's current image, thrown away afterwards. | No | No |
| SSH | An interactive shell from your own terminal, using your own key. | Yes | Yes |

A separate run is the mode for database migrations, a backfill or a one-off import: running replicas are untouched, and the service does not need to be running at all.

## Open the terminal

1. Open the terminal on the service. It opens a shell inside one of its running replicas, in the browser.
2. Work as you would locally. It is a real interactive session: you get a prompt, command history, and programs that redraw the screen work. Lessly uses `bash` if your image has it and falls back to `sh` if it does not, so the shell you get depends on what you built.

A few limits keep forgotten tabs from holding sessions open:

- a session with no activity closes after **30 minutes**;
- every session closes after **4 hours**, active or not;
- you can have **3 sessions open at once**, and only **one per replica**.

**A service that is stopped or asleep has no replica to attach to.** Wake it or scale it up — see [Scale a service](/ship/deployment/scaling) — or use a one-off command as a separate run, which does not need one.

## Run a one-off command

1. Choose the mode. **In a running replica** is the default: the command runs alongside your application, with the same filesystem, the same processes and the same variables. Use it to look at something — read a file, check what the process sees, query an endpoint that is only reachable from inside. A heavy command competes with real requests on that replica.
2. **As a separate run** starts a fresh container from the service's current image with that service's variables, runs your command in it, and throws the container away. Use it for anything that changes state.
3. Send the command. It is handed to `/bin/sh -c`, so pipes, redirection and `&&` work as you would expect — and your image needs a shell for any of it to work.
4. Read the result. You get the standard output, the standard error and the exit code once the command finishes. It is non-interactive: nothing you run can prompt you for input.

Two limits to plan around:

| Limit | Value | What happens at the edge |
|---|---|---|
| Time | 5 minutes unless you ask for more; 30 minutes maximum. | The command is stopped and the result says so. |
| Output | The last 256 KiB of standard output and of standard error. | The beginning is dropped and the result is marked as truncated. For anything chatty, write to a file on a volume or filter the output down before it comes back. |

Through the Lessly MCP server: [`deployment_service_exec`](/reference/mcp-tools/deployment_service_exec). Token scope: a key with execute access to the service.

## Connect over SSH

If you would rather work from your own terminal, you can reach a service with a standard `ssh` client. Authentication is by public key only — there is no password to type or to leak.

1. Register the public half of your key pair under the environment's **Settings → SSH keys**, giving it a label such as `my-laptop`, and paste the OpenSSH public key line (`ssh-ed25519 …` or `ssh-rsa …`).
2. Copy the command Lessly gives you for the service: the host to connect to and a username built from the environment and the service.
3. Run it. You get an interactive shell inside a running replica, the same as the browser terminal. Appending a command to the `ssh` invocation runs just that command and returns, which is handy from a script.

A registered key belongs to your **product** rather than to a single service, so you register it once and it works for every service in it. The key list shows each key's label, its fingerprint, its type and when it was last used. Deleting a key takes effect immediately.

**Two things the SSH connection does not do: file transfer (`scp` and `sftp`) and port forwarding.** It is a command channel rather than a general-purpose tunnel. Copy files in by baking them into your image or putting them on a volume, and reach a private service by running the command inside the environment instead of forwarding a port out of it.

MCP: [`deployment_ssh_key_register`](/reference/mcp-tools/deployment_ssh_key_register), [`deployment_ssh_key_list`](/reference/mcp-tools/deployment_ssh_key_list), [`deployment_ssh_key_delete`](/reference/mcp-tools/deployment_ssh_key_delete), [`deployment_ssh_command`](/reference/mcp-tools/deployment_ssh_command).

## What does not survive

Whichever way you get in, you are working in a container's own filesystem, and that filesystem is temporary. A file you write from the terminal is gone as soon as that replica restarts, is replaced by a deploy, or is dropped by a scale-down. Nothing at all is shared between two separate runs, or between a separate run and the replicas serving traffic — each one starts from the image again.

Three things do persist, and they are the ones to reach for:

- a **volume** mounted into the service, which survives restarts and deploys;
- a **managed database**, which is a service of its own;
- **variables**, which are configuration rather than files.

The same applies to environment variables inside a session. **Exporting a variable in a shell changes that shell and nothing else** — it does not reach the running process, it does not reach the next replica, and it is gone when you disconnect. To change what a service runs with, change its variables and redeploy.

## Next steps

- [Set variables and secrets](/ship/deployment/variables): change what the service runs with, in a way that outlives the session.
- [Choose where state lives](/ship/deployment/data): put the migration's target, and anything you write, somewhere the next deploy keeps.
- [Scale a service](/ship/deployment/scaling): wake or scale up a service so there is a replica to attach to.
- [Watch a service](/ship/deployment/observability): read what the process printed, instead of going in to watch it.
