Remnawave is a panel for managing VPN servers built on the Xray core. It makes it easy to create users, hand out subscriptions, limit traffic and expiry dates, and connect several servers (nodes) to one panel. The project is free, open source and has official documentation.
In this article we have put together the whole installation, from choosing F3 Cloud servers to connecting the first client. The commands come from the official documentation, and we went through the entire installation ourselves on panel version 3.4.5, so the button names and screenshots match what you will see. The firewall settings, backups and camouflage tips are our own additions.
What Remnawave consists of
- Panel – the web interface and API, a PostgreSQL database and a Valkey cache. The panel itself does not carry user traffic.
- Node – a lightweight container with the Xray core. Users connect to the nodes. The panel manages nodes over a secure connection.
- Subscription page – an optional component. It shows users a nice page with instructions and hides the panel address.
The installation order matters
Until at least one node is connected to the panel, users will not be able to connect. That is why the installation always follows the same order – panel, reverse proxy, node, profile, host and users.
What you need
Two servers
The developers recommend putting the panel and the node on different servers. On a single server, the panel and the node will compete for port 443.
And if the node's IP address gets blocked, you will lose access to the panel as well.
| Role | Remnawave requirements | Recommended F3 Cloud plan |
|---|---|---|
| Panel | 2 cores and 2 GB RAM or more, ideally 4 cores and 4 GB, 20 GB disk | DE-RZ9-2 (2 vCPU, 4 GB RAM, 60 GB NVMe) |
| Node | 1 core and 1 GB RAM or more | DE-Promo for personal use, DE-RZ9-1 for a 1 Gbit/s channel |
Install Ubuntu or Debian on both servers. How to order a server is described in the article on ordering a virtual server.
Domain
You will need two A records:
panel.your-domain.compoints to the IP address of the panel server;cdn.your-domain.compoints to the IP address of the node server.
A separate name for the node lets you change the server's IP address later without updating your users' subscriptions.
Choose neutral names
Do not use the words vpn, proxy, xray, vless, reality, remnawave, node or similar ones in the domain and subdomains. The server name is sent in plain text, and such words are easy to match against a list. It is better to use cdn.example.com, photos.example.com or the domain itself without a subdomain.
About Cloudflare
The Remnawave documentation suggests proxying the panel domain through Cloudflare if it works reliably in your region. Cloudflare services currently work intermittently from Russia, so we recommend leaving the proxy off (grey cloud).
For the node domain, the proxy must always be off, because Reality only works with a direct connection to the server.
1. Prepare both servers
Connect to each server over SSH, for example with Termius, and run the same commands.
# Update the system
apt update && apt upgrade -y
# Install Docker with the official script
curl -fsSL https://get.docker.com | sh
# Check that Docker and the Compose plugin work
docker --version && docker compose versionIf both commands showed their versions, Docker is ready.

Docker and Compose are installed
2. Install the panel
Run all the commands in this step on the panel server.
# Create a folder and download the official docker-compose.yml and .env files
mkdir /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.sampleGenerate the secret keys. The command writes random values into the .env file for you.
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)/" .envChange the database password. The developers strongly recommend not keeping the default one.
pw=$(openssl rand -hex 24) && sed -i "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$pw/" .env && sed -i "s|^\(DATABASE_URL=\"postgresql://postgres:\)[^\@]*\(@.*\)|\1$pw\2|" .envOpen the .env file with nano .env and enter your domain in three lines.
FRONT_END_DOMAIN=panel.your-domain.com
PANEL_DOMAIN=panel.your-domain.com
SUB_PUBLIC_DOMAIN=panel.your-domain.com/api/subDo not add https:// or a trailing slash to SUB_PUBLIC_DOMAIN. Save the file with Ctrl + O.
Close the editor with Ctrl + X.
Start the panel and watch the startup log.
docker compose up -d && docker compose logs -f -tWhen the Remnawave banner with the version number appears in the log, the panel has started. Leave the log view with Ctrl + C, and the containers will keep running.

The panel has started
Do not expose the services to the internet
All Remnawave services listen only on 127.0.0.1 and cannot be reached from the internet directly. Do not change this in docker-compose.yml. The panel must only be reached through the reverse proxy from the next step.
If you have changed .env
After any change to .env, recreate the containers with docker compose down && docker compose up -d rather than simply restarting them.
3. Set up Caddy and HTTPS
Remnawave supports Caddy, Nginx and Traefik. Caddy is the easiest to set up, because it obtains and renews the Let's Encrypt certificate by itself.
# Create a folder and a Caddy configuration file
mkdir -p /opt/remnawave/caddy && cd /opt/remnawave/caddy && nano CaddyfilePaste the settings below into the file, replacing the domain with your own.
https://panel.your-domain.com {
encode
reverse_proxy * http://remnawave:3000
}
:443 {
tls internal
respond 204
}The second block answers with an empty page to any request that is not for your domain, for example when someone scans by IP address.
Next to it, create a docker-compose.yml file for Caddy with nano docker-compose.yml and paste the following into it.
services:
caddy:
image: caddy:2.9
container_name: 'caddy'
hostname: caddy
restart: always
ports:
- '0.0.0.0:443:443'
- '0.0.0.0:80:80'
networks:
- remnawave-network
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy-ssl-data:/data
networks:
remnawave-network:
name: remnawave-network
driver: bridge
external: true
volumes:
caddy-ssl-data:
driver: local
external: false
name: caddy-ssl-dataStart Caddy.
docker compose up -d && docker compose logs -f -tAfter a minute, open https://panel.your-domain.com in a browser. You should see the panel registration page with a valid certificate.
4. Register the administrator
The first time you open the panel, it offers to create a super-admin account. The first registered user becomes the main administrator, so register right after Caddy is running.
Choose a username, then press Generate & copy so that the panel creates a strong password for you, and save it in a password manager straight away. Then press Sign up.

