Remnawave install: panel and nodes

Remnawave install for the panel and nodes: Docker behind a TLS reverse proxy, VLESS Reality Vision nodes, subscriptions, backups, updates and common failures.

Current as of September 2026: Remnawave panel 3.4.4, node 3.4.1 with Xray-core 26.7.28 built in, subscription page 8.0.0. We skip Docker and Xray basics and focus on a multi-node setup and on what breaks in production.

What you'll need

  • A VPS for the panel: at least 2 vCPU and 2 GB RAM, 4 and 4 recommended, 20 GB of disk, Ubuntu or Debian.
  • A VPS per location: a node runs on 1 vCPU and 1 GB, but Xray gets hungry under load, so size commercial nodes with headroom. At ForestsNet Cloud these are servers in the Netherlands (Eygelshoven) and Germany (Frankfurt) on AMD EPYC 7713 with NVMe and 1–10 Gbit/s ports; IP addresses are checked daily for blocks by Russia's DPI (TSPU).
  • A domain: A records panel.example.com and sub.example.com pointing to the panel server, nl1.example.com and de1.example.com pointing to the nodes, see Pointing a domain at your server.
  • Docker with the Compose plugin on every server: curl -fsSL https://get.docker.com | sh, details in Docker and Docker Compose.
  • Basic hardening: SSH hardening, the ufw firewall, VPS security checklist.

Step 1. Plan which component runs where

Almost every Remnawave failure is one service that can't reach another, so start with the map.

  • Panel (remnawave/backend): API and web UI on port 3000, Prometheus metrics on 3001. Next to it run PostgreSQL 18 with all the data and Valkey, a Redis-compatible queue and cache on a unix socket. Everything is published on 127.0.0.1 only; the reverse proxy faces the internet.
  • Node (remnawave/node): a container with Xray-core. The panel pushes its configuration, users and keys.
  • Subscription page (remnawave/subscription-page): port 3010. Serves subscriptions from its own domain and calls the panel API with a token.

Connections:

  • Panel → node: the panel connects to the node on NODE_PORT (2222 by default) over HTTPS with mutual certificate authentication, TLS 1.3 only. The panel is the certificate authority; the node's certificate and key are packed into its SECRET_KEY. The node never connects to the panel.
  • Client → node: 443/TCP, the VLESS Reality inbound.
  • Client → subscription page: 443 on sub.example.com; the page then talks to the panel over the internal Docker network.

If the panel goes down, nodes keep serving existing users with the last configuration they received, but new users, renewals and suspensions wait until it's back. Keep the panel on its own VPS rather than on a node: node addresses are the ones you'll end up changing.

Step 2. Install the panel

mkdir -p /opt/remnawave && cd /opt/remnawave
curl -o docker-compose.yml https://raw.githubusercontent.com/remnawave/backend/refs/heads/main/docker-compose-prod.yml
curl -o .env https://raw.githubusercontent.com/remnawave/backend/refs/heads/main/.env.sample

The compose file uses remnawave/backend:3 (updates within major version 3), PostgreSQL 18.4 and Valkey 9. Generate the secrets and the database password:

sed -i "s/^APP_SECRET=.*/APP_SECRET=$(openssl rand -hex 64)/" .env
sed -i "s/^METRICS_PASS=.*/METRICS_PASS=$(openssl rand -hex 64)/" .env
sed -i "s/^WEBHOOK_SECRET_HEADER=.*/WEBHOOK_SECRET_HEADER=$(openssl rand -hex 64)/" .env
pw=$(openssl rand -hex 24)
sed -i "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$pw/" .env
sed -i "s|^\(DATABASE_URL=\"postgresql://postgres:\)[^@]*\(@.*\)|\1$pw\2|" .env

The JWT secrets from older guides are gone: in 3.x a single APP_SECRET signs sessions and API tokens, and the panel refuses to start with change_me. When upgrading from 2.x, rename JWT_AUTH_SECRET to APP_SECRET and keep the value.

Set the domains in .env:

FRONT_END_DOMAIN=panel.example.com
PANEL_DOMAIN=panel.example.com
SUB_PUBLIC_DOMAIN=sub.example.com

