bolt Valebyte VPS from $4/mo — NVMe, 60s deploy.

Get a VPS arrow_forward
eco Beginner Tutorial/How-to

Remnawave: VLESS/Reality Control Panel for Multiple Servers

calendar_month Sep 12, 2026 schedule 21 min read visibility 17 views
Remnawave: панель управления VLESS/Reality для нескольких серверов
info

Need a server for this guide? We offer dedicated servers and VPS in 50+ countries with instant setup.

Need a server for this guide?

Deploy a VPS or dedicated server in minutes.

Remnawave: VLESS/Reality control panel for multiple servers

TL;DR

Remnawave lets you centrally manage users, subscriptions, and multiple Xray nodes with VLESS/Reality: the panel runs on one secured VPS, while exit servers connect as separate nodes. In this guide, you will deploy the panel in Docker, enable HTTPS through Caddy, add nodes, configure PostgreSQL backups, and verify that the entire setup works.

  • The Remnawave panel is best hosted on a separate management VPS with a permanent domain and HTTPS.
  • For a small team, 2 vCPU, 4 GB RAM, 40 GB NVMe, and a port starting from 100 Mbps are sufficient.
  • VLESS/Reality nodes can be located in different countries while remaining managed from a single panel.
  • Secrets, database passwords, and tokens should be stored in the .env file rather than Docker Compose files.
  • HTTPS is mandatory for the panel: Caddy will automatically issue and renew the TLS certificate.
  • The main backup data consists of PostgreSQL, the .env file, the Compose configuration, and panel data.

What we are configuring and why

Remnawave is a self-hosted control panel for Xray-based proxy infrastructure. It solves the problem that appears immediately after moving from one manually configured server to several: users, keys, limits, subscriptions, node settings, and statistics no longer fit into a single configuration file.

Instead of manually editing Xray JSON configurations, the administrator gets a web panel and API. Users are created there, subscription URLs are issued, traffic and expiration limits are assigned, and execution servers connect as nodes. The panel stores state in PostgreSQL, while nodes apply the received configuration and handle client connections.

This article uses a typical architecture with three roles:

  • Control plane: a VPS with Remnawave, PostgreSQL, Redis, and Caddy. It contains the panel, API, accounts, and metadata.
  • Edge nodes: VPS instances in one or more locations running the node agent and Xray Core.
  • Clients: applications supporting VLESS and subscriptions that receive the current configuration through a subscription URL.

VLESS is a modern credential protocol in the Xray ecosystem. Reality is used together with VLESS to provide secure transport without the need to issue a TLS certificate for every public node inbound port. However, the administrative panel must not be available over HTTP: HTTPS is required to protect passwords, tokens, and subscription links.

What will work after configuration

After completing all steps, you will have a panel domain, such as panel.example.com, protected by a TLS certificate. In the panel, you will create an administrator, add one or more nodes, create a test user, and obtain a subscription. Adding the next server will not require copying the database or manually synchronizing users: simply install the node agent and bind it with a token.

This approach is convenient for a development team, personal infrastructure with multiple regions, a small community, or a test environment. It also simplifies node replacement: if a server is unavailable or must be decommissioned, users can be reassigned to new nodes through the panel.

Self-hosted and cloud panels

Criterion Cloud managed panel Remnawave on your own VPS
Data control The user database is stored by a third-party service The PostgreSQL database and secrets are stored on your servers
Updates Performed by the service operator Performed by you on a schedule and after a backup
Node flexibility Limited by platform capabilities You can add your own VPS instances, regions, and access rules
Initial setup Minimal Requires Linux, DNS, Docker, and basic security knowledge
Risks Dependence on another party's account and service policy Responsibility for patches, backups, and server protection

The self-hosted option is justified when control, independence from an external panel, custom data retention rules, and the ability to use nodes from different hosting providers are important. The trade-off is the need to monitor updates and maintain a working recovery plan.

Use the infrastructure only in accordance with the laws of the country where the servers are hosted, provider terms, and the rules of the networks through which traffic passes. Do not publish the administrative panel without HTTPS, and do not leave the API accessible without authentication.

What VPS configuration is needed for this task

