How to Use SSH Port Forwarding for Remote Development

A practical guide to SSH local, remote, and dynamic port forwarding for developers working with remote servers, GPU machines, and AI coding workflows.

If you're doing remote development work—whether you're training models on a GPU server, running a dev environment on a cloud VM, or accessing a database through a bastion host—SSH port forwarding is one of those tools that feels like magic once you understand it. Instead of exposing services to the internet or wrestling with VPNs, you can securely tunnel traffic through SSH connections you already have.

This guide covers the three types of SSH port forwarding with practical examples you'll actually use. By the end, you'll know exactly which flags to use and when.

What Is SSH Port Forwarding and Why Should You Care?

Port forwarding (also called SSH tunneling) lets you route network traffic through an SSH connection. Think of it as creating a secure pipe between your local machine and a remote server, allowing you to access services that aren't directly exposed to the network.

Here are the most common scenarios where developers need port forwarding:

  • Accessing remote Jupyter notebooks or dev servers: You're training a model on a GPU machine and want to view the Jupyter notebook running on port 8888, but that port isn't exposed to the internet.
  • Connecting to databases through a jump host: Your production database is only accessible from within a private network, but you can SSH into a bastion host.
  • Testing webhooks locally: You need to expose your local development server on port 3000 to a remote machine that will send webhook requests to it.
  • Running AI coding agents on remote machines: You're using Claude or GPT-5.2 to write code on a powerful remote server, but want to test the web app locally in your browser.

SSH port forwarding handles all of these cases without requiring firewall changes or public IP exposure.

Local Port Forwarding: Accessing Remote Services Locally

Local port forwarding is the most common type. It forwards a port on your local machine to a port on the remote server (or to another machine accessible from that server).

The Syntax

ssh -L [local_port]:[destination_host]:[destination_port] [user]@[ssh_server]

The -L flag creates a local forward. When you connect to localhost:[local_port] on your machine, the traffic gets tunneled through the SSH connection to [destination_host]:[destination_port] as seen from the remote server.

Example 1: Accessing a Remote Jupyter Notebook

You've started a Jupyter notebook on your GPU server at gpu.example.com, and it's running on port 8888. You want to access it from your local browser.

ssh -L 8888:localhost:8888 user@gpu.example.com

Now open http://localhost:8888 in your browser. Your local port 8888 forwards to port 8888 on the remote machine. The traffic flows through the SSH tunnel, so it's encrypted and doesn't need to be exposed to the internet.

If port 8888 is already in use locally, you can map it to a different local port:

ssh -L 9999:localhost:8888 user@gpu.example.com

Then access http://localhost:9999 instead.

Example 2: Connecting to a Database Through a Bastion Host

Your PostgreSQL database runs on db.internal.example.com:5432, which isn't publicly accessible. But you can SSH into bastion.example.com, which can reach the database.

ssh -L 5432:db.internal.example.com:5432 user@bastion.example.com

Now you can connect to the database using localhost:5432 in your database client. From the SSH server's perspective, the connection goes to db.internal.example.com:5432.

Example 3: Accessing Multiple Services at Once

You can forward multiple ports in a single SSH session by using multiple -L flags:

ssh -L 8888:localhost:8888 -L 6006:localhost:6006 -L 3000:localhost:3000 user@gpu.example.com

This forwards three ports simultaneously—perhaps Jupyter on 8888, TensorBoard on 6006, and a Next.js dev server on 3000.

Remote Port Forwarding: Exposing Local Services to Remote Machines

Remote port forwarding works in the opposite direction. It forwards a port on the remote server to a port on your local machine (or another machine accessible from your local network).

The Syntax

ssh -R [remote_port]:[destination_host]:[destination_port] [user]@[ssh_server]

The -R flag creates a remote forward. When something connects to [remote_port] on the remote server, that traffic gets tunneled back through the SSH connection to [destination_host]:[destination_port] as seen from your local machine.

Example 1: Testing Webhooks Locally

You're developing a webhook handler locally on port 3000, and you need a remote service to send requests to it. The remote service runs on api.example.com.

ssh -R 3000:localhost:3000 user@api.example.com

Now when code on api.example.com connects to localhost:3000, it actually reaches your local machine's port 3000. You can test your webhook handler without deploying it.

Example 2: Letting a Remote AI Agent Access Your Local Dev Server

You're running an AI coding workflow on a powerful remote machine, and you've asked GPT-5.2 or Gemini 3 to build a web app. The agent runs the dev server on your local machine, and now you want to test it from the remote browser.

ssh -R 8080:localhost:5173 user@remote.example.com

If your Vite dev server runs locally on port 5173, you can now access it from the remote machine at localhost:8080.

Important Note on Remote Forwarding