FRONT_END_DOMAIN drives CORS, PANEL_DOMAIN is used for links in notifications, and SUB_PUBLIC_DOMAIN is what the panel builds subscription links from: no https:// and no trailing slash. Without a separate subscription page, use panel.example.com/api/sub.

docker compose up -d && docker compose ps
Important: the PostgreSQL password is applied only when the remnawave-db-data volume is first created. Change it in .env later and the panel can't reach the database and goes into a restart loop. Any .env change needs the containers recreated with docker compose down && docker compose up -d; a restart is not enough.

Step 3. Set up the reverse proxy and lock down the panel

The backend accepts only requests that came through a proxy over HTTPS: without X-Forwarded-For and X-Forwarded-Proto: https it silently drops the connection and logs "Reverse proxy and HTTPS are required". It also can't run under a sub-path like /panel — only at the root of a domain or subdomain.

We'll use Caddy: it gets Let's Encrypt certificates on its own and sets the headers; basics are in Caddy: automatic HTTPS. The panel gets a knock-and-cookie gate right away: without a secret cookie Caddy closes the connection without any response. The sign-up form never faces the internet, which matters because the first user to register becomes the super-admin.

mkdir -p /opt/remnawave/caddy && cd /opt/remnawave/caddy
echo "RW_KNOCK=$(openssl rand -hex 16)" > .env

Caddyfile:

panel.example.com {
	@knock query rw={$RW_KNOCK}
	@allowed header Cookie *rw_gate={$RW_KNOCK}*
	route {
		header @knock Set-Cookie "rw_gate={$RW_KNOCK}; Path=/; Max-Age=2592000; HttpOnly; Secure; SameSite=Lax"
		reverse_proxy @knock remnawave:3000
		reverse_proxy @allowed remnawave:3000
		abort
	}
}

sub.example.com {
	reverse_proxy remnawave-subscription-page:3010
}

docker-compose.yml:

services:
  caddy:
    image: caddy:2.11
    container_name: caddy
    restart: always
    env_file: .env
    ports:
      - "80:80"
      - "443:443"
    networks:
      - remnawave-network
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data

networks:
  remnawave-network:
    external: true

volumes:
  caddy-data:

Run docker compose up -d. To get in, open https://panel.example.com/?rw=<RW_KNOCK value>: Caddy sets a 30-day cookie, and after that the panel opens at its normal address. The panel's compose file created remnawave-network, so Caddy reaches the containers by name.

Tip: on the panel server, allow only 22, 80 and 443 in ufw. Ports published by Docker bypass ufw, which is why 3000, 3001 and 6767 in the panel's compose file are bound to 127.0.0.1 — don't change that to 0.0.0.0.