The load on the Remnawave panel and the load on exit nodes differ. The panel stores users, statistics, settings, and API state; it usually benefits more from stable storage and RAM headroom for PostgreSQL. Nodes need network bandwidth, routing quality, CPU for encryption, and a sufficient traffic allowance.

Minimum resources for the panel

Scenario CPU RAM Storage Network
Testing, up to 20 users, 1–2 nodes 1 vCPU 2 GB 25 GB NVMe 100 Mbps
Production minimum, up to 100 users, 3–5 nodes 2 vCPU 4 GB 40 GB NVMe 100–1000 Mbps
Several hundred users, detailed statistics 4 vCPU 8 GB 80 GB NVMe 1 Gbps

For the management server, a practical starting option is 2 vCPU, 4 GB RAM, 40 GB NVMe, and a 1 Gbps network. This headroom allows Docker, PostgreSQL, Redis, Caddy, and the panel to run simultaneously without encountering OOM issues during updates or database migrations. As one neutral option, you can choose a VPS with the specified characteristics, but it is more important to check the traffic allowance, IPv4 availability, and the ability to open TCP ports 80 and 443.

Resources for one VLESS/Reality node

For a node with dozens of active users, 1–2 vCPU and 1–2 GB RAM are usually sufficient if no other heavy services run on the server. With high aggregate speeds, a large number of simultaneous connections, or intensive video traffic, choose 2–4 vCPU, 2–4 GB RAM, and a 1 Gbps port. Xray usually does not require much storage: 20 GB is enough if logs are rotated and there is no local statistics archive on the server.

Do not assess a server solely by the number of registered users. The number of simultaneous clients, average speed, and monthly traffic volume are more critical. For example, 30 users who connect occasionally may consume fewer resources than five regular users with intensive downloads.

When you need dedicated instead of VPS

A dedicated server makes sense when a node is consistently loaded close to port capacity, predictable CPU performance is required, 5–10 Gbps is needed, a higher traffic allowance is necessary, or a guaranteed network profile is required. It is also appropriate for a large node with several hundred simultaneous clients.

A dedicated server is usually unnecessary for the panel: the database and API consume significantly fewer resources than user traffic transfer. A rational setup is a small, reliable panel on a separate VPS and more powerful nodes where justified by statistics.

How to choose a location

The panel location affects latency when managing nodes, but has almost no effect on user speed: useful traffic goes through edge nodes. Choose a region for the panel with stable access to your nodes and predictable domain support.

Node locations are selected based on latency to users, route quality, traffic allowance, acceptable-use policy, and IPv4 availability. Do not place all nodes with one provider and in one country if fault tolerance is important to you. A minimally sensible geographic setup is a panel in one region and two nodes in different data centers.

Server preparation

The following assumes a clean VPS running Ubuntu Server 24.04 LTS x86_64. As of 2026, this is a convenient LTS base for Docker infrastructure: it receives security updates, includes a current kernel, and is well supported by most images. If you use Debian 12 or 13, the step logic is the same, but package names may differ slightly.

First, log in to the server as root using the password provided by the provider. Immediately create a separate administrator account: working permanently as root increases the consequences of a command error or an SSH key compromise.

Creating a user and SSH key access

adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
nano /home/deploy/.ssh/authorized_keys
chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh

Paste the contents of your public SSH key into authorized_keys, for example, a line beginning with ssh-ed25519. Test the login in a second terminal window without closing the current root session:

ssh deploy@SERVER_IP

Only disable root password authentication after a successful login. Open the SSH configuration:

sudo nano /etc/ssh/sshd_config.d/99-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
X11Forwarding no
MaxAuthTries 3

Check the syntax and restart SSH. If the validation command returns an error, do not restart the service until the file is fixed.

sudo sshd -t && sudo systemctl restart ssh

System updates and basic tools

Update the system before installing Docker. After a kernel update, reboot the VPS if the system reports that it is required.

sudo apt update && sudo apt full-upgrade -y
sudo apt install -y ca-certificates curl gnupg git jq nano unzip \
  ufw fail2ban chrony logrotate
sudo reboot

After rebooting, reconnect as the deploy user. Check time synchronization: accurate time is necessary for TLS certificates, tokens, and logs.

timedatectl status
chronyc tracking

Firewall and Fail2ban

