How much self-hosting can one VPS accomplish?
My goal for this project was to really see how much I could multi-task a VPS. Ideally I would be able to:
- Replace my hosting plan for a static site or two
- Run a wireguard VPN server
- Use that VPN to securely tunnel traffic into my home network without opening ports in my firewall
This was the challenge that I set for myself, and the good news is that it turns out that it’s pretty easy! That said I’m not a networking or IT expert, so it took me a good long time to figure it out.
Also a quick disclaimer I do not endorse this method as fully secure. I highly recommend that you evaluate this for yourself and make sure to harden both your home network and the VPS that you will be using against attack. For systems in use, I have another process I use to secure the VPS.
Spoilers…
The solution looks like this:
- A VPS using docker compose to run:
- a wireguard server
- SWAG (linuxserver.io solution for an nginx reverse proxy)
- Whatever other services you desire
- A machine on your home network using docker compose to run:
- a wireguard client
- SWAG or another nginx reverse proxy
- Whatever other services you desire
This isn’t going to be a tutorial on docker compose, so best hit some tutorials on that first if you’re new to docker and docker compose. I also won’t be covering how to point domains and sub-domains to the VPS, as that is pretty simple and easily found online already. This will also be much easier if you have some knowledge of what a reverse proxy is, and of nginx in particular.
1. Set up Wireguard.
This is pretty easy to do if you use the linuxserver.io docker image for wireguard. For the VPS which will be running wireguard in server mode, you can essentially just copy the docker-compose.yml file reproduced here.
version: "2.1"
services:
wireguard:
image: lscr.io/linuxserver/wireguard:latest
container_name: wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE #optional
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC #Don't forget to set this
- SERVERURL=wireguard.domain.com #Can be a domain or VPS IP address
- SERVERPORT=51820 #optional
- PEERS=1 #optional
- PEERDNS=auto #optional
- INTERNAL_SUBNET=10.13.13.0 #optional
- ALLOWEDIPS=0.0.0.0/0 #optional
- PERSISTENTKEEPALIVE_PEERS= #optional
- LOG_CONFS=true #optional
volumes:
- /path/to/appdata/config:/config
- /lib/modules:/lib/modules #optional
ports:
- 51820:51820/udp
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped
Once this is running just dive into “/path/to/appdata/config/peer1” Where wireguard will have made a file called peer1.conf. This is the configuration file you will need for the peer to be able to connect to.
On your home server, you will now need to spin up a wireguard container to connect to your VPS. The wireguard portion of this file will look like this:
wireguard_tunnel:
image: ghcr.io/linuxserver/wireguard
container_name: wireguard_tunnel
cap_add:
- NET_ADMIN
- SYS_MODULE
ports:
- 80:80
- 443:443
- 51820:51820/udp
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC #Don't forget to set this
volumes:
- /path/to/appdata/config:/config
- /lib/modules:/lib/modules
restart: unless-stopped
Once this is up and running go to “/path/to/appdata/config/” and create a file called “wg0.conf” and give it the same contents as “peer1.conf” from above. Now run sudo docker restart wireguard_tunnel
and it should connect! You can check that it’s connected by running sudo docker exec -ti wireguard_tunnel wg show
and you should get something like this:
interface: wg0
public key: *your public key*
private key: (hidden)
listening port: 51820
peer: *redacted*
preshared key: (hidden)
endpoint: xxx.xxx.xxx:51820
allowed ips: 10.13.13.0/24
latest handshake: 10 seconds ago
transfer: 9.41 MiB received, 129.34 MiB sent
With some differences to ip addresses and ports if you chose to change those in your docker-compose files. Now all we have to do is reverse proxy traffic that comes to our VPS either to the appropriate site, or down the tunnel to our self-hosted service.
2. SWAG reverse proxy
(SWAG)[https://docs.linuxserver.io/images/docker-swag] is yet another container put together by linuxserver.io that is going to make running everything very, very, easy. Again, we can pretty much use the docker compose example they give, but there is one important exception. If we want to be able to proxy traffic down the wireguard tunnel, our SWAG container must use wireguard for networking! This also means that instead of opening ports that SWAG will use in its own container definition, we open those ports in wireguard’s container definition. This results in a docker-compose.yml file that looks somthing like this:
version: "2.1"
services:
wireguard:
image: lscr.io/linuxserver/wireguard:latest
container_name: wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE #optional
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC #Don't forget to set this
- SERVERURL=wireguard.domain.com #Can be a domain or VPS IP address
- SERVERPORT=51820 #optional
- PEERS=1 #optional
- PEERDNS=auto #optional
- INTERNAL_SUBNET=10.13.13.0 #optional
- ALLOWEDIPS=0.0.0.0/0 #optional
- PERSISTENTKEEPALIVE_PEERS= #optional
- LOG_CONFS=true #optional
volumes:
- /path/to/appdata/config:/config
- /lib/modules:/lib/modules #optional
ports:
- 443:443
- 80:80
- 51820:51820/udp
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped
swag:
image: lscr.io/linuxserver/swag:latest
container_name: swag
cap_add:
- NET_ADMIN
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
- URL=yourdomain.url
- VALIDATION=http
- SUBDOMAINS=www,another,subdomain
- EXTRA_DOMAINS= anotherdomain.url,service.anotherdomain.url,thriddomain.url
volumes:
- /path/to/appdata/config:/config
network_mode: "service-wireguard"
depends_on:
- wireguard
restart: unless-stopped
Rebuild this stack and SWAG will come online and should automatically generate ssl certs for any domains and/or sub-domains you listed.
If you want to direct SWAG to serve a static site that is hosted on your VPS, simply go to “/path/to/appdata/config/nginx/site-confs” and copy the default sample file to a new file and fill it with the necessary info. All you should need to change is the server to one of your domains or sub domains, something like:
server_name yourdomain.url;
or
server_name subdomain.yourdomain.url;
and then change the root directory to the location of your static site.
root /config/www;
or
root /config/second_site;
Unless you’re doing something wild, this should work once you put the files for your site in the correct location, and restart SWAG.
3. Proxy-ing Traffic through Wireguard.
Thankfully we’ve already done most of the work to make this happen! That said, to get our wireguard traffic to go to the right place, we will need a reverse proxy on the server side as well. To keep things simple (and expand out experience) we’ll use a standard nginx on this side of things. As before we need to make sure that the reverse proxy uses wireguard for its networking otherwise it won’t be able to see any of the traffic that comes in through wireguard. All set up, it will look something like this:
wireguard_tunnel:
image: ghcr.io/linuxserver/wireguard
container_name: wireguard_tunnel
cap_add:
- NET_ADMIN
- SYS_MODULE
ports:
- 80:80
- 443:443
- 51820:51820/udp
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
volumes:
- ./wireguard/config:/config
- /lib/modules:/lib/modules
restart: unless-stopped
nginx:
image: nginx
container_name: nginx
network_mode: "service:wireguard_tunnel"
depends_on:
- wireguard_tunnel
restart: unless-stopped
volumes:
- path/to/appdata/nginx/conf.d:/etc/nginx/conf.d:rw
Opening port 80 and 443 is not necessary if you don’t care about reaching your nginx container on your local network, but it can be very helpful for debugging. If you use a port other than one of these, and you hope to reach it on your home network, open those instead. I’ll use port 80 for this demo, but most choices work fine. Since we’ve got this set up let’s go ahead and prepare ourselves to receive traffic. Create a file at path/to/appdata/nginx/conf.d
named nginx.conf
. We’ll assume that you want to forward traffic to jellyfin, since this is a common service that people want to open to the internet. Assuming that jellyfin is on the standard port 8096, our file would look something like this:
server {
listen 80;
location / {
proxy_pass http://xxx.xxx.x.xxx:8096; #use the IP of your machine running jellyfin.
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
set_real_ip_from 10.13.13.1/32; #assuming you used standard wireguard options
real_ip_header X-Forwarded-For;
}
}
Now we just need to send traffic down the pipe! One of the nice things about SWAG is that it has reverse proxy examples for many common services set up! On your VPS run:
sudo cp /path/to/appdata/config/nginx/proxy-confs/jellyfin.subdomain.conf.sample /path/to/appdata/config/nginx/proxy-confs/jellyfin.subdomain.conf
sudo nano /path/to/appdata/config/nginx/proxy-confs/jellyfin.subdomain.conf
Now we just need to change a few here. We need to send the traffic to the wireguard IP of our home server, and we need to send it to port 80 (or whatever port you chose) on that server. In the end the file will look somethig like this:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name jellyfin.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
location / {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app 10.13.13.2; #assuming default wireguard config
set $upstream_port 80; #assuming you followed the example above
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
}
location ~ (/jellyfin)?/socket {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app 10.13.13.2; #assuming default wireguard config
set $upstream_port 80; #assuming you followed the example above
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
At this point (make sure you restart nginx and swag) everything should work! On single low-tier VPS you can host multiple static sites and have access to home services without opening any ports in your personal firewall or network. Enjoy!