Registering the main administrator
If you have lost access to the panel
If you lose access to the panel, you can reset the administrator with the built-in recovery tool. Run docker exec -it remnawave cli on the panel server.
5. Connect the node
On the node server, create a folder for it.
mkdir /opt/remnanode && cd /opt/remnanodeIn the panel, open Nodes and press +. Fill in the form:
- Internal name – any name that makes sense to you, for example
DE-Frankfurt-1; - Country – the server's country, users will see its flag;
- Domain or IP – the node address, for example
cdn.your-domain.com; - Node Port – the port the panel uses to manage the node. The default is
2222.

Creating a node
Press Copy docker-compose.yml. The panel copies a ready-made file that looks roughly like this.
services:
remnanode:
container_name: remnanode
hostname: remnanode
image: remnawave/node:latest
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"On the node server, open the editor with nano docker-compose.yml, paste the copied content, save the file and start the node.
docker compose up -d && docker compose logs -f -tThe log will show a table with the API port, the Xray version and the server specifications. This means the node has started and is waiting for the panel.

The node has started
Go back to the panel and press Next. At the Core configuration step, press Select and tick the default profile Default-Profile for now. We will create the working profile in the next step. Press Create node. After a few seconds you will see the message “Connection successful. Xray Core is up and running!”.

The panel has connected to the node
Close the node port to outsiders
The node port is only needed by the panel. Allow connections to it only from the panel server's IP address. The commands are in the “Firewall” section below.
6. Create a VLESS Reality profile
By default the panel creates the Default-Profile profile with a single Shadowsocks inbound. To get around blocking, add a VLESS Reality profile.
- Open Config Profiles, press +, enter a profile name, for example
VLESS-Reality, and press Create. The profile editor will open. - Below the editor, next to the Format button, press the menu button and choose Load from GitHub.
- Choose the Example VLESS-TCP-REALITY config template and press Load template. It already contains the
VLESS_TCP_REALITYinbound on port 443.

Choosing a profile template
Right after loading, the panel highlights the error please fill in a valid value for "target". This is normal, because the template still needs a few values.

The template is loaded, only the Reality fields are left to fill in
The easiest way to generate the Reality keys is right on the node server.
# Create a Reality key pair. Paste the PrivateKey value into the privateKey field
docker exec remnanode xray x25519
# Create a value for the shortIds array
openssl rand -hex 8In the realitySettings block, fill in these fields:
privateKey– the PrivateKey value from the first command;shortIds– the value from the second command, inside the square brackets and quotes;targetandserverNames– the address of the camouflage site and its name, see the warning below for details.
The message Xray config is valid should appear at the bottom of the editor. Press Save.
Do not imitate big third-party sites
Do not put sites such as google.com, microsoft.com or apple.com in target and serverNames, because this is one of the main signs servers are being blocked by today. The most reliable option is to imitate your own domain with the self-steal scheme. To do this, install nginx with a site on your domain on the node server, set target to 127.0.0.1:8443, put your domain in serverNames and set xver: 0. Step-by-step instructions are in the article about self-steal for VLESS Reality.
Now all that is left is to enable the new inbound for the node and the users.
- In Nodes, open your node, press the edit button in the Core configuration block, tick the
VLESS-Realityprofile and save the node. - Open Users → Internal Squads, click
Default-Squad, tick theVLESS-Realityprofile and save. Without this, users will not receive the new inbound.

The VLESS-Reality profile is enabled in Default-Squad
7. Create a host
The host defines which address and which parameters users get in their subscription.
- Open Subscription → Hosts and press +.
- Turn on the Host visibility switch. It is off by default, and a hidden host does not get into the subscription.
- In the Remark field, enter a name users will understand, for example “Germany”.
- Click the No inbound selected block, tick
VLESS_TCP_REALITYand confirm your choice. The port fills in automatically. - In the Address field, enter
cdn.your-domain.com. - In the Options block, press Add, choose Fingerprint and set it to
firefox. - Press Save.

