There are many ways to expose HTTP services behind NAT so that they can be accessed from the internet. The technique commonly employed involves setting up a network tunnel using VPN and an HTTP reverse proxy. Cloudflare Tunnel is one example.
In this article, I want to share my experience and how I expose HTTP services on a local network to the internet using WireGuard VPN tunnel and Nginx as an HTTP reverse proxy. The HTTP service I will expose is Immich. For those who don’t know, Immich is a self-hosted photo and video management solution; an alternative to Google Photos.
I won’t discuss the details of how to install Immich because installing Immich using Docker is very easy to do. Instead, I’ll focus on the configuration of Nginx and VPN tunnel, as well as the topology used.
Prerequisites
Before we get started, there are a few conditions that need to be met:
- A domain name or subdomain that uses Cloudflare as its authoritative DNS server.
- A VPS with a public IP address (WireGuard and Nginx installed, which will later be used for reverse proxying the local network).
- A PC, VM, or LXC on the local network to run Immich, Nginx, Docker, and Certbot.
Topology
When writing this article, I used the following network topology:
To provide further context, let me break down the components of the above topology:
- The subdomain used for Immich is
i.arch.or.id
. - The public IP address of the VPS server is
154.26.xxx.xx
. - The VPS server’s WireGuard tunnel IP address is
10.88.88.51
. - The local area network (LAN) uses the
192.168.2.0/24
subnet. - Immich is installed on an LXC container located on the local network,
with an IP address of
192.168.2.105
. - The Immich LXC is connected to the VPS server and utilizes the IP tunnel
10.88.88.105
. - Nginx and Certbot are also installed on the Immich LXC.
The ultimate goal of this article is to demonstrate how I can access my photos and videos at home from anywhere, using a network tunnel to connect to my local Immich server. This setup allows me to synchronize or upload files faster when I’m at home, while still being able to access my media remotely through the Immich application.
Configuration
Cloudflare: DNS records & Edge Certificates
To configure Cloudflare, you need to delegate the authoritative DNS server and
add or point the A
/AAAA
record for the subdomain that will be used by
Immich to your VPS public IP. In this article, I pointed the A
record
i.arch.or.id
to the IP 154.26.xxx.xx
.
To ensure that LXC on your local network can make a smooth request for SSL certificates using Certbot, you’ll need to modify several default Cloudflare settings:
- Update the encryption mode to
Full
. To do this, navigate to domain management -> SSL/TLS -> Overview, and modify the “SSL/TLS encryption” toFull
. This is necessary for Cloudflare to accept the “self-signed certificate” from the origin server. - Disable both “Always Use HTTPS” and “Automatic HTTPS Rewrites”. To achieve this, go to domain management -> Edge Certificates, and ensure that these features are not active. This is necessary for the SSL request verification from LXC to the Let’s Encrypt server to run smoothly.
VPS: WireGuard & Nginx
You need to set up and run WireGuard on the VPS server which will be used to communicate with the LXC server on the local network. If you’re new to WireGuard configuration, I recommend reviewing my previous articles on setting up a WireGuard VPN server manually or using WireGuard-UI.
Here’s an example of my WireGuard configuration on my VPS server:
1[Interface]
2PrivateKey = SomeRandomStringThatShouldBePrivate
3Address = 10.88.88.51/22
4ListenPort = 51822
5
6# Immich LXC server
7[Peer]
8PublicKey = SomeRandomStringThatPublicMayKnow
9AllowedIPs = 10.88.88.105/32
Next, I configured Nginx on the VPS server as a reverse proxy to the LXC server. My Nginx configuration is similar to the following:
1upstream immich_app {
2 server 10.88.88.105:443;
3}
4
5server {
6 listen 80;
7 listen 443 ssl;
8 server_name i.arch.or.id;
9
10 # Self-signed certificates
11 ssl_certificate /etc/nginx/certs/fullchain.pem;
12 ssl_certificate_key /etc/nginx/certs/privkey.pem;
13
14 # Acme challenge handler
15 location /.well-known/acme-challenge/ {
16 allow all;
17 proxy_set_header Host $host;
18 proxy_set_header X-Real-IP $remote_addr;
19 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
20 proxy_set_header X-Forwarded-Proto https;
21
22 # This avoid SSL_do_handshake() failed on HTTPS upstream
23 proxy_ssl_name $host;
24 proxy_ssl_server_name on;
25 proxy_ssl_verify off;
26
27 proxy_pass https://immich_app;
28 }
29
30 keepalive_timeout 70;
31 sendfile on;
32 client_max_body_size 100m;
33
34 location / {
35 proxy_set_header Host $host;
36 proxy_set_header X-Real-IP $remote_addr;
37 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
38 proxy_set_header X-Forwarded-Proto https;
39
40 # This avoid SSL_do_handshake() failed on HTTPS upstream
41 proxy_ssl_name $host;
42 proxy_ssl_server_name on;
43 proxy_ssl_verify off;
44
45 # enable websockets: http://nginx.org/en/docs/http/websocket.html
46 proxy_http_version 1.1;
47 proxy_set_header Upgrade $http_upgrade;
48 proxy_set_header Connection "upgrade";
49 proxy_redirect off;
50
51 proxy_pass https://immich_app;
52 }
53}
It’s worth noting that Nginx virtual host configuration for i.arch.or.id
on
the VPS server uses Self-signed certificates for SSL/TLS encryption. This
isn’t an issue, as we’ve previously configured Cloudflare’s SSL/TLS
encryption mode to Full
.
With this configuration in place, HTTP requests from the internet are routed through Cloudflare and utilize a valid SSL certificate from Cloudflare. The request is then forwarded to the VPS server and subsequently to the LXC server via the WireGuard VPN tunnel.
Local LXC: WireGuard, Immich (Docker), Nginx, Certbot
Install WireGuard and configure it to connect to the WireGuard server on the VPS. Here’s an example of my WireGuard configuration on my LXC server:
1[Interface]
2PrivateKey = SomeRandomStringThatShouldBePrivateII
3Address = 10.88.88.105/22
4
5# VPS server
6[Peer]
7PublicKey = SomeRandomStringThatPublicMayKnowII
8AllowedIPs = 10.88.88.51/32
9Endpoint = 154.26.xxx.xxx:51822
10PersistentKeepalive = 15
Next, install Immich by following the process outlined on its official website
for installing Immich using Docker. By default,
Immich will use TCP port 2283
.
Create an Nginx virtual host configuration for Immich. On my local LXC server, Nginx will function as a reverse proxy and handle Acme challenges to obtain a valid certificate. Here’s an example of the Nginx virtual host configuration for Immich:
1upstream immich_app {
2 server 127.0.0.1:2283;
3}
4
5server {
6 listen 80;
7 server_name i.arch.or.id;
8 root /srv/http/default;
9
10 location /.well-known/acme-challenge/ {
11 allow all;
12 }
13 location / { return 301 https://$host$request_uri; }
14}
15
16server {
17 listen 443 ssl;
18 server_name i.arch.or.id;
19 ssl_certificate /etc/nginx/certs/fullchain.pem;
20 ssl_certificate_key /etc/nginx/certs/privkey.pem;
21
22 # allow large file uploads
23 client_max_body_size 50000M;
24
25 location / {
26 # Set headers
27 proxy_set_header Host $http_host;
28 proxy_set_header X-Real-IP $remote_addr;
29 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
30 proxy_set_header X-Forwarded-Proto $scheme;
31
32 # enable websockets: http://nginx.org/en/docs/http/websocket.html
33 proxy_http_version 1.1;
34 proxy_set_header Upgrade $http_upgrade;
35 proxy_set_header Connection "upgrade";
36 proxy_redirect off;
37
38 proxy_pass http://immich_app;
39 }
40}
From the configuration above, it is clear that I initially uses a self-signed certificate. Later, the certificate will be automatically replaced with a valid one from Let’s Encrypt, utilizing Certbot.
Note: Before requesting an SSL certificate, ensure that the connection between the VPS server and LXC via the WireGuard tunnel is running smoothly. Also, verify that the Nginx configuration on both the VPS server and LXC server is correctly set up.
Install the Certbot Nginx plugin. On Ubuntu-based systems, you can install the
certbot Nginx plugin using sudo apt install python3-certbot-nginx
. After
installing the plugin, request an SSL certificate from the XLC server:
1sudo certbot --nginx -d i.arch.or.id
Replace i.arch.or.id
with your (sub)domain.
LAN: Local DNS resolver
The final step is to configure devices on the local area network (LAN) so that
the subdomain i.arch.or.id
resolves to the local IP address of the LXC server
(192.168.2.105
). A reliable approach is to use a local DNS resolver that can
be utilized by all devices within the LAN network. The configuration for this
DNS resolver will depend on the specific characteristics of each LAN network.
For my LAN setup, I have two DNS resolvers. The first one is located on my Router (MikroTik), and the second is AdGuard Home running on a Linux Container. In addition, I utilize AdGuard home as a DHCP server for my local network.
Here’s a capture of the DNS resolver configuration on my MikroTik router and AdGuard Home:
With this setup, all devices on the local network that obtain their IP
addresses via DHCP will immediately use the IP 192.168.2.105
when attempting
to access the subdomain i.arch.or.id
.
Limitations
- Due to the 100MB per request upload limit imposed by Cloudflare (for its free version), the Immich synchronization process from the internet is likely to fail, particularly when synchronizing videos.