On the panel server, you need to open SSH, HTTP, and HTTPS. Do not expose PostgreSQL, Redis, or internal Docker ports. If you change the SSH port, first open the new port and ensure that you can connect.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'SSH'
sudo ufw allow 80/tcp comment 'HTTP for ACME'
sudo ufw allow 443/tcp comment 'HTTPS panel'
sudo ufw enable
sudo ufw status verbose

Fail2ban on Ubuntu already includes a basic SSH filter. Create a local configuration with a reasonable ban time:

sudo tee /etc/fail2ban/jail.d/sshd.local >/dev/null <<'EOF'
[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h
EOF

sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

If the panel will accept connections from nodes over a separate API port, do not open it to the entire internet. Either use the panel's secured HTTPS address and a node token, or add specific UFW rules using node IP addresses. The exact mechanism depends on the Remnawave version and the selected connection mode.

Software Installation — Step by Step

As of 2026, a practical way to run Remnawave is Docker Engine and the Docker Compose Plugin. Containerization isolates PostgreSQL, Redis, the panel, and the reverse proxy, while updates are reduced to downloading new images and restarting the Compose stack.

Docker Engine 28.x and Docker Compose v2.x are used below. Exact minor versions change regularly, so after installation be sure to check the actual versions with docker version and docker compose version. For production, do not pin critical services to unsupported latest images without a testing process.

Installing Docker Engine

Add the official Docker repository for Ubuntu 24.04 and install the engine, CLI, Buildx, and Compose Plugin.

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

Allow the deploy user to manage Docker without sudo and reconnect to the SSH session. This change effectively gives the user administrative privileges on the server, so do not add regular users to the docker group.

sudo usermod -aG docker deploy
exit
ssh deploy@SERVER_IP
docker version
docker compose version
docker run --rm hello-world

Creating the Project Directory

Keep all panel files in one directory with predictable permissions. This example uses /opt/remnawave. The owner will be the deploy user, and only that user will have access to the secrets file.

sudo mkdir -p /opt/remnawave/{data,backups,caddy}
sudo chown -R deploy:deploy /opt/remnawave
cd /opt/remnawave
umask 077

Obtaining the Official Project Configuration

Remnawave is actively developed, so variable names and the set of containers may change between releases. Before deployment, use the official project repository and its .env.example file as the source of truth. Do not copy Compose files from random Telegram messages or old videos.

cd /opt
sudo git clone https://github.com/remnawave/backend.git remnawave-source
sudo chown -R deploy:deploy /opt/remnawave-source
cd /opt/remnawave-source
git tag --sort=-version:refname | head -n 10

Select the latest stable release tag, not a random development branch. In the example below, the variable is set manually: replace the value with a tag displayed by the previous command. This approach enables reproducible updates and rollbacks.

export REMNAWAVE_VERSION="v0.0.0"
git checkout "$REMNAWAVE_VERSION"
find . -maxdepth 3 -type f \( -name 'compose.yml' -o -name '.env.example' \) -print

Do not use the value v0.0.0 literally: it is a placeholder. Substitute an existing stable tag. If the release repository provides an official install script, first read its contents with less, then use it only if you understand the files and containers it creates.

Preparing Compose Files

Copy the official example Compose configuration and the variables example to the working directory. File names may differ between versions; a safe general action template is shown below.

cd /opt/remnawave-source
cp .env.example /opt/remnawave/.env
find . -maxdepth 3 -iname 'compose.yml' -print

If the repository file is named, for example, docker-compose.yml, copy it:

cp docker-compose.yml /opt/remnawave/compose.yml
cd /opt/remnawave
chmod 600 .env
nano .env

Before starting, check which services and images are declared. The command does not start containers; it only expands variables and validates YAML.

docker compose --env-file .env -f compose.yml config > /tmp/remnawave-resolved.yml
less /tmp/remnawave-resolved.yml

In production, the panel must have persistent volumes for PostgreSQL, Redis if required, and the application's own data. A PostgreSQL container without a volume will survive a restart but lose all data after recreation—this is one of the most dangerous mistakes during initial deployment.

Remnawave, Node, and HTTPS Configuration

This section configures the panel environment, Caddy as a reverse proxy, and node connection logic. Take the exact Remnawave variable names from the .env.example of the selected release. Do not add nonexistent parameters to the file: Docker Compose will ignore some of them, giving you a false impression that the configuration has been applied.

DNS Before Launching HTTPS

Create an A DNS record for the panel domain, for example panel.example.com, pointing to the public IPv4 address of the management VPS. If IPv6 is available, add a correct AAAA record or do not create one: incorrect IPv6 often interferes with certificate issuance.

Check resolution from your local computer and from the server itself:

dig +short A panel.example.com
curl -4 ifconfig.me
getent ahostsv4 panel.example.com

The DNS address must match the server's public IP. Also make sure that TCP ports 80 and 443 are not blocked by the provider's external firewall.

.env Secrets File

Generate cryptographically strong values. Do not use your email password, domain name, date of birth, or short strings. Save the results in a password manager: without them, restoring the panel from a backup may be impossible.

openssl rand -hex 32
openssl rand -base64 48
openssl rand -hex 24

An example structure is shown below. Names such as POSTGRES_PASSWORD, REDIS_PASSWORD, JWT_SECRET, and APP_URL are typical for Docker applications, but before starting, map them to the variables in the official file of the selected Remnawave release.

# Public panel URL
APP_URL=https://panel.example.com

# PostgreSQL
POSTGRES_DB=remnawave
POSTGRES_USER=remnawave
POSTGRES_PASSWORD=REPLACE_WITH_LONG_RANDOM_PASSWORD

# Redis, if included in the release compose file
REDIS_PASSWORD=REPLACE_WITH_ANOTHER_LONG_RANDOM_PASSWORD

# Application secrets
JWT_SECRET=REPLACE_WITH_64_OR_MORE_RANDOM_CHARACTERS
ENCRYPTION_KEY=REPLACE_WITH_RANDOM_KEY

# Initial administrator, if variables are supported by the release
[email protected]
ADMIN_PASSWORD=REPLACE_WITH_UNIQUE_LONG_PASSWORD

# Log time zone
TZ=UTC

Make sure the file is not committed to Git. If you initialize a local repository for infrastructure, add .env to .gitignore. Permissions must remain restricted:

cd /opt/remnawave
chmod 600 .env
ls -l .env

Caddy for TLS and Reverse Proxy

Do not expose the backend container port directly to the internet. Bind the backend only to the Docker network or to 127.0.0.1, and expose Caddy externally on ports 80 and 443. Caddy automatically obtains a certificate from Let’s Encrypt or another supported ACME authority and renews it.

Create the /opt/remnawave/Caddyfile file. In the reverse_proxy line, specify the service name and port from your actual Compose file. This is often backend:3000, app:3000, or another internal service.

cd /opt/remnawave
nano Caddyfile
{
    email [email protected]
}

panel.example.com {
    encode zstd gzip

    reverse_proxy backend:3000

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server
    }

    log {
        output stdout
        format json
    }
}