A host with the firefox fingerprint
Why firefox and not chrome
Since late 2025, the traffic filtering systems of Russian ISPs (TSPU) judge a TLS connection by several signs at once, namely the hosting subnet, the site name (SNI) and the client fingerprint. The chrome fingerprint is the default in almost every VPN app and panel, so the combination of “Chrome fingerprint and data-centre IP address” has become a reliable proxy marker for the filters. Users report that with several simultaneous connections using this fingerprint, the connection “freezes” for a few minutes, and switching to firefox helps.
The edge fingerprint used to help as well, but since September 2026 it has been incompatible with new Xray versions. Starting with Xray 26.9.8 (included, for example, in 3X-UI 3.8.0 and later), a Reality server only accepts clients with post-quantum ML-KEM key exchange. The edge profile in Xray corresponds to the old Edge 85 without ML-KEM, so a connection with it will not be established. The firefox profile supports ML-KEM if the app has Xray core 26.3.23 or newer, so keep your clients up to date. Do not use random or randomized either, because they may pick a profile without ML-KEM.
8. Create a user and a subscription
- Open Users and press +.
- Enter a Username without spaces. You will not be able to change it later.
- Set the Traffic limit, where
0means unlimited. - Check the Expiry date, because by default the panel sets a period of one day.
- In the Internal squads block, tick
Default-Squad. - Press Create.

Creating a user
Open the user you have created. Their card has a Subscription URL field that looks like https://panel.your-domain.com/api/sub/identifier, with a copy button next to it. This is the link to send to the user.

The user card and the subscription link
Connecting a client
Copy the subscription link from the user card and import it into the app, as shown in the import instructions. The subscription updates itself, so users do not need to set anything up again when the server changes.
Firewall
The official documentation has no firewall commands, so here is our UFW-based setup. If you have moved SSH to another port, use it instead of 22.
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable# Replace PANEL_IP with the IP address of the panel server
ufw allow 22/tcp
ufw allow 443/tcp
ufw allow from PANEL_IP to any port 2222 proto tcp
ufw enableOther protective measures are described in the article on protecting a server from unauthorised access.
Subscription page (optional)
A separate subscription page hides the panel address from users and shows them connection instructions. It can run on the same server as the panel.
- Add an A record for the subscription domain, for example
sub.your-domain.com, and set it in theSUB_PUBLIC_DOMAINvariable in/opt/remnawave/.env. Then recreate the panel container withcd /opt/remnawave && docker compose down remnawave && docker compose up -d. - Create a token in Remnawave Settings → API tokens.
- Create the
/opt/remnawave/subscriptionfolder with a.envfile in it.
APP_PORT=3010
REMNAWAVE_PANEL_URL=http://remnawave:3000
REMNAWAVE_API_TOKEN=token_from_the_panel- Create a
docker-compose.ymlfile in the same folder.
services:
remnawave-subscription-page:
image: remnawave/subscription-page:latest
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:
driver: bridge
external: true- Start the page with
docker compose up -dand add a new block to/opt/remnawave/caddy/Caddyfile.
https://sub.your-domain.com {
encode
reverse_proxy * http://remnawave-subscription-page:3010
}- Restart Caddy with
cd /opt/remnawave/caddy && docker compose down && docker compose up -d.
Updating
Update the panel first, then the nodes.
# On the panel server
cd /opt/remnawave && docker compose pull && docker compose down && docker compose up -d
docker image prune# On the node server
cd /opt/remnanode && docker compose pull && docker compose down && docker compose up -dIf you have installed the subscription page, update it too with the same command from the /opt/remnawave/subscription folder.
Backups
Remnawave has no official backup guide. It is enough to save the database and the configuration files, because the Valkey cache keeps nothing important on disk. This is how we do it.
cd /opt/remnawave
# Save the database to a file with today's date
docker exec remnawave-db pg_dump -U postgres postgres > backup-$(date +%F).sql
# Save the configuration files to an archive
tar czf remnawave-config-$(date +%F).tar.gz .env docker-compose.yml caddy/CaddyfileDownload the copies to your computer or to other storage, because a copy on the same server will not help if something happens to it. We cover backups in more detail in a separate article.
Common problems
The panel does not open
Check that the A record of the panel domain points to the server's IP address. Then look at the logs with docker compose logs in the /opt/remnawave and /opt/remnawave/caddy folders. If Caddy could not obtain a certificate, its log will show the reason.
The node does not connect to the panel
Make sure that port 2222 on the node server is open for the panel's IP address and that SECRET_KEY has been copied into docker-compose.yml in full. You can view the node log with docker compose logs in the /opt/remnanode folder.
The profile does not save, an error about target
The message please fill in a valid value for "target" means that the template fields in the realitySettings block have not been filled in. Fill in target, serverNames, privateKey and shortIds as described in step 6.
SPAWN_ERROR: xray error
Xray on the node could not start with this profile, most often because of a mistake in the settings or a port that is already in use. Check the Xray log with docker exec remnanode xlogs and fix the profile.
The user does not see the server in the subscription
Check three things. The profile is enabled in Default-Squad, the user belongs to this squad, and the Host visibility switch is on for the host. By default a new host is created with it turned off.
The connection works, but sites do not open
Check that serverNames, the keys and shortIds in the profile match what the client receives, and that the node's IP address is not blocked by the provider.
Our resources
Telegram channel: https://t.me/f3cloud_news
F3 Cloud: https://f3cloud.com