Panel 3.x has no built-in cookie gate. The docs offer remnawave/caddy-with-auth with TOTP (the image hasn't been updated since March 2025), TinyAuth in front of nginx, and Cloudflare Zero Trust; in nginx the same knock is done with map $http_cookie, as in the eGames script. Telegram OAuth login may break behind such gates.

Step 4. First login and the subscription page

Open the knock link and register — this account becomes the super-admin. In Remnawave Settings, add a passkey and turn off password login; if you lock yourself out, the Rescue CLI brings it back: docker exec -it remnawave cli.

Issue an API token for the subscription page in Remnawave Settings → API Tokens. In 3.x tokens must have an expiry — set it generously and add a reminder: an expired token breaks subscriptions for everyone at once.

/opt/remnawave/subscription/docker-compose.yml:

services:
  remnawave-subscription-page:
    image: remnawave/subscription-page:8.0.0
    container_name: remnawave-subscription-page
    hostname: remnawave-subscription-page
    restart: always
    env_file: .env
    ports:
      - "127.0.0.1:3010:3010"
    networks:
      - remnawave-network

networks:
  remnawave-network:
    external: true

.env next to it:

APP_PORT=3010
REMNAWAVE_PANEL_URL=http://remnawave:3000
REMNAWAVE_API_TOKEN=token_from_the_panel
TRUST_PROXY=1

The scheme in REMNAWAVE_PANEL_URL must be http://. The repository's .env.sample has https://remnawave:3000, but inside the Docker network the backend speaks plain HTTP, and the page adds the proxy-check headers itself only for an http:// address. TRUST_PROXY=1 means one trusted proxy, Caddy. Start it with docker compose up -d.

A subscription link looks like https://sub.example.com/<shortUuid>: a browser gets a page with setup instructions, an app gets its configuration.

Step 5. Build the VLESS Reality Vision profile

A config profile (Config Profiles) is a complete Xray config for a node. The panel validates it with a lightweight core on every save and fills in users itself, so leave clients empty. For VLESS with Reality over RAW (formerly TCP), clients get flow: xtls-rprx-vision automatically.

Reality target: your own site on the node

Reality borrows the TLS handshake of a target site (target, formerly dest) and forwards everything that fails authentication to it. The setup here is self-steal: each node has its own subdomain with an A record pointing to its IP, Caddy with a Let's Encrypt certificate listens on 127.0.0.1:9443 on the node, target points there, and serverNames lists your domains. The SNI honestly resolves to the node's address, a visitor without the key sees an ordinary website, and Caddy serves TLS 1.3 and HTTP/2 — what Reality requires of a target — out of the box. One profile serves all nodes, and a burned subdomain is replaced in the panel: clients pick up the new SNI with their next subscription update.

On ForestsNet Cloud servers this is the only option: the Terms of Service prohibit making a server impersonate someone else's site, that is, putting a domain you don't own into SNI, target or a certificate. Such setups get IP addresses blocked, and that hurts every customer on the network.

The profile editor also has a ready-made self-steal template from the remnawave/templates repository (menu → Download from Github): it uses nginx on the unix socket /dev/shm/nginx.sock with PROXY protocol (xver: 1). The Caddy variant on 127.0.0.1:9443 below is simpler: no socket and no /dev/shm mount in the node container. A ready-made self-steal installer for Caddy or nginx is in DigneZzZ's scripts from the community catalog on docs.rw.

Keys, shortIds and minClientVer

Generate x25519 keys in the profile editor: bottom menu → Generate Keys. The privateKey goes into the profile; the panel derives the public key and hands it to clients. shortIds are hex strings of even length up to 16 characters, for example from openssl rand -hex 8; clients get the first element of the array, so put a non-empty ID first.

The Xray in node 3.4.1 accepts Reality clients only from Xray 26.3.27 and newer by default. Mihomo and sing-box — the cores behind Clash Verge, FlClashX, Hiddify and Karing — identify themselves as version 1.8 and get refused, which is why Remnawave's templates set "minClientVer": "0.0.0". XTLS warns that old and third-party clients stand out by their TLS fingerprint and that lowering the threshold raises the risk of blocking. If all your users run recent Xray-based apps such as Happ, v2rayNG or v2rayN, you can drop the line.

A self-steal profile shared by all nodes:

{
  "log": { "loglevel": "warning" },
  "inbounds": [
    {
      "tag": "VLESS_REALITY",
      "listen": "0.0.0.0",
      "port": 443,
      "protocol": "vless",
      "settings": { "clients": [], "decryption": "none" },
      "sniffing": { "enabled": true, "destOverride": ["http", "tls", "quic"] },
      "streamSettings": {
        "network": "raw",
        "security": "reality",
        "realitySettings": {
          "target": "127.0.0.1:9443",
          "xver": 0,
          "serverNames": ["nl1.example.com", "de1.example.com"],
          "privateKey": "GENERATE_IN_THE_PANEL",
          "shortIds": ["9f3c1a7e5b2d4086"],
          "minClientVer": "0.0.0"
        }
      }
    }
  ],
  "outbounds": [
    { "protocol": "freedom", "tag": "DIRECT" },
    { "protocol": "blackhole", "tag": "BLOCK" }
  ],
  "routing": {
    "rules": [
      { "ip": ["geoip:private"], "outboundTag": "BLOCK" },
      { "domain": ["geosite:private"], "outboundTag": "BLOCK" },
      { "protocol": ["bittorrent"], "outboundTag": "BLOCK" }
    ]
  }
}

The private rules keep clients out of the node's local services, including Caddy on 127.0.0.1. The bittorrent rule catches only part of torrent traffic — for the rest see How to block torrents on your VPN or proxy and A torrent detector for Remnawave.

Step 6. Install the nodes

The self-steal site

On each node, run Caddy: it gets a Let's Encrypt certificate for the node's subdomain via HTTP-01 on port 80 and serves the site on 127.0.0.1:9443. Don't proxy the node's DNS record through Cloudflare — clients must reach the IP directly. Put some plausible static content into /opt/selfsteal/site, at least an index.html.

/opt/selfsteal/Caddyfile:

{
	https_port 9443
	default_bind 127.0.0.1
	auto_https disable_redirects
}

http://nl1.example.com {
	bind 0.0.0.0
	redir https://nl1.example.com{uri} permanent
}