By default, remote forwards only bind to localhost on the remote server. If you need other machines to connect to the forwarded port, you'll need to modify /etc/ssh/sshd_config on the remote server to set GatewayPorts yes or GatewayPorts clientspecified, then restart the SSH daemon. Without this, only processes on the remote machine itself can use the forwarded port.

Dynamic Port Forwarding: A SOCKS Proxy for Everything

Dynamic port forwarding turns your SSH connection into a SOCKS proxy. Instead of forwarding specific ports, you configure your applications to send all their traffic through the proxy, and the remote server forwards each connection as needed.

The Syntax

ssh -D [local_port] [user]@[ssh_server]

The -D flag creates a SOCKS proxy on your local machine at [local_port].

Example: Browsing as If You're on the Remote Network

You need to access multiple internal services on a private network, and you don't want to set up individual port forwards for each one.

ssh -D 1080 user@bastion.example.com

Now configure your browser or system to use localhost:1080 as a SOCKS5 proxy. All traffic from that browser will be routed through the SSH connection, so you can access any service that's reachable from bastion.example.com.

In Firefox, go to Settings > Network Settings > Manual proxy configuration > SOCKS Host: localhost, Port: 1080, select SOCKS v5.

You can also configure specific CLI tools to use the proxy:

curl --proxy socks5://localhost:1080 http://internal.example.com

Dynamic forwarding is particularly useful when you're working with microservices or internal tools spread across many different ports and hosts.

Making Port Forwarding Persistent with SSH Config

Typing out these commands every time gets old quickly. You can add port forwarding rules directly to your ~/.ssh/config file to make them persistent.

Example SSH Config with Local Forwarding

Host gpu-dev
    HostName gpu.example.com
    User your-username
    LocalForward 8888 localhost:8888
    LocalForward 6006 localhost:6006
    LocalForward 3000 localhost:3000

Now you can just run:

ssh gpu-dev

And all three port forwards are automatically established.

Example SSH Config with Remote Forwarding

Host api-server
    HostName api.example.com
    User your-username
    RemoteForward 3000 localhost:3000

Example SSH Config with Dynamic Forwarding

Host bastion
    HostName bastion.example.com
    User your-username
    DynamicForward 1080

Keeping SSH Connections Alive

Port forwarding sessions can drop if the connection is idle. Add these options to keep connections alive:

Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3

This sends a keepalive packet every 60 seconds. If three consecutive packets fail, the connection closes.

Common Patterns and Pro Tips

Chaining Port Forwards Through Multiple Jumps

If you need to tunnel through multiple servers, use ProxyJump:

Host internal-db
    HostName db.internal.example.com
    User your-username
    ProxyJump bastion.example.com
    LocalForward 5432 localhost:5432

This automatically creates an SSH connection through the bastion host to reach the internal database.

Binding to Specific Interfaces

By default, local forwards bind to localhost. To bind to all interfaces (allowing other machines on your network to use the forward), specify * or 0.0.0.0:

ssh -L 0.0.0.0:8888:localhost:8888 user@remote.example.com

Be cautious with this—it exposes the forwarded port on your local network.

Running SSH in the Background

If you only need the tunnel and don't want an interactive shell:

ssh -f -N -L 8888:localhost:8888 user@remote.example.com

The -f flag backgrounds the process, and -N tells SSH not to execute any commands (useful for port forwarding only).

Checking Active Tunnels

While connected, you can press ~# (tilde followed by hash) in an SSH session to see active tunnels. This is especially helpful when you're forwarding multiple ports and need to verify they're all working.

Port Forwarding in Agents UI

If you're using Agents UI for your remote development workflows, you don't need to remember these SSH flags. Agents UI has a built-in port forwarding interface that lets you set up local and remote forwards visually, manage multiple tunnels across different SSH sessions, and see which ports are currently active.

This is particularly useful when you're working with AI coding agents like Claude or GPT-5.2 across multiple remote machines—you can quickly forward ports to test web applications, access remote notebooks, or expose local services without dropping to the command line.

Conclusion

SSH port forwarding is a fundamental skill for remote development. Whether you're accessing a Jupyter notebook on a GPU server, connecting to a database through a bastion host, or testing webhooks locally, knowing when to use -L, -R, or -D will save you time and avoid the complexity of VPNs or public exposure.

Start with local forwarding for most use cases, use remote forwarding when you need to expose local services to remote machines, and reach for dynamic forwarding when you need flexible access to an entire remote network. Once you've found your common patterns, add them to your SSH config file to make them permanent.

With these techniques, you can build secure, efficient remote development workflows that keep your services private while giving you the access you need.

Try Agents UI

A native terminal for AI coding agents with persistent sessions, SSH workflows, and built-in editing.