Intranet penetration to build overleaf server
Introduction
In the previous article "Building a Fast and Secure VNC Service from Scratch", we achieved a connection from a non-public network machine to a public network machine using DDNS and set up a VNC service on the public machine. In the mid-series article "Learn Intranet Penetration the Hard Way", we used the DDNS-enabled public machine as a jump server to connect to another non-public network machine, thus realizing the dream of working from home. The goal of this follow-up article is to build a web service (specifically Overleaf) on that other non-public network machine based on the mid-series article, allowing local access and ultimately saving the annual $89 subscription fee.
Prerequisites
- A jump server that can connect to the public network (in my case, it's still the development board supporting RVV1.0 from the mid-series article)
- A higher-performance Linux server for installing the community edition of Overleaf
- Ensure that the host, server, and jump server can SSH connect to each other
- Ensure the jump server has a corresponding domain name
Steps
First, Ensure the Overleaf Server Runs Properly
You can refer to this blog post or the official documentation, but here are the steps for convenience:
git clone https://github.com/overleaf/toolkit.git ./overleaf-toolkit
cd ./overleaf-toolkit
sudo bin/init
Modify ./config/overleaf.rc
:
OVERLEAF_LISTEN_IP=0.0.0.0 # Listen on all IPs; by default, it's only accessible locally
OVERLEAF_PORT=9999 # The default port is 80, but it might be occupied
Then start the service:
sudo bin/up -d
sudo bin/down # To shut down
If the docker-compose proxy is not working well, you can change the mirror source:
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://docker.imgdb.de",
"https://docker-0.unsee.tech",
"https://docker.hlmirror.com",
"https://docker.lms.run",
"https://func.ink",
"https://lispy.org",
"https://docker.xiaogenban1993.com"
]
}
EOF
Test whether the page displays correctly by curling http://localhost:9999, for example:
> unset http_proxy && curl http://localhost:9999/
Found. Redirecting to /login%
If there are no issues, it confirms the server itself is functioning properly. Proceed to the next step.
Server First Performs Intranet Penetration to the Jump Server
You can refer to this document.
Download the same version of frpc
and frps
(the author uses 0.61.0
). Modify frpc.toml
on the client (i.e., the Overleaf server):
serverAddr = <frps server domain name or IP>
serverPort = 7000
transport.protocol = "quic"
[[proxies]]
name = "web"
type = "http"
localPort = 9999 # Corresponds to your Overleaf server's local port
customDomains = <frps server domain name>
Then, modify frps.toml
on the frps server (i.e., the jump server):
bindPort = 7000
quicBindPort = 7000
vhostHTTPPort = <the mapped port in jump server>
After that, start the client's frpc
:
./frpc -c frpc.toml
And the server's frps
:
./frps -c frps.toml
Assuming your server's domain name is foo.example.com
and the jump server maps the HTTP port to 8080
, test with curl on the jump server:
> unset http_proxy && curl http://foo.example.com:8080
Found. Redirecting to /login
If successful, it means frp is working correctly. If you only need to access it in an HTTP environment and the domain is registered, you can already use http://foo.example.com:8080
to access it. Otherwise, proceed to the next step.
It is recommended to add frps and frpc to systemctl for easy startup on boot, for example:
### /etc/systemd/system/frps.service
[Unit]
Description=frps
After=network.target syslog.target
Wants=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/frps -c /usr/local/bin/frps.toml
Restart=always
[Install]
WantedBy=multi-user.target
Obtain Let's Encrypt Certificate for Domain (If Not Available)
Let's Encrypt uses Certbot for domain ownership validation, with certificates valid for 90 days. However, the default parameters allow automatic renewal. When Nginx is properly configured, only a single command is needed, and you can select the first option during the process.
> sudo certbot certonly --preferred-challenges dns -d <frps server domain name>
How would you like to authenticate with the ACME CA?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: Nginx Web Server plugin (nginx)
2: Runs an HTTP server locally which serves the necessary validation files under
the /.well-known/acme-challenge/ request path. Suitable if there is no HTTP
server already running. HTTP challenge only (wildcards not supported).
(standalone)
3: Saves the necessary validation files to a .well-known/acme-challenge/
directory within the nominated webroot path. A seperate HTTP server must be
running and serving files from the webroot path. HTTP challenge only (wildcards
not supported). (webroot)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-3] then [enter] (press 'c' to cancel): 1
Alternatively, if you consider this a recursive issue, you can opt for manual acquisition and switch to automatic renewal after configuring Nginx. Manual acquisition requires editing two TXT records with your DNS provider; the detailed process will not be elaborated here.
sudo certbot certonly --manual --preferred-challenges dns -d <frps server domain name>
After successful validation, Certbot stores the certificate at /etc/letsencrypt/live/<frps server domain name>/fullchain.pem
and the private key at /etc/letsencrypt/live/<frps server domain name>/privkey.pem
.
HTTP to HTTPS
Besides the method described in the documentation, which involves "using the https2http plugin to expose a local HTTP service via HTTPS," you can also achieve this using nginx.
After installing nginx, edit /etc/nginx/sites-available/default
:
server {
listen <https port> ssl;
server_name <frps server domain name>;
ssl_certificate <your cert>;
ssl_certificate_key <your cert key>;
client_max_body_size 50M;
location / {
proxy_pass http://<frps server domain name>:<the mapped port in jump server>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Here, <https port>
is typically 443
(under normal conditions, i.e., if the domain is registered), <frps server domain name>
is your jump server's domain name, <your cert>
and <your cert key>
are the certificate and private key previously obtained via Let's Encrypt, and <the mapped port in jump server>
refers to the HTTP protocol service port previously mapped by frp on the jump server.
After filling in the parameters:
sudo systemctl daemon-reload
sudo systemctl restart nginx
sudo systemctl enable nginx
Assuming your server's domain name is foo.example.com
and the mapped HTTPS port on the jump server is 443
, test it with curl on the jump server:
> unset http_proxy && curl https://foo.example.com
Found. Redirecting to /login
This indicates that the nginx service is functioning properly. If your jump server's domain name is already registered, you should be able to access the URL and open the page normally from your local machine.
Bypassing Unregistered Restrictions
In short, avoid using common ports for HTTPS, which might help bypass the restrictions, but stability is not guaranteed.
When accessing locally, simply add the port you set after the domain name, and it should work.
User Configuration
Access https://foo.example.com/launchpad to set up an administrator account and add user accounts (if no email server is configured, the administrator can add users in the backend. The system will generate a registration success link for password setup—simply share this link with the registered users).
Then, visit https://foo.example.com/ to log in with your newly registered account and start using the platform. That's all—congratulations! 🎉
Updated on Mar 18th, 2025
frp https2http Plugin
Using the frp https2http plugin allows you to convert Overleaf's HTTP connection to HTTPS locally, eliminating the need for configuring Nginx on a jump server and simultaneously resolving the WebSocket connection timeout issue mentioned in the FAQ.
- Client
serverAddr = <frps server domain name>
serverPort = 7001
transport.protocol = "quic" # optional
auth.method = "token" # optional
auth.token = <your secret token> # optional
[[proxies]]
name = "test_htts2http" # any name is ok
type = "https"
customDomains = <frps server domain name>
[proxies.plugin]
type = "https2http"
# port correspond to overleaf config's local port
localAddr = "127.0.0.1:9999"
# HTTPS certificate-related configurations
# use <frps server domain name> registered from certbot
crtPath = "./server.crt"
keyPath = "./server.key"
hostHeaderRewrite = "127.0.0.1"
requestHeaders.set.x-from-where = "frp"
- Server
bindPort = 7001
quicBindPort = 7001 # optional
vhostHTTPSPort = <https port>
auth.method = "token" # optional
auth.token = <your secret token> # optional
You can even add an additional layer of Netlify's CDN reverse proxy to make this <https port>
the default 443.
Certbot Manual Hook
To manually simulate a DNS subscription/renewal for the domain example.com
using the foo.sh
script, execute the following command:
sudo certbot certonly \
--manual \
--preferred-challenges dns \
--manual-auth-hook /path/to/foo.sh \
--dry-run \
-d example.com
Certbot will pass two parameters to foo.sh
:
$CERTBOT_DOMAIN
: The domain you are renewing.$CERTBOT_VALIDATION
: The validation string.
The script should insert a TXT record in your DNS provider with the name _acme-challenge. + $CERTBOT_DOMAIN
and the value $CERTBOT_VALIDATION
. If you use Netlify as your DNS provider, I have implemented an automated solution in dns-auth.sh
and renew.py
at https://github.com/junyu33/netlify-dynamic-dns-py. Simply replace /path/to/foo.sh
with the absolute path to dns-auth.sh
, such as /home/junyu33/netlify-dynamic-dns-py/dns-auth.sh
.
If the operation is successful, remove the --dry-run
parameter to perform an actual automatic renewal. If the renewal succeeds, Certbot will set up a scheduled task to call this script again when the certificate is about to expire, so ensure your hook script remains at the specified path.
FAQ
What to do if the WebSocket test times out in the Launchpad interface?
Ignore it (it does not seem to affect usage currently), or use the frp https2http plugin.
Why does it show "connection error" when entering a project?
The reason is a WebSocket connection timeout. The solution is the same as above.
Why can I only upload files up to 1M in size?
See this issue. The cause is an issue with the nginx configuration, which could be either inside the Docker container or on the jump server (though the latter has been ruled out since client_max_body_size 50M
has been added there).
Is it possible to avoid using HTTPS?
At least on the latest version of Microsoft Edge, it will directly show "connection not secure" and prevent proceeding. Other scenarios have not been tested.
Can I use a self-signed certificate for HTTPS? (e.g., generated using OpenSSL)
Same as above.
References
https://github.com/overleaf/toolkit/blob/master/doc/quick-start-guide.md
https://ziuch.com/article/self-hosted-overleaf#105da52b1a26809bbcb5d8294bc18757
https://jinli.io/p/自建在线latex编译预览服务overleaf开源社区版/
https://gofrp.org/zh-cn/docs/examples/vhost-http/
https://gofrp.org/zh-cn/docs/examples/https2http/
https://github.com/overleaf/docker-image/issues/20
https://github.com/overleaf/overleaf/wiki/HTTPS-reverse-proxy-using-Nginx