https://nl1.example.com {
	root * /srv/site
	file_server
}

/opt/selfsteal/docker-compose.yml:

services:
  caddy:
    image: caddy:2.11
    container_name: selfsteal
    restart: always
    network_mode: host
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./site:/srv/site:ro
      - caddy-data:/data

volumes:
  caddy-data:

The node

In the panel, open Nodes → Management → +: country, name, address and port 2222. The Copy docker-compose.yml button copies a file with the SECRET_KEY — base64 of a JSON holding the panel's CA certificate, the node's certificate and key, and the public key for JWT checks. Treat it like a password. On the node, save the file as /opt/remnanode/docker-compose.yml:

services:
  remnanode:
    container_name: remnanode
    hostname: remnanode
    image: remnawave/node:3.4.1
    network_mode: host
    restart: always
    cap_add:
      - NET_ADMIN
    ulimits:
      nofile:
        soft: 1048576
        hard: 1048576
    environment:
      - NODE_PORT=2222
      - SECRET_KEY="key_from_the_panel"

The panel puts in the latest tag — replace it with an explicit version: latest means whatever was newest at each server's last pull, and your fleet quietly drifts across versions. NET_ADMIN is needed by the node's nftables plugins, including Torrent Blocker, and for dropping connections of removed users.

cd /opt/remnanode && docker compose up -d && docker compose logs -f -t

Back in the panel, click Next, pick the profile and enable the VLESS_REALITY inbound on the node. Within seconds the node card shows the Xray and node versions.

The node firewall

Open NODE_PORT to the panel's IP only. The node uses host networking, so ufw rules apply to it directly:

ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow from 203.0.113.10 to any port 2222 proto tcp
ufw enable

203.0.113.10 is the panel server; port 80 lets Caddy issue and renew its certificate.

Tip: a node has traffic tracking with a limit and a reset day. The panel counts user traffic once, and only outbound traffic counts toward a ForestsNet plan, so you can set the limit roughly equal to the plan — no halving, which the docs suggest for hosts that bill both directions.

Step 7. Hosts and squads for several locations

A host is the client side of an inbound: the address, port, SNI and label the user sees in the app. Create a host per node in Hosts: inbound VLESS_REALITY, address nl1.example.com, port 443, and in the advanced settings enable Override SNI from Address — otherwise the client gets the first element of serverNames. Selecting nodes in the host form is only a label for the admin.

Access is controlled by internal squads (Internal Squads) — sets of inbounds from any profiles. A user gets the hosts of the inbounds in their squads and is added to Xray on every node where those inbounds are enabled. The unit of access is the inbound, not the node: if some customers should see only the Netherlands and others Germany as well, create a profile with its own inbound per location and build squads such as Base and Premium from them. External squads (External Squads) override subscription templates and settings for a group of users.

Step 8. Users and subscriptions

Each user has a traffic limit and a reset strategy: NO_RESET, DAY, WEEK (Mondays), MONTH (the 1st) or MONTH_ROLLING — monthly on the day the user was created, handy for subscriptions bought on arbitrary dates. The same form sets the expiry date and squads. Statuses are ACTIVE, DISABLED, LIMITED (traffic used up) and EXPIRED; an inactive user gets placeholder entries such as "Subscription expired" instead of servers, with texts editable in Subscription → Settings. The traffic multiplier on a node lets you count an expensive location's traffic double, for example.

The HWID device limit (Subscription → Settings → HWID Device Limit) is off by default. Not every app sends an HWID — the docs list Happ, v2RayTun, Koala Clash and FlClashX — and once it's on, a client without an HWID gets an empty subscription or an "App not supported" placeholder unless the limit is disabled for that user.

Response Rules pick the response format from request headers. By default a browser gets the subscription page, Mihomo, Stash, sing-box and Clash clients get a config from their template in Templates, and everything else gets a base64 list of links. A separate rule can serve a custom template to a specific app.

