Skip to content

Commit a82571b

Browse files
committed
add rathole tunnel article, in progress
1 parent 049457e commit a82571b

File tree

5 files changed

+364
-0
lines changed

5 files changed

+364
-0
lines changed

src/constants/collections.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export const CATEGORIES = [
5858
name: 'resources',
5959
icon: 'mdi:book-open-variant-outline',
6060
},
61+
{
62+
name: 'homelab',
63+
icon: 'mdi:flask-empty-outline',
64+
},
6165
] as const;
6266

6367
// use imported images here
31.8 KB
Loading
33.2 KB
Loading
1.1 MB
Loading
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
---
2+
title: Expose home server with Rathole tunnel and Traefik
3+
description: |
4+
A practical example how to permanently expose your home server to the internet.
5+
publishDate: 2025-04-29
6+
heroImage: '../../../../content/post/2025/04-29-rathole-traefik-home-server/_images/rathole-traefik-architecture-16-9.png'
7+
heroAlt: Rathole Traefik architecture diagram
8+
tags:
9+
- devops
10+
- docker
11+
- self-hosting
12+
category: homelab
13+
toc: true
14+
draft: false
15+
---
16+
17+
import { Image } from 'astro:assets';
18+
19+
import { IMAGE_SIZES } from '../../../../constants/image';
20+
21+
import FirewallImage from '../../../../content/post/2025/04-29-rathole-traefik-home-server/_images/firewall.png';
22+
import HomeServerContainersImage from '../../../../content/post/2025/04-29-rathole-traefik-home-server/_images/home-server-containers.png';
23+
24+
25+
## Introduction
26+
27+
In the previous article I wrote about temporary SSH tunneling technique to bypass CG-NAT. This method is not suitable for exposing permanent services, at least not without `autossh` manager. Proper tools for this are [rapiz1/rathole](https://github.com/rapiz1/rathole) or [fatedier/frp](https://github.com/fatedier/frp). I chose Rathole since it's written in Rust and offers better performance and benchmarks.
28+
29+
## Prerequisites
30+
31+
- A VPS server with a public IP and Docker, ideally small, you can't use ports `80` and `443` for any other services aside Rathole
32+
- A home server
33+
- A domain name
34+
35+
## Architecture overview
36+
37+
We will use Rathole for encrypted tunnel between VPS and local network. We will also use Traefik since we want to host multiple websites on our home server, like you would on any server.
38+
39+
The main question is where to run Traefik:
40+
41+
1. On the VPS
42+
2. On the home server
43+
44+
I highly prefer the option 2 because that way the entire configuration is stored in our home server. The home server is almost tunnel agnostic, and you can reuse it on any tunneled or non-tunneled server. Otherwise we would need to maintain state between the VPS and the home server, debug both together, etc.
45+
46+
Another point is that with option 2, we avoid the gap of unencrypted traffic on the VPS between Traefik (TLS) and Rathole (Noise Protocol). You can read more about the comparison of these two options in this article: https://blog.mni.li/posts/caddy-rathole-zero-knowledge/.
47+
48+
The downside is that Rathole will exclusively occupy ports `80` and `443` on the VPS, preventing any other process from using them. We won't be able to run other web servers on that VPS, so it's best to use a small one dedicated to this purpose.
49+
50+
image
51+
52+
## Rathole server
53+
54+
We will run Rathole server inside a Docker container on our VPS. Rathole uses the same binary for both server and client, you just pass the right option `<--server|--client>` and the `.toml` configuration file.
55+
56+
Here is the Rathole server configuration [rathole.server.toml](https://github.com/nemanjam/rathole-server/blob/5226ff53992abe930302098677a570151ebff927/rathole.server.toml):
57+
58+
```toml title="rathole.server.toml"
59+
[server]
60+
bind_addr = "0.0.0.0:2333"
61+
62+
[server.transport]
63+
type = "noise"
64+
65+
[server.transport.noise]
66+
local_private_key = "private_key"
67+
68+
[server.services.traefik-http]
69+
token = "secret_token_1"
70+
bind_addr = "0.0.0.0:5080"
71+
72+
[server.services.traefik-https]
73+
token = "secret_token_1"
74+
bind_addr = "0.0.0.0:5443"
75+
```
76+
77+
Let's explain it: we choose port `2333` for the control channel and we bind it to all interfaces inside the Docker container with `0.0.0.0` IP. We choose `noise` encryption protocol and specify a private key. Public key will be used on the Rathole client. Public and private keys pair is generated with:
78+
79+
```bash
80+
docker run -it --rm rapiz1/rathole --genkey
81+
```
82+
83+
Then we define two tunnels, one for HTTP and another for HTTPS. For HTTP tunnel we define the name `server.services.traefik-http`, set the value for `token`, and choose the port `5080` and again we bind to all container interfaces with `0.0.0.0`. Similarly for HTTPS we set the name to `server.services.traefik-https`, provide a `token` value and choose port `5443`.
84+
85+
Important note is that beside a different name, just a different token value is sufficient to create another tunnel (Rathole service). This practically means we can use a single Rathole server container to expose multiple home servers (Rathole clients) on the same ports `5080` and `5443`, which is pretty convenient.
86+
87+
Token is just a random base64 string, we generate it by running this:
88+
89+
```bash
90+
openssl rand -base64 32
91+
```
92+
93+
After configuration file we define a Rathole server container with [docker-compose.yml](https://github.com/nemanjam/rathole-server/blob/5226ff53992abe930302098677a570151ebff927/docker-compose.yml):
94+
95+
96+
```yml title="docker-compose.yml"
97+
services:
98+
99+
rathole:
100+
image: rapiz1/rathole:v0.5.0
101+
container_name: rathole
102+
command: --server /config/rathole.server.toml
103+
restart: unless-stopped
104+
ports:
105+
# host:container
106+
- 2333:2333
107+
- 80:5080
108+
- 443:5443
109+
volumes:
110+
- ./rathole.server.toml:/config/rathole.server.toml:ro
111+
```
112+
In the command we set the `--server` option and pass the `.toml` configuration file and mount it as a read-only bind-mount volume.
113+
114+
Important part are the port mappings, here you can see that Rathole server container will occupy ports `2333`, `80`, and `443` exclusively on the host VPS. This practically means we wont be able to run any other webservers on ports `80` and `443`. We will also need to open ports `80`, `443` and `2333` in the VPS firewall. You don't need to open ports `5080` and `5443`, those are used only by Rathole internally.
115+
116+
## Rathole client and connecting with Traefik
117+
118+
We run Rathole client and Trafik inside the Docker containers on the home server. Configuring Rathole client and connecting it to Traefik is a bit more complex and tricky.
119+
120+
Here is the Rathole client configuration [core/rathole.client.toml.example](https://github.com/nemanjam/traefik-proxy/blob/e8fece09e31ec99ddd21559f343d0ddea9fb55bf/core/rathole.client.toml.example):
121+
122+
```toml title="core/rathole.client.toml.example"
123+
[client]
124+
remote_addr = "123.123.123.123:2333"
125+
126+
[client.transport]
127+
type = "noise"
128+
129+
[client.transport.noise]
130+
remote_public_key = "public_key"
131+
132+
# this is the important part
133+
# Rathole knows traffic comes from 5080 and 5443, control channel told him
134+
# DON'T do ANY mapping in docker-compose.yml
135+
# just pass traffic from Rathole on ports which Traefik expects (80 and 443)
136+
137+
[client.services.traefik-http]
138+
token = "secret_token_1"
139+
local_addr = "traefik:80"
140+
141+
[client.services.traefik-https]
142+
token = "secret_token_1"
143+
local_addr = "traefik:443"
144+
```
145+
146+
Let's go through it. First we define the VPS server IP `remote_addr` and the control channel port `2333`, set `noise` encryption protocol and a public key this time `remote_public_key`.
147+
148+
Now comes the important and tricky part, defining tunnels - services. We repeat the service name and token that we used in Rathole server config.
149+
150+
**And now the most important part:** `local_addr`, for this we target Traefik hostname - service name from `core/docker-compose.local.yml` and Traefik listening ports `80` and `443`. That's it. It might look simple and obvious, this is the correct setup. I must emphasize, don't fall into temptation to set any additional port mappings in `core/docker-compose.local.yml`, functionality will break, all should be done in `core/rathole.client.toml`.
151+
152+
Another note: You might wonder why ports `5080` and `5443` aren't repeated anywhere in the client config `core/rathole.client.toml`? The answer is "no need for it", we already specified the port `2333` of the control channel, which will communicate all additional required information between Rathole server and client.
153+
154+
Now that we have configured Rathole client, we need to define Rathole client and Traefik containers.
155+
Here is the Rathole client container and the important part of the Traefik container [core/docker-compose.local.yml](https://github.com/nemanjam/traefik-proxy/blob/e8fece09e31ec99ddd21559f343d0ddea9fb55bf/core/docker-compose.local.yml):
156+
157+
```yml title="core/docker-compose.local.yml"
158+
services:
159+
160+
rathole:
161+
# 1. default official x86 image
162+
image: rapiz1/rathole:v0.5.0
163+
164+
# 2. custom built ARM image (for Raspberry pi)
165+
# image: nemanjamitic/my-rathole-arm64:v1.0
166+
167+
# 3. build for arm - AVOID, use prebuilt ARM image above
168+
# build: https://github.com/rapiz1/rathole.git#main
169+
# platform: linux/arm64
170+
171+
container_name: rathole
172+
command: --client /config/rathole.client.toml
173+
restart: unless-stopped
174+
volumes:
175+
- ./rathole.client.toml:/config/rathole.client.toml:ro
176+
networks:
177+
- proxy
178+
179+
traefik:
180+
image: 'traefik:v2.9.8'
181+
container_name: traefik
182+
restart: unless-stopped
183+
184+
# for this to work both services must be defined in the same docker-compose.yml file
185+
depends_on:
186+
- rathole
187+
188+
# other config...
189+
190+
networks:
191+
- proxy
192+
193+
# leave this commented out, just for explanation
194+
# Rathole will pass Traffic through proxy network directly on 80 and 443
195+
# defined in rathole.client.toml
196+
# ports:
197+
# - '80:80'
198+
# - '443:443'
199+
200+
# other config...
201+
```
202+
203+
Let's start with Rathole service. Similarly line for server, we run the Rathole binary, this time in client mode with `--client` and we pass the client config file `/config/rathole.client.toml` which we also bind mount as volume. Important part, we set both Rathole and Traefik containers on the same **external** network `proxy` so they can communicate with each other and with the host.
204+
205+
Additional notes about the Rathole image:
206+
207+
- Always make sure to use the same Rathole image version for both server and client for compatibility.
208+
- `x86` - By default Rathole provides only the `x86` image, if your home server uses that architecture you are good.
209+
- `ARM` - If you have ARM home server (e.g. Raspberry Pi) you will have to build the image yourself or use a prebuilt, unofficial one. **Avoid** building images on low power ARM single board computers, it will take a long time and a lot of RAM and CPU power. Instead either pre-build one yourself and push to Dockerhub or you can reuse mine `nemanjamitic/my-rathole-arm64:v1.0` image (uses Rathole `v0.5.0`).
210+
211+
Now the Traefik container. It must be on the same `proxy` external network same as Rathole. Another important part: It must **wait** for Rathole container to boot up `depends_on: rathole` because the traffic will come from the Rathole tunnel. **Do not** expose ports `80` and `443`, Rathole already bound to those Traefik container ports, as we defined in the Rathole client config `core/rathole.client.toml`.
212+
213+
The rest of the Traefik container definition is left out here, because it's the usual configuration, unrelated to Rathole tunnel. Bellow is the quick reminder about general Traefik configuration.
214+
215+
**Traefik reminder**
216+
217+
218+
1. Provide the `.env` file with variables needed for Traefik:
219+
220+
```bash
221+
cp .env.example .env
222+
```
223+
224+
```bash title=".env"
225+
SITE_HOSTNAME=homeserver.my-domain.com
226+
227+
# important: must put value in quotes "..." and escape $ with \$
228+
TRAEFIK_AUTH=
229+
230+
# will receive expiration notifications
231+
232+
```
233+
2. On your home server host OS you must create an external Docker network:
234+
235+
```bash
236+
docker network create proxy
237+
```
238+
239+
3. Create `acme.json` file with permission `600`:
240+
241+
```bash
242+
touch ~/homelab/traefik-proxy/core/traefik-data/acme.json
243+
244+
sudo chmod 600 ~/homelab/traefik-proxy/core/traefik-data/acme.json
245+
```
246+
247+
4. Always start with the staging Acme server for testing and swap to production once satisfied:
248+
249+
```yml
250+
# core/traefik-data/traefik.yml
251+
252+
certificatesResolvers:
253+
letsencrypt:
254+
acme:
255+
# always start with staging certificate
256+
caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
257+
# caServer: 'https://acme-v02.api.letsencrypt.org/directory'
258+
```
259+
260+
5. To clear the temporary staging certificates, clear the contents of `acme.json`
261+
262+
```bash
263+
truncate -s 0 acme.json
264+
```
265+
266+
That's it. Once done, we can run Rathole client and Traefik containers on our home server with:
267+
268+
```bash
269+
docker compose -f docker-compose.local.yml up -d
270+
```
271+
272+
<Image {...IMAGE_SIZES.FIXED.MDX_XL} src={HomeServerContainersImage} alt="Running containers on the home server" />
273+
274+
## Exposing multiple servers
275+
276+
Fortunately Rathole made it trivial to run multiple tunnels using a single Rathole server. We don't need to open any additional ports or run multiple container instances. We just use the different tunnel name e.g. `server.services.traefik-http` and the value for the `token` for each tunnel - service, that's it.
277+
278+
**Rathole server:**
279+
280+
Rathole server configuration file example [rathole.server.toml](https://github.com/nemanjam/rathole-server/blob/5226ff53992abe930302098677a570151ebff927/rathole.server.toml):
281+
282+
```toml title="rathole.server.toml"
283+
[server]
284+
bind_addr = "0.0.0.0:2333"
285+
286+
[server.transport]
287+
type = "noise"
288+
289+
[server.transport.noise]
290+
local_private_key = "private_key"
291+
292+
# separated based on token, use the same ports
293+
294+
# home server 1 - local
295+
[server.services.traefik-http]
296+
token = "secret_token_1"
297+
bind_addr = "0.0.0.0:5080"
298+
299+
[server.services.traefik-https]
300+
token = "secret_token_1"
301+
bind_addr = "0.0.0.0:5443"
302+
303+
# home server 2 - pi
304+
[server.services.pi-traefik-http]
305+
token = "secret_token_2"
306+
bind_addr = "0.0.0.0:5080"
307+
308+
[server.services.pi-traefik-https]
309+
token = "secret_token_2"
310+
bind_addr = "0.0.0.0:5443"
311+
```
312+
313+
In the code above I use this Rathole server to connect a two Rathole client home servers `traefik-http` and `pi-traefik-http` for HTTP tunnels, and `traefik-https` and `pi-traefik-https` for HTTPS tunnels.
314+
315+
**Rathole client:**
316+
317+
On the each Rathole client you just specify which tunnels you are using. For example, on the "pi" home server you will use just its HTTP/HTTPS par and omit the other ones, [core/rathole.client.toml.example](https://github.com/nemanjam/traefik-proxy/blob/e8fece09e31ec99ddd21559f343d0ddea9fb55bf/core/rathole.client.toml.example):
318+
319+
```toml title="core/rathole.client.toml.example"
320+
[client]
321+
remote_addr = "123.123.123.123:2333"
322+
323+
[client.transport]
324+
type = "noise"
325+
326+
[client.transport.noise]
327+
remote_public_key = "public_key"
328+
329+
# pi
330+
[client.services.pi-traefik-http]
331+
token = "secret_token_2"
332+
local_addr = "traefik:80"
333+
334+
[client.services.pi-traefik-https]
335+
token = "secret_token_2"
336+
local_addr = "traefik:443"
337+
```
338+
339+
## Open the firewall on the VPS
340+
341+
Like for any webserver, on the VPS you will need to open ports `80` and `443` to listen for HTTP/HTTPS traffic. Additionally you will need to open the port `2333` for the Rathole control channel - tunnel.
342+
343+
<Image {...IMAGE_SIZES.FIXED.MDX_MD} src={FirewallImage} alt="Opened port for Rathole tunnel in the firewall" />
344+
345+
## Completed code
346+
347+
- **Rathole server:** https://github.dev/nemanjam/rathole-server
348+
- **Rathole client and local Traefik:** https://github.com/nemanjam/traefik-proxy/tree/main/core
349+
350+
## Conclusion
351+
352+
## References
353+
354+
- Rathole repository https://github.com/rapiz1/rathole
355+
- Local or remote Traefik discussion https://github.com/rapiz1/rathole/issues/169
356+
- Local and remote Traefik comparison, Tailscale benchmarks https://blog.mni.li/posts/caddy-rathole-zero-knowledge/
357+
- Rathole Docker example configuration https://nitinja.in/tech/
358+
- Rathole `.toml` environment variables discussion https://github.com/rapiz1/rathole/issues/218
359+
- frp repository https://github.com/fatedier/frp
360+

0 commit comments

Comments
 (0)