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.