Add Caddy as a separate service to the Compose file if the official template does not include a reverse proxy. The caddy and backend containers must be on the same Docker network. The example below demonstrates the working principle; merge it with the services from the official compose file without creating a second PostgreSQL or Redis instance.

services:
  caddy:
    image: caddy:2.10-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./data/caddy/data:/data
      - ./data/caddy/config:/config
    networks:
      - remnawave

networks:
  remnawave:
    name: remnawave

If the network in your Compose file already has a different name, use it. After merging the configuration, check the final YAML again.

docker compose --env-file .env -f compose.yml config > /tmp/remnawave-final.yml
docker compose --env-file .env -f compose.yml up -d
docker compose ps

The initial startup may take several minutes: images are downloaded, PostgreSQL starts, and database migrations are performed. Monitor the logs until application readiness messages appear.

docker compose logs -f --tail=100

Verifying the Panel and TLS

Check the HTTP redirect, HTTPS, and certificate. If the application has a dedicated status endpoint, use it; common options are /health, /api/health, or /healthz. The exact route depends on the panel version.

curl -I http://panel.example.com
curl -I https://panel.example.com
curl -sS https://panel.example.com/health || true
openssl s_client -connect panel.example.com:443 -servername panel.example.com < /dev/null 2>/dev/null | \
  openssl x509 -noout -issuer -subject -dates