Apps by platform:

  • Android: v2rayNG and Happ (Xray), Hiddify (sing-box), FlClashX (Mihomo).
  • iOS and macOS: Happ, Streisand and V2Box (Xray), Shadowrocket, Stash, sing-box.
  • Windows, Linux and macOS: v2rayN (Xray), Clash Verge and Koala Clash (Mihomo), Hiddify and Throne (sing-box).

Step 9. Operations

PostgreSQL backups

The database holds everything: users with their UUIDs, profiles, hosts, settings and the keys of the certificate authority the nodes trust. Lose it without a copy and you're issuing a new SECRET_KEY to every node and new credentials to every user. Keep .env next to the dump: with a different APP_SECRET, issued API tokens stop working.

/usr/local/bin/rw-backup.sh:

#!/bin/sh
set -e
D=/opt/remnawave/backup
mkdir -p "$D"
docker exec remnawave-db pg_dump -U postgres -d postgres -Fc > "$D/rw-$(date +%F-%H%M).dump"
cp /opt/remnawave/.env "$D/env-$(date +%F)"
find "$D" -type f -mtime +14 -delete
chmod +x /usr/local/bin/rw-backup.sh
echo "20 3 * * * root /usr/local/bin/rw-backup.sh" > /etc/cron.d/rw-backup

Ship copies off the server, see Database backups. Restore onto the same panel version: start only the database, load the dump, then start the rest.

cd /opt/remnawave && docker compose up -d remnawave-db
docker exec -i remnawave-db pg_restore -U postgres -d postgres --clean --if-exists < backup/rw-2026-09-30-0320.dump
docker compose up -d

Updates

Panel first, then nodes:

cd /opt/remnawave && docker compose pull && docker compose down && docker compose up -d

The remnawave/backend:3 tag won't jump to the next major version on its own, and that's a good thing: in 3.0 users in the REST API switched from UUIDs to numeric ids, and bots built on the old API broke. Before a major upgrade, read the release notes and take a dump. Update nodes one at a time: change the tag, run docker compose pull && docker compose up -d, check the version on the node card, move on.

Logs and monitoring

  • Panel: docker compose logs -f remnawave, processes via docker exec -it remnawave pm2 monit, liveness via curl -fsS http://127.0.0.1:3001/health.
  • Node: docker logs -f remnanode for the node itself, docker exec remnanode xlogs for Xray. If you write Xray logs to the host under /var/log/remnanode, set up logrotate, or the access log will eat the disk.
  • Metrics: http://127.0.0.1:3001/metrics with basic auth from METRICS_USER and METRICS_PASS. For alerts, use remnawave_node_status (1 means connected) and remnawave_node_online_users; there's a ready Grafana dashboard, 25064.
  • Notifications: node outages can go to a Telegram bot (IS_TELEGRAM_NOTIFICATIONS_ENABLED, TELEGRAM_BOT_TOKEN, TELEGRAM_NOTIFY_NODES) or a webhook (WEBHOOK_ENABLED, WEBHOOK_URL).

Migrating from Marzban

The official route is the remnawave/migrate tool, v2.1.0 (Marzban, Marzneshin, 3X-UI). It moves users with their VLESS UUIDs, Trojan and Shadowsocks passwords, limits and expiry dates. Bring the nodes up first. If your old setup already used your own domain, keep the addresses, ports, Reality key, SNI and shortId — clients keep working without a subscription update; configs that used someone else's target have to be reissued. Remnawave has no VMess. For the source panel, see Marzban install: panel and nodes.

./remnawave-migrate --panel-type=marzban \
  --panel-url=https://marzban.example.com --panel-username=admin --panel-password='password' \
  --remnawave-url=https://panel.example.com --remnawave-token='API-token' \
  --dest-headers="Cookie:rw_gate=RW_KNOCK_value" \
  --internal-squad=SQUAD_UUID --preserve-status --preserve-subhash --last-users=5

--dest-headers gets the tool through the gate from step 3. The tool predates panel 3.0, so start with a trial run of --last-users=5. Old /sub/<token> links can be served by the subscription page: add MARZBAN_LEGACY_LINK_ENABLED=true, CUSTOM_SUB_PREFIX=sub and MARZBAN_LEGACY_SECRET_KEY — the result of SELECT secret_key FROM jwt LIMIT 1; in the Marzban database — to its .env, and set SUB_PUBLIC_DOMAIN=sub.example.com/sub in the panel's .env.

