# How I think about SSH tunnels before copying the command

A beginner-friendly, step-by-step guide to local, remote, and dynamic SSH tunnels, with simple mental models before using the SSH Tunnel Builder.

- Date: 2026-09-27
- URL: https://ilham.dev/posts/how-to-use-ssh-tunnel-builder/
- Markdown: https://ilham.dev/posts/how-to-use-ssh-tunnel-builder/index.md
- Tags: ssh, devops, networking, tools
- Reading time: 6 min


SSH tunnels are powerful, but the flags are not friendly at first sight:

```sh
ssh -L 8080:localhost:80 user@example.com
```

The command is short, but the meaning is not obvious. Which machine is
`localhost`? Why are there two ports? What is the difference between `-L`, `-R`, and
`-D`?

The [SSH Tunnel Builder](/tools/ssh-tunnel-builder/) helps assemble the command, but
it is still useful to understand the idea in plain language before copying anything.

## The simple idea

An SSH tunnel is a safe pipe between your computer and another machine.

You already use SSH to open a terminal on a server:

```sh
ssh user@example.com
```

A tunnel uses that same SSH connection to carry other traffic too. Instead of only
sending terminal text, it can also forward a port.

In very simple terms:

> “When I connect to this port here, please send the traffic through SSH and open
> that port over there.”

That is all a tunnel is.

## The three tunnel types

There are three common SSH tunnel modes:

| Mode | Flag | Plain meaning |
| --- | --- | --- |
| Local tunnel | `-L` | Open a port on my computer that forwards to something reachable from the server. |
| Remote tunnel | `-R` | Open a port on the server that forwards back to something reachable from my computer. |
| Dynamic tunnel | `-D` | Open a local SOCKS proxy so apps can send traffic through the SSH server. |

Most people need `-L` first. Start there.

## Local tunnel: the one I use most often

A local tunnel is useful when a service is reachable from the server, but not from
your laptop.

Example situation:

- PostgreSQL runs on the server at `localhost:5432`;
- the database port is not open to the internet;
- I want to connect from my laptop with a database client.

The tunnel command is:

```sh
ssh -L 15432:localhost:5432 user@example.com
```

Read it slowly:

```text
-L 15432:localhost:5432
   │     │         │
   │     │         └─ target port, seen from the server
   │     └─────────── target host, seen from the server
   └───────────────── local port on my computer
```

So the meaning is:

> Open port `15432` on my laptop. When I connect to it, send the traffic through
> SSH to `example.com`, then from that server connect to `localhost:5432`.

After the tunnel is open, my database client connects to:

```text
localhost:15432
```

not directly to the server database port.

## Why use 15432 instead of 5432?

You can use `5432` locally if it is free. But many laptops already have PostgreSQL
running on `5432`, so I often choose `15432` to avoid conflict.

A common habit is to add `1` in front of the original port:

- remote `5432` becomes local `15432`;
- remote `3306` becomes local `13306`;
- remote `6379` becomes local `16379`.

This is not required. It just makes the mapping easier to remember.

## Local tunnel example for a private web app

Suppose an internal admin panel is reachable on the server at:

```text
localhost:8080
```

but it is not exposed publicly. You can create:

```sh
ssh -L 18080:localhost:8080 user@example.com
```

Then open this in your browser:

```text
http://localhost:18080
```

Your browser talks to your laptop. SSH carries the traffic to the server. The server
then talks to its own `localhost:8080`.

## Important detail: whose localhost?

This is the part that confuses many people.

In this command:

```sh
ssh -L 18080:localhost:8080 user@example.com
```

The first side, `18080`, is on **your computer**.

The `localhost:8080` after it is resolved from the **server's point of view**.

So `localhost` does not always mean your laptop. Inside the target part of a local
tunnel, it means localhost as seen by the SSH server.

## Remote tunnel: exposing something from my computer to a server

A remote tunnel is the reverse direction.

Example situation:

- I run a development app on my laptop at `localhost:3000`;
- I have a server at `example.com`;
- I want the server to forward one of its ports back to my laptop.

The command is:

```sh
ssh -R 9000:localhost:3000 user@example.com
```

Meaning:

> Open port `9000` on the server. When something connects to that server port,
> send the traffic back through SSH to my laptop's `localhost:3000`.

This can be useful for demos, callbacks, or temporary testing. Be careful: depending
on server SSH settings, remote forwarded ports may be reachable only from the server
itself, or from the network. Do not expose private development apps unless you mean
to.

## Dynamic tunnel: a SOCKS proxy through SSH

A dynamic tunnel is different. It does not forward one fixed target. It creates a
SOCKS proxy.

Command:

```sh
ssh -D 1080 user@example.com
```

Meaning:

> Open a SOCKS proxy on my laptop at port `1080`. Apps configured to use that proxy
> will send their traffic through the SSH server.

This is useful when you need to browse as if you are coming from the server's
network. It is not the same as a full VPN, but it can solve some testing problems.

## How to use the SSH Tunnel Builder

Open [SSH Tunnel Builder](/tools/ssh-tunnel-builder/) and choose the tunnel type.
Then fill the form slowly.

For a local tunnel, fill:

1. **SSH host**: the server you SSH into, for example `example.com`.
2. **SSH user**: the Linux user, for example `ubuntu`.
3. **Local port**: the port to open on your computer, for example `15432`.
4. **Target host**: the host as seen by the server, often `localhost`.
5. **Target port**: the real service port, for example `5432`.
6. Optional identity file: for example `~/.ssh/id_ed25519`.

The builder will assemble a command like:

```sh
ssh -L 15432:localhost:5432 ubuntu@example.com
```

Copy it, run it in a terminal, and keep that terminal open while you use the tunnel.
If you close the SSH session, the tunnel closes too.

## Common options I usually add

For longer-running tunnels, these options are useful:

```sh
ssh -N -L 15432:localhost:5432 ubuntu@example.com
```

`-N` means “do not open a remote shell”. It is good when I only want the tunnel.

For keepalive:

```sh
ssh -N \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -L 15432:localhost:5432 \
  ubuntu@example.com
```

This helps SSH notice broken connections instead of hanging forever.

## Troubleshooting checklist

If the tunnel does not work, I check these in order:

1. Can I SSH into the server without the tunnel?
2. Is the local port already used on my computer?
3. Is the target host correct from the server's point of view?
4. Is the target service actually running?
5. Is a firewall blocking the server from reaching the target?
6. Am I connecting my client to the local forwarded port, not the original remote port?

For local tunnels, this is a common mistake:

```text
Wrong: connect database client to example.com:5432
Right: connect database client to localhost:15432
```

The whole point is that your local port becomes the safe entrance.

## My rule of thumb

When I see `-L`, I read it as:

> “Make something remote feel local.”

When I see `-R`, I read it as:

> “Make something local available from the remote server.”

When I see `-D`, I read it as:

> “Use the SSH server as a proxy path.”

That mental model makes the command much less intimidating. The builder helps with
the exact flags; the mental model helps me choose the right tunnel.