Open https://panel.example.com in a browser. If the initial administrator is not created through environment variables, use the standard bootstrap command from the release documentation or create an account through the initial setup wizard. Do not leave default or temporary credentials in place.

Adding the First Node

A node should run on a separate server or, for testing, on the same VPS. For production, it is better to separate the control plane and edge node: under high network load, the node will not affect PostgreSQL and the management interface.

In the web panel, open the nodes section, create a node with a clear name, for example de-fra-01, and specify its public address. The panel usually creates a registration token or provides connection parameters. Save the token once: treat it as a password because it allows the node to register in your infrastructure.

On the node VPS, repeat the basic hardening from the preparation section: updates, a user with an SSH key, UFW, and Fail2ban. Then install Docker in the same way. The official node package and registration method may differ between Remnawave releases, but the pattern is always the same: the node receives the panel URL, a unique token, and starts as a container.

sudo mkdir -p /opt/remnawave-node
sudo chown -R deploy:deploy /opt/remnawave-node
cd /opt/remnawave-node
nano .env
PANEL_URL=https://panel.example.com
NODE_TOKEN=REPLACE_WITH_TOKEN_CREATED_IN_PANEL
TZ=UTC

Use the node Docker Compose file specifically from the official repository and the same compatible release as the backend. A typical startup sequence looks like this:

cd /opt/remnawave-node
docker compose --env-file .env -f compose.yml pull
docker compose --env-file .env -f compose.yml up -d
docker compose ps
docker compose logs -f --tail=100

After connecting, return to the panel: the node should appear as online. Only then create an inbound VLESS/Reality and assign it to the node. For Reality, the panel usually generates keys, a short ID, and destination parameters. Do not reuse the same Reality private key across independent inbounds unless necessary; use panel-generated values and protect access to them as secrets.

Test User and Subscription

Create a test user with a small limit, for example 1 GB and a validity period of 24 hours. Assign the created inbound and node to the user. Then copy the subscription URL into a supported client and refresh the subscription.

Check not only that the import succeeds, but also the full path: client connection, the online user appearing in the panel, traffic counter activity, and the absence of errors in node logs. To monitor containers on the node, use:

docker compose ps
docker stats --no-stream
docker compose logs --tail=200
ss -lntup
sudo ufw status numbered

Open on the node only the TCP and UDP ports assigned by the inbound configuration. The panel port is usually not needed on the node. If Remnawave uses an outgoing secure connection from the node to the panel, no inbound rule for the service channel will be required on the node at all.

Backups and Maintenance

A panel backup is not a copy of a single Docker container. Critical data is stored in PostgreSQL, while the ability to decrypt or connect the system after recovery depends on the .env file, application keys, and Compose configuration. A backup without secrets may be useless.

What to Save

  • PostgreSQL logical dump: users, nodes, settings, statistics, and subscriptions.
  • The /opt/remnawave/.env file with passwords and application keys.
  • compose.yml, Caddyfile, and additional configuration files.
  • Caddy data from /opt/remnawave/data/caddy: certificates can be reissued, but backing them up speeds up recovery.
  • A document with DNS records, the list of nodes, open ports, and the Remnawave version.

Do not rely on a VPS snapshot as your only backup. A snapshot is useful before a major update, but it is stored with the same provider and does not replace regular copies kept elsewhere.

Local PostgreSQL Dump

First, find the exact PostgreSQL service name in Compose. In this example, it is called postgres. The command creates a compressed dump in the backup directory.

cd /opt/remnawave
docker compose ps
mkdir -p backups
docker compose exec -T postgres pg_dump \
  -U "$POSTGRES_USER" \
  -d "$POSTGRES_DB" \
  --format=custom \
  | gzip > "backups/remnawave-$(date +%F-%H%M).dump.gz"

Verify that the file is not empty. For the initial check, it is useful to perform a restore on a separate test server rather than simply confirming that the archive was created.

ls -lh backups/
gzip -t backups/remnawave-.dump.gz

Automated Backup with restic

Restic supports client-side encryption and S3-compatible storage. This is convenient because external storage receives already encrypted data. Create a separate bucket and separate access keys exclusively for backups; do not use keys from the main cloud account with full permissions.

Install restic:

sudo apt install -y restic
sudo install -d -m 700 /root/.config/restic
sudo nano /root/.config/restic/remnawave.env
sudo chmod 600 /root/.config/restic/remnawave.env

Do not store the environment file in Git, and fill it with the actual values for your S3-compatible storage:

export RESTIC_REPOSITORY="s3:https://s3.example.net/remnawave-backups"
export RESTIC_PASSWORD="REPLACE_WITH_LONG_UNIQUE_BACKUP_PASSWORD"
export AWS_ACCESS_KEY_ID="REPLACE_WITH_BACKUP_ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="REPLACE_WITH_BACKUP_SECRET_KEY"

Initialize the repository once. The RESTIC_PASSWORD password must be stored separately from the VPS: if it is lost, the archives cannot be decrypted.

sudo bash -c 'source /root/.config/restic/remnawave.env && restic init'

Create a script that dumps the database, archives the configuration, sends the data to restic, and applies the retention policy.

sudo nano /usr/local/sbin/backup-remnawave.sh
sudo chmod 700 /usr/local/sbin/backup-remnawave.sh
#!/usr/bin/env bash
set -euo pipefail

APP_DIR="/opt/remnawave"
BACKUP_DIR="${APP_DIR}/backups"
STAMP="$(date +%F-%H%M%S)"

source /root/.config/restic/remnawave.env

mkdir -p "${BACKUP_DIR}"

cd "${APP_DIR}"

docker compose exec -T postgres pg_dump \
  -U "${POSTGRES_USER}" \
  -d "${POSTGRES_DB}" \
  --format=custom | gzip > "${BACKUP_DIR}/postgres-${STAMP}.dump.gz"

tar -C "${APP_DIR}" -czf "${BACKUP_DIR}/config-${STAMP}.tar.gz" \
  .env compose.yml Caddyfile data/caddy 2>/dev/null || true

restic backup "${BACKUP_DIR}" --tag remnawave --tag postgres
restic forget --prune \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6

find "${BACKUP_DIR}" -type f -mtime +3 -delete

Run the script daily during a low-load period. Cron is convenient for small infrastructure:

sudo crontab -e
17 3   * /usr/local/sbin/backup-remnawave.sh >> /var/log/backup-remnawave.log 2>&1

Test a manual run and inspect the repository contents:

sudo /usr/local/sbin/backup-remnawave.sh
sudo bash -c 'source /root/.config/restic/remnawave.env && restic snapshots'
sudo tail -n 100 /var/log/backup-remnawave.log

Updates Without Data Loss

For a single panel, it is better to use a maintenance window. A backend update may include a database migration, so first create a verified backup, read the release notes, and record the current version. Do not update the backend and all nodes simultaneously unless you have verified release compatibility.

cd /opt/remnawave
sudo /usr/local/sbin/backup-remnawave.sh
docker compose ps
docker compose pull
docker compose up -d
docker compose logs --tail=100
docker compose ps

For multiple nodes, use a rolling approach: update the panel, then one lightly loaded node, verify creation and connection of a test user, then update the remaining nodes one at a time. Before updating a node, temporarily exclude it from issuing new configurations or wait for the number of active sessions to decrease.

Once a month, check disk space, Docker status, PostgreSQL size, and TLS certificate expiration:

df -h
docker system df
docker compose exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
  -c "SELECT pg_size_pretty(pg_database_size(current_database()));"
sudo fail2ban-client status sshd
docker compose logs caddy --tail=50

Troubleshooting + FAQ

Why is Caddy not obtaining a certificate and showing an ACME error in the logs?

First, check DNS: the panel domain's A record must point to the current public IPv4 address of the VPS. Then make sure ports 80 and 443 are open both in UFW and in the provider's external firewall. The error often occurs due to an old AAAA record: the ACME check tries to use IPv6, which is not configured on the server. Check dig A and dig AAAA, then view docker compose logs caddy.

The panel opens by IP address but does not work by domain. What should I check?

Check whether APP_URL matches the actual HTTPS domain, and restart Compose after changing .env. In the Caddyfile, the domain must be specified without the https:// scheme and without an extra path. Make sure the reverse proxy points to the correct backend service name and its internal port. The docker compose config command will help you view the final configuration and detect unresolved environment variables.