Troubleshooting

The node won't connect

  • Port closed. From the panel server: curl -vk --max-time 5 https://<node IP>:2222. A TLS error means the port is open and the node answers; a timeout means ufw, the provider's firewall or a wrong port on the node card.
  • Broken SECRET_KEY. The key got truncated or picked up a line break, and the node exits with SECRET_KEY contains invalid JSON or Invalid SECRET_KEY payload. Copy the compose file from the panel again.
  • Panel reinstalled without the database. The certificate authority is new — issue a new SECRET_KEY to every node.
  • Xray won't start. The node is connected but inbounds are dead; the node log shows "Xray Core … failed to start" with the tail of the Xray log (full log: docker exec remnanode xlogs). It's almost always a profile error or port 443 already in use.
  • Clock skew. mTLS and JWT depend on time: check timedatectl on both sides.

The subscription is empty or shows placeholders

  • "No hosts found", "Check Hosts tab", "Check Internal Squads tab". The user's squads have no inbound with an enabled host, or the host is disabled, hidden or restricted to other squads. If the host is delivered but connections fail, the inbound isn't enabled on the node.
  • "Subscription expired", "limited", "disabled". That's the user's status, not a fault.
  • Empty response or "App not supported". The HWID limit is on and the app sends no HWID; "Limit of devices reached" means the device limit is used up.
  • 404. No user with that shortUuid: deleted, or the link was reissued.
  • Errors for everyone at once. docker logs remnawave-subscription-page: the API token expired, or REMNAWAVE_PANEL_URL says https://.
  • Links in the panel point to the wrong place. SUB_PUBLIC_DOMAIN includes a scheme or trailing slash, or the container wasn't recreated after editing .env.

Reality fails on some networks

First connect with the same client from a network without censorship: if it fails there too, the problem is the node or the profile. If it fails only inside a censored country:

  • Russia: the first 14–25 KB arrive, then silence. Since June 2025, TSPU at many providers freezes TCP connections to the subnets of large hosting providers and CDNs. XHTTP and a new subdomain don't help; a relay server inside Russia does. The TCP 16-20 checker from hyperion-cs/dpi-checkers shows whether a given provider applies it.
  • Russia: heavy pages hang for a couple of minutes. On some mobile networks a burst of TLS handshakes to one server freezes new connections, and Vision opens one handshake per app connection. A second inbound on XHTTP and the firefox fingerprint in the host's advanced settings help.
  • Nothing works on mobile only. Whitelisting: the operator lets through only approved IP ranges, and no protocol reaches a foreign VPS. Russia's mobile networks do this during restrictions, and Iran has cut international traffic almost entirely during shutdowns. That takes an entry server inside the country — a separate setup.
  • One node dropped while the others work. Its address or SNI is blocked — in China and Iran this is the usual failure mode. Change the self-steal subdomain first, then the address; if a ForestsNet address is blocked, contact support.

Commands for testing from a customer's network are covered in 3x-ui install and VLESS Reality setup, protocol choice in Self-hosted VPN protocols.

If the handshake fails for every client of a node, it isn't DPI: the host's SNI isn't in serverNames, the target doesn't answer (on the node: curl -vk --resolve nl1.example.com:9443:127.0.0.1 https://nl1.example.com:9443), or the host is bound to an inbound of another profile with a different key.

502 from the reverse proxy

  • The panel isn't running or keeps restarting. docker compose ps in /opt/remnawave and curl -fsS http://127.0.0.1:3001/health; a common cause of the loop is a database password changed after the first start.
  • The proxy is on the wrong network. Caddy or nginx must be attached to remnawave-network, or the name remnawave won't resolve.
  • Missing proxy headers. Without X-Forwarded-For and X-Forwarded-Proto: https the backend drops connections and logs "Reverse proxy and HTTPS are required". nginx needs proxy_set_header X-Forwarded-Proto $scheme and TLS on nginx itself: behind Cloudflare in Flexible mode the scheme is http.
  • Panel under a sub-path. location /panel isn't supported — use a separate domain or subdomain.

Sources