Remote and Container Connections

Most of the time you’ll run CIDER against an nREPL server on your own machine, as described in Up and Running. This page covers the trickier setups: reaching a server on another host, inside a container, or over a Unix domain socket.

nREPL connections are completely insecure by default - there’s no authentication, and anyone who can reach the port gets arbitrary code execution inside your process. Never expose an nREPL port directly to a network you don’t fully trust.

For any non-local server - and especially a production one - SSH tunneling is the way to go: keep nREPL bound to the remote’s own localhost, and reach it through an encrypted SSH tunnel that piggybacks on your existing SSH credentials. CIDER can set up that tunnel for you.

Working with Remote Hosts

Sometimes the nREPL server lives elsewhere - another host on your network, a container, or even a production box.

There are two ways to get there.

The first is to start the nREPL server yourself and point cider-connect (or cider-connect-cljs) at the remote host and port. For remote hosts CIDER can tunnel the connection over SSH rather than connecting directly:

  • nrepl-use-ssh-fallback-for-remote-hosts (default nil) - fall back to an SSH tunnel when a direct connection to a remote host fails.

  • nrepl-force-ssh-for-remote-hosts (default nil) - skip the direct attempt and always tunnel. This is the right setting when the nREPL port is bound only to the remote’s localhost, which is what you want for anything reachable from an untrusted network.

CIDER forwards a free local port to the nREPL port on the remote and connects to that local port, so tunnels to several remote servers that happen to use the same nREPL port don’t collide. If the forwarding doesn’t come up within nrepl-ssh-tunnel-timeout seconds CIDER gives up with an error rather than hanging - the countdown resets while ssh is producing output, so password or 2FA prompts won’t trip it, and the tunnel’s buffer holds ssh’s diagnostics if you need to investigate.

CIDER can also use ssh merely to discover which nREPL ports are listening on a remote host when you run cider-connect-\* - set cider-infer-remote-nrepl-ports to t (default nil).

The second way is to run cider-jack-in-* while editing remote files over TRAMP (e.g. a file opened as /ssh:my-box:/path/to/project). CIDER then starts the build tool and the nREPL server on the remote host, reuses the TRAMP connection’s parameters (host, port, user) to establish the SSH tunnel, and connects back through it. The same tunnel reuse happens when you cider-connect-\* to a host you’re already editing over TRAMP.

Enabling the SSH options above, or jacking in over TRAMP, makes CIDER lean on TRAMP for some SSH operations, which parses config files such as ~/.ssh/config and ~/.ssh/known_hosts. Complex or nonstandard ssh configs can trip this up - the tips below help.

SSH config tips

Remote work goes a lot more smoothly when you let your ~/.ssh/config do the heavy lifting; CIDER, TRAMP and plain ssh then all share one definition of how to reach a host:

  • Host aliases keep the cider-connect prompts short.

  • Connection sharing (ControlMaster auto, ControlPath, ControlPersist) reuses a single SSH connection for subsequent operations, which dramatically cuts the per-request latency you’d otherwise pay over TRAMP.

  • Bastion / jump hosts are handled by ProxyJump.

If you go through a bastion host, beware that TRAMP injects its own connection-sharing options, which can conflict with a ProxyJump (or ProxyCommand) in your ssh config. Set tramp-use-connection-share to nil (or suppress) so TRAMP defers to your ~/.ssh/config.

Known TRAMP caveats

A few things still need attention when jacking in over TRAMP:

  • Executable resolution is best-effort on remote hosts. CIDER cannot reliably check whether a given command exists on the other side, so a missing executable typically only surfaces when the server fails to start. When in doubt, set cider-jack-in-cmd (see Overriding the Jack-In Command) to the exact command line you want to run.

  • Endpoint discovery for cider-connect (cider-locate-running-nrepl-ports) runs on the host given by the buffer’s default-directory, so from a TRAMP buffer CIDER queries the remote host. Port files (.nrepl-port and friends) are read over TRAMP and work without any extra tooling; the process scan additionally needs ps and lsof installed on the remote (in a container that usually means apt-get install procps lsof or the image’s equivalent). Without them you still get the port-file suggestions, just not the scan’s. The scan result is cached per default-directory for cider-running-nrepl-paths-cache-ttl seconds (5 by default), so back-to-back completions don’t re-spawn the subprocesses; call cider-clear-running-nrepl-paths-cache to discard it on demand.

  • Server-printed bind addresses. Some servers (e.g. babashka) print the bound address as localhost, 127.0.0.1, or 0.0.0.0. CIDER treats those as wildcard/loopback and falls back to the TRAMP host so the resulting connection lands on the right machine; an explicit non-loopback host in the server’s output is honored as-is.

  • Quoting on Windows is local-OS-aware. PowerShell-specific encoding is selected from your local cider-clojure-cli-command, not the remote host’s shell, so cross-OS jack-ins (e.g. Linux Emacs → Windows host) may need an explicit cider-jack-in-cmd.