The backend container keeps restarting. How can I find the cause?

Start with docker compose logs --tail=200 backend. The most common causes are an incorrect PostgreSQL password, an incomplete database startup, a missing required environment variable, or a migration incompatible with the old database schema. Check the database status with docker compose ps and the PostgreSQL service logs. Do not delete volumes in an attempt to “fix” startup: this will delete users and settings. First save a database dump and compare .env with the official example for your version.

The node was created, but it remains offline in the panel. What should I do?

Check the panel URL in the node variable: it must start with https:// and use a domain with a valid certificate. Make sure the registration token was copied without spaces and belongs specifically to this node. View the node container logs and check outbound access from the node to the panel on TCP 443 with curl -I https://panel.example.com. If strict firewall rules exist between the servers, allow outbound HTTPS connections from the node and inbound port 443 on the panel.

The user imported a subscription, but the connection is not established. What should I check?

Check whether the user has an active inbound and an online node assigned. Then make sure the inbound port is open in UFW on the node and is not used by another service: use ss -lntup. View the node logs during the connection attempt; they will show whether traffic reached Xray. Also check the user expiration date, traffic limit, and correct system time. Do not manually change UUIDs, Reality keys, or inbound parameters in the node container: the panel must remain the configuration source.

What is the minimum suitable VPS configuration?

For a test panel with one or two nodes, a VPS with 1 vCPU, 2 GB RAM, 25 GB SSD or NVMe, and a network connection of at least 100 Mbps is sufficient. For real-world operation, it is better to choose 2 vCPU, 4 GB RAM, and 40 GB NVMe from the start, because PostgreSQL, Docker, and updates consume additional memory. Each small node needs only 1 vCPU, 1–2 GB RAM, and 20 GB of disk space, but choose bandwidth and the monthly traffic limit according to actual usage.

Should I choose a VPS or dedicated server for this task?

A VPS is almost always sufficient for the management panel: it does not carry the main user traffic and is usually limited to database, API, and statistics workloads. A VPS is also suitable for small or medium nodes with dozens of active clients. Choose dedicated hardware for a large edge node with sustained high load, a requirement for 1–10 Gbps, a high traffic volume, or predictable CPU performance. A practical strategy is to host the panel on a VPS and add dedicated servers only after metrics confirm the load.

Can I keep the panel and the first node on the same server?

For a lab environment or personal use, this is acceptable and reduces cost. However, such a server becomes a single point of failure: if there are network, load, or update issues, both the panel and user connections will be unavailable at the same time. In addition, high throughput on the node can slow down PostgreSQL and the web interface. For production, it is better to separate the control plane from the edge node across at least two VPS instances. This also simplifies migration and diagnostics.

How do I restore the panel after losing a VPS?

Create a new VPS, install Docker, and deploy the same compatible Remnawave release. Restore .env, the Compose files, and then PostgreSQL from the dump. It is important to use the same application secrets; otherwise, some encrypted data may become inaccessible. After recovery, switch DNS to the new IP, wait for the TLS certificate to be issued, and verify node connectivity. Regularly test this procedure on a separate machine: a backup is considered valid only after a successful restore.

Conclusions and Next Steps

You now have a basic self-hosted Remnawave infrastructure: a secured panel with HTTPS, PostgreSQL, backups, and the ability to connect multiple VLESS/Reality nodes. The main operational principle is to separate the management panel from exit servers and not store secrets in plain text.

  1. Add a second node in another data center and verify that users receive an up-to-date subscription after changing the assignment.
  2. Set up external monitoring for panel availability, TLS certificate expiration, disk usage, and node status.
  3. Perform a test restore of PostgreSQL and the configuration on a separate VPS before the first major update.

As the load grows, collect actual metrics for concurrent connections, traffic, CPU, and RAM. Based on them, scale the nodes rather than the management panel unless necessary.

Was this guide helpful?

Your feedback helps us improve our guides.

Share this post:

Send this guide to someone who may find it useful.

Telegram VKVK WhatsApp Facebook LinkedIn XX

remnawave: vless/reality control panel for multiple servers
support_agent
Valebyte Support
Usually replies within minutes
Hi there!
Send us a message and we'll reply as soon as possible.