🇺🇸
Author(s) or some of authors are not native speaker(s) of English.
Text may contain bad spelling, incorrect expressions, verbal turns, sentence constructions, etc.
📝
This is a brief action note without explanations. If you don’t understand what it’s about, it’s better to learn a little theory first. Not how it works, but what it is.

If the VPS is new and there is no Docker:

apt update && apt upgrade -y
apt install -y curl wget sudo git
curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh ./get-docker.sh
🚧
If you have a firewall, allow ports 80/tcp and 443/tcp for SSL.

Installation via Script

This is an automatic script for installing and configuring the panel domain, as well as other domains (if needed) and SSL for them. But the NGINX config will be fully configured only for the panel.

git clone https://github.com/nozsh/home-server-vps-ztnet-init ztnet && cd ztnet && bash init/init.sh

Next, create a network in the admin panel. Connect to the network on the home server following the instructions. And on the VPS run:

NET_ID="XYZ"; docker exec ztnet sh -c "cd /var/lib/zerotier-one && mkdir networks.d && cd networks.d && touch $NET_ID.conf"

Where XYZ is the network ID.

If you added other domains besides the panel, you need to edit the NGINX configs – change proxy_pass. You can skip adding additional domains right away. Later just use the script. Where <ZeroTier_IP> is the home server IP in the ZeroTier network, APP_PORT is the port on which the service runs on the home server.

docker compose down && docker compose up -d && docker compose logs -f

And if the home server loses connect:

sudo systemctl restart zerotier-one

Manually

mkdir ztnet && cd ztnet && wget -O docker-compose.yml https://raw.githubusercontent.com/sinamics/ztnet/main/docker-compose.yml
nano docker-compose.yml

Change everything as you need according to the documentation.

Reverse proxy (NGINX)

nano docker-compose.yml

It should be like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
services:
  zerotier:
    ports:
      - "9993:9993/udp"
      - "80:80"
      - "443:443"

  nginx:
    image: nginx:alpine
    container_name: nginx
    restart: unless-stopped
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - /etc/letsencrypt:/etc/letsencrypt:ro
    network_mode: "service:zerotier"
    depends_on:
      - zerotier
Example of complete docker-compose.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
services:
  postgres:
    image: postgres:15.2-alpine
    container_name: postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: database_password
      POSTGRES_DB: ztnet
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - app-network

  zerotier:
    image: zyclonite/zerotier:1.14.2
    hostname: zerotier
    container_name: zerotier
    restart: unless-stopped
    volumes:
      - zerotier:/var/lib/zerotier-one
    cap_add:
      - NET_ADMIN
      - SYS_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    networks:
      - app-network
    ports:
      - "9993:9993/udp"
      - "80:80"
      - "443:443"
    environment:
      - ZT_OVERRIDE_LOCAL_CONF=true
      - ZT_ALLOW_MANAGEMENT_FROM=172.31.255.0/29

  ztnet:
    image: sinamics/ztnet:latest
    container_name: ztnet
    working_dir: /app
    volumes:
      - zerotier:/var/lib/zerotier-one
    restart: unless-stopped
    ports:
      - 3000:3000
    environment:
      POSTGRES_HOST: postgres
      POSTGRES_PORT: 5432
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: database_password
      POSTGRES_DB: ztnet
      NEXTAUTH_URL: "https://ztnet-panel.domain.org"
      NEXTAUTH_SECRET: "something_random"
      NEXTAUTH_URL_INTERNAL: "http://ztnet:3000"
    networks:
      - app-network
    links:
      - postgres
    depends_on:
      - postgres
      - zerotier

  nginx:
    image: nginx:alpine
    container_name: nginx
    restart: unless-stopped
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - /etc/letsencrypt:/etc/letsencrypt:ro
    network_mode: "service:zerotier"
    depends_on:
      - zerotier
 
volumes:
  zerotier:
  postgres-data:
 
networks:
  app-network:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.31.255.0/29

NGINX configs

mkdir -p nginx/conf.d
nano nginx/conf.d/panel-ztnet.domain.org.conf
Config for panel
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
server {
    listen 80;
    server_name ztnet-panel.domain.org;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name ztnet-panel.domain.org;

    ssl_certificate /etc/letsencrypt/live/ztnet-panel.domain.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ztnet-panel.domain.org/privkey.pem;

    ssl_session_cache shared:le_nginx_SSL:10m;
    ssl_session_timeout 1440m;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://ztnet:3000;
        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;
    }
}
nano nginx/conf.d/your-app.domain.org.conf
Config for some services
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
server {
    listen 80;
    server_name your-app.domain.org;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name your-app.domain.org;

    ssl_certificate /etc/letsencrypt/live/your-app.domain.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-app.domain.org/privkey.pem;

    ssl_session_cache shared:le_nginx_SSL:10m;
    ssl_session_timeout 1440m;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://<ZeroTier_IP>:APP_PORT;
        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;
    }
}

Change where highlighted. Where <ZeroTier_IP> is the home server IP in the ZeroTier network, APP_PORT is the port on which the service runs on the home server.

SSL

SSL can be obtained through the Docker too, but it will be easier through the host.

apt install certbot
certbot certonly --standalone --non-interactive --agree-tos --email [email protected] -d ztnet-panel.domain.org
ls /etc/letsencrypt/live/
💡
SSL cannot be obtained/renewed if the zerotier container is running, due to port conflict.

Start

docker compose up -d && docker compose logs -f

In the admin panel, create a network. Connect to the network on the home server following the instructions. Next, you need to add the VPS running ZTNet to the network with the home server:

docker exec -it ztnet sh
cd /var/lib/zerotier-one
mkdir networks.d
cd networks.d
touch <network>.conf

Where <network> is the network ID.

exit
docker compose down && docker compose up -d && docker compose logs -f

And if the home server loses connect:

sudo systemctl restart zerotier-one

Add/Configure Services (NGINX)

To add a new service, copy the existing NGINX configuration, and change the domain and proxy_pass. And request an SSL certificate.

docker compose down && docker compose up -d && docker compose logs -f

Or if you used the automatic configuration script (or just download this script separately), from the directory where docker-compose.yml is located, run:

bash init/add_new.sh

The script will stop the Docker containers, create the NGINX config, request SSL and start the containers. If there were no errors – no manual actions are needed.

Useful commands

Docker

You can check that the connection reaches your home server through the NGINX container with the command:

docker exec nginx wget -O- http://<ZeroTier_IP>:APP_PORT --timeout=5

If something didn’t start, and/or doesn’t work properly after:

docker compose restart && docker compose logs -f

Run:

docker compose down && docker compose up -d && docker compose logs -f

ZeroTier

sudo zerotier-cli join <network>
sudo zerotier-cli leave <network>
sudo zerotier-cli listnetworks

Scripts

VPS

“certbot_renew.sh”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/bin/bash

cd /root/ztnet
docker compose down

certbot renew --quiet
#certbot renew

bash start_nologs.sh
#bash restart_nologs.sh

In the same directory where docker-compose.yml is located:

“start.sh”:

1
2
3
#!/bin/bash

docker compose up -d && docker compose logs -f

“start_nologs.sh”:

1
2
3
#!/bin/bash

docker compose up -d

“restart.sh”:

1
2
3
4
#!/bin/bash

docker compose down
docker compose up -d && docker compose logs -f

“restart_nologs.sh”:

1
2
3
4
#!/bin/bash

docker compose down
docker compose up -d

Cron

sudo crontab -e

VPS:

0 4 1 * * /bin/bash /root/certbot_renew.sh

Home Server:

2 4 1 * * systemctl restart zerotier-one