For Docker containers (e.g. a buffer opened via Emacs 29+'s built-in /docker:container:/path TRAMP method) the server-starting half of cider-jack-in-* generally works: project detection, executable lookup and the build tool all run inside the container. The tricky part is the *connect-back* step - the container’s name is not a host your Emacs can reach, and CIDER’s SSH tunneling doesn’t apply to container methods. CIDER resolves this automatically when the container publishes the nREPL port: it asks docker port (or podman port) which local port the server is reachable on and connects there. Since a jack-in’s nREPL port is random by default, this only helps if you fix the port and publish it; for anything else, the approach described in the next section - start the server in the container yourself, publish the port, and cider-connect to it - remains the recommended one.

The same published-port resolution applies to cider-connect’s port suggestions: from a `/docker:…​ buffer, ports discovered inside the container are offered as the local ports they’re published on, and unpublished (unreachable) ones are dropped.

Working with Containers (Docker or others)

By 'containers' we mean Docker containers, or similar technologies, for running the JVM that will host our nREPL server. The files which we edit may / may not be edited using TRAMP. They could as well be mounted inside the container, so they appear as local.

Because CIDER can’t always detect if it’s dealing with remote files, it’s advisable to not rely on cider-jack-in and its remote support described above, but to start the nREPL server via command line from inside the container, and cider-connect to it.

This requires to first get a shell inside the running container and then start a nREPL server manually, or configure the container to start an nREPL automatically when the container starts.

In order to connect Emacs to nREPL, we need to make sure that the port of nREPL is reachable to our local Emacs, so that we can cider-connect to it. There are several solutions for this depending on the concrete scenario.

Working with Containers running on localhost

The nREPL port should be set to a fixed value as we need to give this during the docker start command in order to forward the port from container to host. This requires as well that nREPL server listens to "0.0.0.0" and not only to "localhost".

Publish the port under the same number on both sides (e.g. -p 7888:7888). A mismatched mapping like -p 12345:7888 technically works, but it blinds CIDER’s port suggestions: a .nrepl-port file in a mounted project directory records the container’s port, which on the host side is either wrong or (correctly) discarded as dead, so cider-connect can’t suggest anything and you have to type the forwarded port yourself.

If your source directories are mounted into the container, also set cider-path-translations so that stacktraces and jump-to-definition resolve container paths (like /app/src/…​) back to the files on your machine - see Translate File Paths. A .dir-locals.el in the project root is the natural place for it:

((nil . ((cider-path-translations . (("/app" . "/Users/me/projects/foo"))))))

Working with Containers running on remote hosts

In case we have the container running on a remote machine, we need to do the same setup as above and additionally use ssh to forward the already-forwarded port again to our local machine.

This can be done using a command such as "ssh -L 12345:localhost:12345 remote-server", assuming that 12345 was the nREPL port exposed by the container.

Working with devcontainers locally or remotely

Development Containers is a standard to describe container-based development environments. It includes a CLI. It uses Docker/Podman behind the scenes. So the principles of making sure that the nREPL port becomes available stay the same, but there are slightly different ways to configure this (given by the devcontainer standard).

There are several CLI tools to manage devcontainers, as there are several container technologies. The following example uses the devcontainers cli, but there are others (devpod, gitpod…​).

Example: Working with containers (using devcontainer) on a remote server

In this scenario and assuming a folder /home/me/my-clj-code containing the relevant devcontainer related config files (devcontainer.json) we can first start remotely a devcontainer via:

# executed on MY_REMOTE_SERVER
devcontainer up --workspace-folder /home/me/my-clj-code

Then we can start a nREPL server inside the container on the remote host as shown below (executed from your local machine). The command tunnels as well the remote port 12345 to local machine on port 12345:

ssh -t -L 12345:localhost:12345 MY_REMOTE_SERVER \
    devcontainer exec  --workspace-folder /home/me/my-clj-code \
    "clojure -Sdeps '{:deps {nrepl/nrepl {:mvn/version \"1.7.0\"} cider/cider-nrepl {:mvn/version \"0.59.0\"}}}' -m nrepl.cmdline -p 12345 -b 0.0.0.0 --middleware '[\"cider.nrepl/cider-middleware\"]' "

For this to work, we need as well to configure devcontainer.json with a snippet that exposes port 12345 from the container to the (remote) host:

"appPort": [
  // will make container port 12345 available as 12345 on remote host
  // (which will be further tunneled to 12345 on local machine)
  "12345:12345"
	],

This results then in having port 12345 available locally and we can cider-connect to it, using localhost:12345. Editing of the files can then happen via TRAMP. As the files are "on the remote machine" and also mounted inside the container on the remote machine, we have two possible TRAMP file syntaxes to edit them - either of:

  • /ssh:MY_REMOTE_SERVER:/home/me/my-clj-code/…​

  • /ssh:MY_REMOTE_SERVER|docker:DOCKER_CONTAINER_ID:/workspaces/my-clj-code/…​

Connecting via unix domain file socket

Unix socket support was introduced in nREPL 0.9. Currently CIDER’s support for Unix sockets is considered experimental and its interface might change in future CIDER releases.

When locally running nREPL servers, there is the option to listen on a socket file instead of opening a network port. As long as access to the parent directory of the socket is sufficiently protected, this is much more secure than the network port, since any local user can access the port-provided REPL. It can also be helpful in other cases, e.g. when working with virtual networks (containers) where sharing a file socket can be vastly simpler than managing bridge networks and firewall setups.

After having started an nREPL server on a file socket, e.g. with the clj command (see https://nrepl.org/nrepl/usage/server.html for other examples),

$ clj -R:nREPL -m nrepl.cmdline --socket nrepl.sock

you can then connect CIDER by using the local-unix-domain-socket special hostname with cider-connect: M-x cider-connect RET local-unix-domain-socket RET nrepl.sock RET.

Sharing a socket file with a container via a volume mount only works where the container runs directly on your kernel (i.e. Linux). Docker Desktop on macOS and Windows runs containers inside a VM, and unix domain sockets can’t cross that boundary - use a forwarded TCP port there instead.

At the moment only with leiningen, commands like cider-jack-in will detect and use the unix domain socket if one is requested via the :socket argument. This can be arranged by specifying a prefix argument to cider-jack-in, e.g. C-u M-x cider-jack-in, or by adjusting cider-lein-parameters.