Skip to content

Step-by-step guide — Part 1: Install

Part 1 of the step-by-step guide: installing TWS Panel on a clean VPS, from scratch all the way to the setup wizard. The README has the short version; this guide explains every step, what shows up on screen, and what to do when something goes wrong.

The steps are numbered 1 to 8. The panel’s scripts (install.sh, uninstall.sh) refer to these numbers.

StepWhereWhat it does
1ProviderGet a VPS
2Your computer → VPSSign in as root and check the system
3VPSCreate your non-root user
4Your computerGenerate the SSH key and install it on the VPS
5VPSInstall git
6VPSClone the repository into /opt/tws-panel
7VPSRun the installer
8BrowserOpen the panel and follow the wizard

After Step 8, the wizard’s Security step has its own guide: Security wizard. Then Part 2 continues the setup.

A VPS (virtual server) with:

  • Ubuntu 22.04 or 24.04 LTS, clean (no other software installed). When creating it, prefer 24.04.
  • Recommended minimum: 1 vCPU, 2 GB of RAM, and 25 GB of disk.
  • Ports 80 and 443 open to the internet. The HTTPS certificate depends on them. If your provider has a firewall in its hosting dashboard, open both.
  • For email: port 25 unblocked by the provider. Many providers block it, especially on new accounts. You check this in Step 4 of Part 2.

You will need two things the provider gives you: the VPS IP and the root password. Before going on, look in the provider’s dashboard for “Console”, “VNC”, “Rescue”, or “Recovery mode” and confirm it exists: it is your way back in if you lose SSH access. See I lost access.

Without a domain, everything works with automatic *.sslip.io addresses (for example, https://203-0-113-10.sslip.io). With a domain of your own, you get:

  • your own address for the panel (panel.example.com), instead of one with the IP in the name;
  • professional email, which requires a domain;
  • your own addresses for your projects.

You will create records in the domain’s DNS (at the provider where it is registered, or on Cloudflare). The examples in Part 2 use Cloudflare. You can leave the domain for later: nothing in Part 1 depends on it.

AccountWhat forRequired?
GitHubPublish projects from a repository. A private repository needs a read-only token (the screen teaches you how to generate one).Only if your code is on GitHub
CloudflareR2, the external storage for backups (the free plan covers 10 GB per month; it asks for a card). It also works as the domain’s DNS.Recommended
TelegramReceive the panel’s notices on your phone.Optional

None of them is used in Part 1; you only need them in Part 2.

⚠️ Got an error when connecting: "REMOTE HOST IDENTIFICATION HAS CHANGED!"

If, when trying to connect, you saw this in big capital letters and the connection was refused:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
...
Host key verification failed.

What happened. SSH keeps the “fingerprint” of every server you have signed in to. The fingerprint the server just presented is different from the one stored on your computer — and SSH would rather refuse the connection than connect you to a server that may not be yours.

When this is expected: you reinstalled the VPS operating system, switched VPS, or the provider reassigned that same IP to another machine. In those cases the server really is a different one, and the old fingerprint is no longer valid.

The fix — delete the old fingerprint for that IP and connect again. Run on your computer:

Terminal window
ssh-keygen -R YOUR_IP
ssh root@YOUR_IP

(YOUR_IP is a placeholder: replace it with your VPS IP.)

It will ask whether you trust the new identity — answer yes. That’s it, the connection goes back to normal.

🔄 Already had the panel installed and want to start over?

Before reinstalling the whole machine, check whether that is really your case — almost always it isn’t.

I just want to redo the setup wizard. You don’t need to reinstall anything. On the VPS:

Terminal window
cd /opt/tws-panel
sudo ./scripts/reset-setup.sh # the wizard goes back to step 0
sudo ./scripts/show-token.sh # shows the setup token again

Your projects, domains, email and security history stay intact. Use --full on the first command if you also want to delete the panel’s administrator account.

I want to remove the panel and install it again, without reinstalling the system. Useful for testing the installation as many times as you like. On the VPS:

Terminal window
cd /opt/tws-panel
./scripts/uninstall.sh --dry-run # only shows what would be deleted; changes nothing
./scripts/uninstall.sh # shows it again and asks you to type "remover"

(The script still prints in Portuguese: to confirm, type the word remover, which means “remove”.)

It deletes everything the panel created: the panel and its containers, the proxy (Caddy), the email server, the deployed projects with their data, the /opt/tws-projects folder, the hardening scripts copied to the VPS and, last of all, the /opt/tws-panel folder itself. Before deleting, it lists exactly what it found. Afterwards, move to another folder (cd ~) and start over from Step 6 (cloning the repository) — the git from Step 5 is still installed. Want to keep the repository folder? Use --keep-repo and go straight to Step 7.

I want a truly clean machine. In that case you do need to reinstall the operating system, and it happens in two places:

1. In your VPS provider’s panel. Look for “Reinstall”, “Reimage” or similar — the name varies by provider. Choose Ubuntu 24.04 LTS and write down the new root password it generates; the old one stops working.

2. On your computer. Since the system is a different one, the server now identifies itself with a different key, and your next connection will be refused with the warning REMOTE HOST IDENTIFICATION HAS CHANGED!. This is expected, and the fix is in the block right above:

⬆️ Got an error when connecting: “REMOTE HOST IDENTIFICATION HAS CHANGED!”

After that, just follow from Step 1 onwards, as with a new VPS — because now it is one.

Get a VPS running Ubuntu 24.04 LTS (recommended minimum: 1 vCPU / 2 GB RAM / 25 GB disk), as described in Before you start.

Sign in to the VPS and confirm the OS version:

Terminal window
ssh root@YOUR_IP
cat /etc/os-release # expected: PRETTY_NAME="Ubuntu 24.04.x LTS" (or 22.04)
🔐 First time connecting via SSH? What shows up and what to answer

On the first connection to any new server, SSH shows this warning:

The authenticity of host '203.0.113.10 (203.0.113.10)' can't be established.
ED25519 key fingerprint is SHA256:Xk9vN2mQpL7dR4wT8yB3cF6hJ1uA5sE0gH9zK2xW4vM.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

What to answer: type yes and press Enter.

What is this? The fingerprint is the server’s “digital signature” — it is how your computer recognizes the VPS on later connections. The warning appears only the first time; after that the fingerprint is saved and the connection is direct.

Common errors:

  • “I type the password and nothing shows up” — that’s normal! The terminal shows no characters (not even *) while you type passwords. Type calmly and press Enter.
  • Permission denied (publickey,password) — wrong password, or the VPS provider requires an SSH key. Check the password in the provider’s panel and try again.
  • The fingerprint warning shows up again after reinstalling the VPS — normal, the machine changed. Remove the old entry with ssh-keygen -R YOUR_IP and connect again.

This is the user who will operate the VPS from now on:

Terminal window
adduser YOUR_USER # replace YOUR_USER with any name you like; you choose the password on the spot
usermod -aG sudo YOUR_USER # grants administrator permission (sudo)
# --- Mistyped the password, gave up midway or want to start over? Choose ONE of the two: ---
passwd YOUR_USER # sets/resets only the password, keeping the user
deluser --remove-home YOUR_USER # deletes the user and their folder, so you can create everything again

(YOUR_USER is a placeholder for the username you choose.)

👤 Stuck on adduser? What shows up and what to fill in

When you run adduser YOUR_USER, the system asks a series of questions. In the examples below we use “kelvin” as an example name — in your case the username you chose will appear. This is how it looks:

Adding user `kelvin' ...
Adding new group `kelvin' (1000) ...
Adding new user `kelvin' (1000) with group `kelvin' ...
Creating home directory `/home/kelvin' ...
Copying files from `/etc/skel' ...
New password:
Retype new password:
passwd: password updated successfully
Changing the user information for kelvin
Enter the new value, or press ENTER for the default
Full Name []:
Room Number []:
Work Phone []:
Home Phone []:
Other []:
Is the information correct? [Y/n]

What to fill in, step by step:

  1. New password: — create a strong password that you will remember (it is your user’s password, used for SSH and for sudo). Note: nothing shows while you type — not even *. That’s normal, the password is being recorded. Type it and press Enter.
  2. Retype new password: — repeat the same password.
  3. Full Name, Room Number, Work Phone, Home Phone, Other — optional data. You can leave everything blank: just press Enter on each one.
  4. Is the information correct? [Y/n] — type Y (or just Enter) to confirm.

Common errors:

  • Sorry, passwords do not match followed by Try again? [y/N] — the two passwords you typed were different. Answer y to type them again. Beware of the capital N: it is the default, so just pressing Enter is the same as answering n.

    What if I already answered n? Then adduser gives up on the password but keeps going and creates the user — you will see passwd: password unchanged and, at the end, Adding new user .... The user exists, but has no password, and cannot sign in over SSH. Nothing is lost: set the password now, without recreating anything.

    Terminal window
    passwd YOUR_USER

    If you prefer to start from zero, delete the user and run adduser again:

    Terminal window
    deluser --remove-home YOUR_USER
  • I typed n (or anything) at Room Number / Work Phone — these are purely cosmetic fields, they affect neither login nor permissions. If you want to clear them: chfn YOUR_USER and press Enter on each field.

  • BAD PASSWORD: ... — weak password warning. The system accepts it, but prefer a long one (a phrase with words + numbers, e.g.: horse-battery-42-window).

  • “I think I mistyped because I saw nothing” — no problem: if you got it wrong, adduser complains (Sorry, try again.) and asks again.

Check that it worked before moving on:

Terminal window
id YOUR_USER

sudo must appear in the list of groups. If it doesn’t, the second command didn’t take.

Now end the root session. We are not reconnecting yet — the next step happens on your computer, and exit already leaves you there:

Terminal window
exit # leaves root and closes the SSH connection

You are back at your computer’s terminal. Step 4 continues from there.

🤔 What about su - YOUR_USER, won't that do?

It works, and it’s faster — but it has a side effect worth knowing about.

With su - YOUR_USER you stay inside the root session — just with another identity on top. An exit returns you to root instead of ending access, and it is easy to forget that that terminal still has a root session open underneath.

By signing in over SSH directly as your user, the session is theirs from start to finish: exit really ends it, and anything that requires privilege goes through sudo — which asks for a password and is logged in the system log. It is the difference between “I’m root wearing another hat” and “I’m a regular user and I ask permission when I need to”.

This matters even more if you will leave the session open for a while: better that it be your user’s, not root’s.

And why don’t we use that shortcut here? Besides the root session that stays open, it keeps you inside the VPS — and Step 4 happens on your computer. You would have to leave anyway, so exit solves both things at once.

⏱️ Want to control how long your SSH session survives idle? (optional — read if the connection drops by itself when you stop typing)

If you have noticed the session closing after a few minutes idle, the culprit is almost always the VPS provider, not the server: network firewalls tend to drop connections that sit without traffic for a while. Your computer can fix this by sending a sign of life every minute. Run on your computer, not on the VPS:

Terminal window
# on YOUR computer (Linux/macOS). On Windows with PuTTY, the equivalent field is
# "Seconds between keepalives" under Connection.
printf 'Host *\n ServerAliveInterval 60\n ServerAliveCountMax 3\n' >> ~/.ssh/config

After that, reconnect. This does not weaken security — the session is still ended if the network really goes down; what changes is that it stops looking abandoned while you are working.

Do this before going to the wizard.

The wizard’s Security step will turn off password login over SSH (hardening phase 02). From then on, the key is your way in — and the wizard asks for the public key right in the first phase, with an automatic 5-minute rollback running. By generating it now, with the terminal already open, you don’t have to stop midway to switch windows.

You just left the VPS, so you are already in the right place: the commands below run on your computer. That is where the key needs to exist so you can authenticate later — if it were generated on the VPS, it would be no use for signing in to it.

First, check whether you already have a key. If you already use SSH for something — GitHub, another server, work — it probably already exists, and generating another on top of it deletes the old one. Run:

Terminal window
ls -l ~/.ssh/*.pub

On Windows (PowerShell):

Terminal window
Get-ChildItem ~\.ssh\*.pub

Look only at the file names that showed up and follow the first row of the table that matches what you saw:

If the list shows…Use this keyWhat to do now
id_ed25519.pub (even if other files appear alongside it)id_ed25519Don’t generate anything. Go straight to ⬇️ Install the key on the VPS (it’s a link — click it)
id_rsa.pub, but not id_ed25519.pubid_rsaDon’t generate anything. Go to ⬇️ Install the key on the VPS and, there, use the commands with id_rsa
Only files with other names (e.g. github_deploy.pub, some_service_deploy.pub)none of themGenerate a new key in the next block
No such file or directory (or nothing)—Generate a new key in the next block

Why this rule, and not “pick whichever you like best”:

  • id_ed25519 and id_rsa are names that ssh tries on its own every time you connect. Using one of them, signing in to the VPS works with no extra configuration on your computer. id_rsa is older, but the server accepts it just the same.
  • Keys with other names were created for a specific service (a deploy, a company, a tool). Don’t reuse them: besides ssh not trying them on its own, mixing accesses means that whoever has that key also gets into your VPS.
  • Generating a new id_ed25519, in those last two cases, deletes nothing — it doesn’t exist yet, so there is nothing to overwrite.

Now generate the key pair (only if the table above told you to generate). It is the same command on Linux, macOS, WSL and Windows PowerShell:

Terminal window
ssh-keygen -t ed25519

It asks three questions, in this order:

What shows upWhat to do
Enter file in which to save the key (...id_ed25519):Just Enter — accepts the default location
Enter passphrase (empty for no passphrase):Type a passphrase. Nothing shows on screen, not even an asterisk
Enter same passphrase again:Repeat the same one

The passphrase is an extra password that protects the key in case someone gets access to your computer. It is recommended, and so you don’t have to type it on every connection you can keep it in the session:

Terminal window
eval "$(ssh-agent -s)" && ssh-add ~/.ssh/id_ed25519

When the command finishes, check that the key was created:

Terminal window
cat ~/.ssh/id_ed25519.pub

On Windows (PowerShell) the command to display it is different:

Terminal window
Get-Content ~\.ssh\id_ed25519.pub

A single line will appear, a long one, looking something like this:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH8k2p... your-user@your-computer

If the line showed up, the key is created.

Install the key on the VPS — now, while the password still works. The command always says which file to install; use the line for the key the table chose:

Terminal window
ssh-copy-id -i ~/.ssh/id_ed25519.pub YOUR_USER@YOUR_IP

If the table told you to use id_rsa, the line is this one (instead of the one above):

Terminal window
ssh-copy-id -i ~/.ssh/id_rsa.pub YOUR_USER@YOUR_IP

It asks for the adduser password one last time and writes your public key to the server. On Windows (PowerShell), where ssh-copy-id doesn’t exist, the equivalent is:

Terminal window
Get-Content ~\.ssh\id_ed25519.pub | ssh YOUR_USER@YOUR_IP "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

With id_rsa, replace id_ed25519.pub with id_rsa.pub on that line. From here on, whenever a command mentions id_ed25519 (the cat, the ssh-add), anyone using id_rsa makes the same swap.

Now test, and this is the test that matters:

Terminal window
ssh YOUR_USER@YOUR_IP

If you get in without it asking for the account password, it’s working. It may ask for the key passphrase — that is a different thing, it’s local, and has nothing to do with the server.

🔑 What if I lose, delete or corrupt the private key?

Prevention (do this before moving on):

  • Keep a copy of the private key somewhere safe, such as a password manager. Whoever has that copy can access the VPS — treat it like a password.

  • A more robust alternative: have a second key, generated on another device (another computer, or your phone). The wizard field accepts one key at a time, so install the second one after finishing the wizard, while the first one still works.

    Take the public key from the other device (the contents of its .pub), connect to the VPS from the computer that already has access and append the line:

    Terminal window
    echo "PASTE_THE_SECOND_PUBLIC_KEY_HERE" >> ~/.ssh/authorized_keys

    (Replace PASTE_THE_SECOND_PUBLIC_KEY_HERE with the key itself.)

    Use >> and not > — a lone > erases the existing keys and locks you out on the next connection. Check the result with cat ~/.ssh/authorized_keys: both lines should appear. That way, losing a device doesn’t leave you without access.

If you have already lost the key — or the password, or access to the panel — see I lost access: now what?, which covers each case and the way back.

The test above already left you connected to the VPS. From here on, all the steps are done there, with that user — anything that needs administrator permission will ask for sudo and your password.

Terminal window
sudo apt update && sudo apt install -y git
📦 git was already installed. Is that normal?

Yes, it’s normal. “Ubuntu 24.04 LTS” in your provider’s panel is almost never the raw system from Canonical: it is a template assembled by the provider, and git, curl, wget and vim usually come in that package. Finding git already there on a freshly created machine doesn’t mean someone got into it.

The Step 5 command is still valid: if the package is already there, apt replies git is already the newest version and does nothing.

Want to confirm instead of trusting? These are three read-only checks, none of which alters the system:

Terminal window
dpkg -V git # silence = files identical to the official package's
apt-cache policy git # the origin should be archive.ubuntu.com or security.ubuntu.com
last -a | head # only your own logins should appear

dpkg -V compares each installed file against the package’s official signature — no output is the good result. In apt-cache policy, what matters is the origin: an unknown repository there would be a reason to stop and investigate.

🔑 First sudo: the giant warning and the password that doesn't show

The first time you use sudo with a new user, a classic warning appears:

We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:
#1) Respect the privacy of others.
#2) Think before you type.
#3) With great power comes great responsibility.
[sudo] password for kelvin:

What to do:

  1. The warning is just welcome ceremony (a Linux tradition) — it doesn’t require an answer.
  2. At [sudo] password for kelvin: (on your terminal the name of the user you created will appear, not “kelvin”), type YOUR user’s password (the one you created in adduser), not the root password.
  3. Remember: nothing shows on screen while you type — not even *. Type it and Enter.

That long warning only appears once. After that sudo asks for the password directly — and, for a few minutes, not even that (it “remembers” that you authenticated).

Common errors:

  • Sorry, try again. — wrong password. You get 3 attempts before the command fails.

  • kelvin is not in the sudoers file — the user doesn’t have administrator permission, because the usermod from Step 3 never ran for this user. Since sudo itself is blocked, the fix has to be made as root. On your computer, open the root session, grant the permission and confirm:

    Terminal window
    ssh root@YOUR_IP
    usermod -aG sudo YOUR_USER
    id YOUR_USER # must list "sudo"
    exit

    And now the detail that fools everyone: go back to your user’s terminal, close the session with exit and connect again. Linux only reads a user’s groups at the moment the session opens — yours was opened before the group existed, so it would keep refusing the command even with the fix already applied. Once reconnected, test with sudo whoami: it should answer root.

Clone into /opt and give ownership of the folder to your user:

Terminal window
sudo git clone --branch main https://github.com/kelvindk9w/tws-panel.git /opt/tws-panel
sudo chown -R $USER:$USER /opt/tws-panel
cd /opt/tws-panel

The installer installs Docker if needed, builds the image, brings the containers up and finishes by showing your panel’s address, already with HTTPS: https://YOUR-IP-WITH-HYPHENS.sslip.io (e.g. https://203-0-113-10.sslip.io). No tunnel and no choosing a port.

Terminal window
./scripts/install.sh
🩺 Pre-flight: what shows on screen and what to do in each scenario

Scenario 1 — clean VPS (the expected case): the report comes out all green and the installation continues on its own, without asking for anything. (The installer prints in Portuguese for now. In short: the machine is being inspected without changes, then OS, RAM, free disk, Docker, running containers and ports are listed, and it ends with “clean machine detected — proceeding with the installation”.)

[tws-panel] Pré-flight: inspecionando a máquina (nada será alterado nesta etapa)…
✓ SO: Ubuntu 24.04.2 LTS (suportado)
✓ RAM: 1984 MB
✓ Disco livre em /: 23 GB
✓ Docker: ausente (será instalado por este script)
✓ Nenhum container Docker em execução
✓ Portas do proxy e do e-mail (80/443/25/465/587/143/993/8080) livres
[tws-panel] Máquina limpa detectada ✓ — prosseguindo com a instalação.

You don’t need to do anything — just wait for the build (it takes a few minutes the first time).

Scenario 2 — VPS already in use: the report shows items with ⚠ and the installer stops and waits for your decision. (In short: the installer lists the ports and services it found, warns that the VPS does not look clean, says it never removes or stops anything, and asks you to type “continuar” to proceed anyway.)

⚠ Portas em uso: 80 443
⚠ Serviço ativo: nginx
================================================================================
⚠️ ATENÇÃO: esta VPS NÃO parece estar limpa (2 ponto(s) acima).
O TWS Panel foi feito para uma VPS Ubuntu LIMPA. Continuar pode causar
conflitos (portas, serviços, recursos) com o que já existe na máquina.
Este instalador NUNCA remove ou para nada que já exista — mas os
serviços do painel podem falhar ao subir se as portas estiverem ocupadas.
Para prosseguir mesmo assim, digite "continuar" — ou rode com
--force (PAAS_FORCE=1) em automações.
================================================================================
Digite "continuar" para prosseguir:

What to do:

  • Recommended: press Ctrl+C (or simply type nothing and close it) to abort — nothing was installed or changed. Resolve the conflicts (e.g. disable nginx if it is no longer used, or get a clean VPS) and run the installer again.
  • If you know what you’re doing (e.g. the listed service doesn’t use the panel’s ports): type continuar and press Enter. By typing it, you confirm that you read the report and accept the risk of conflicts.

Common errors:

  • Typed it wrong (continua, Continuar) — the installer aborts with Instalação abortada. Nada foi instalado ou alterado. (Portuguese for “Installation aborted. Nothing was installed or changed.”). Just run it again.
  • Don’t use --force on your first try — it skips exactly the reflection this warning is meant to provoke. --force exists for automation, not for hurrying.

Right after the pre-flight, the installer asks two questions. They decide how the panel’s live terminal works — it is where the security scan and the hardening run:

  1. Which user the terminal opens as. It lists the VPS users that have administrator permission, but you are the one who types the name — even if there is only one. Type the user you created in Step 3. root is also accepted, but we don’t recommend it: a forgotten browser tab left open would give root access to the VPS to whoever sat in front of it.
  2. How to run what needs root. Answer 1 (senha, i.e. password) — it’s the recommended one: when something needs administrator rights, the panel uses sudo inside the terminal and you type your password there, as you would over SSH. The other option, 2 (segundo-plano, i.e. background), doesn’t ask for a password: those commands run as root behind the scenes, and you check them afterwards on the Audit log screen.

If the user won’t do (doesn’t exist, has no password, has no sudo…), the installer explains what to do and asks again. Nothing is installed before you answer. The panel’s port is chosen by the installer itself: it is internal only, you will never type it.

🔑 Password or background? What happens in each, no fine print

senha (password) mode (recommended). The terminal opens as your user. When a scan or a hardening phase needs root, the panel runs the command with sudo in the terminal itself, and sudo asks for your password — the same one from adduser. Nothing runs as administrator without you seeing and authorizing it.

  • The trade-off: the password leaves your browser, passes through the panel and reaches the VPS terminal. It is not saved, logged or sent anywhere — the project is open source and this can be verified in the code. On the way between your browser and the VPS it goes encrypted: the panel is opened over HTTPS (or through the SSH tunnel, if you choose that access).
  • Requires that the user has a password and is in the sudo group — which Step 3 already did.

segundo-plano (background) mode. The terminal also opens as your user, but the commands that need root run as root, behind the scenes, through the same bridge the panel already uses to talk to the VPS. The output shows up in the terminal just so you can follow along.

  • The trade-off: it doesn’t ask for a password, so you don’t authorize command by command. The check comes afterwards: everything is recorded on the panel’s Audit log screen.
  • Doesn’t require sudo — you can even use a user created just for the panel. The installer warns if the chosen user doesn’t have sudo, because then, signing in over SSH with it, you can’t do anything as administrator.

In both modes, the scheduled automatic monitoring — which runs by itself, with nobody to type a password — keeps running as root in the background, and each run is recorded in the Audit screen.

Condition for both modes to really protect you: the terminal user must not be in the docker group. Anyone in that group becomes root without a password with a single command, so they would bypass both the password prompt and the Audit. If the user is in it, the installer warns and offers to remove them — see Why the panel doesn’t put you in the docker group.

Installing through automation (with nobody to answer)? Pass the choices directly:

Terminal window
./scripts/install.sh --terminal-user=YOUR_USER --root-mode=senha

Without these options, with --force or without an interactive terminal, the installer doesn’t guess: it keeps the old behavior (terminal as root) and warns. On a reinstall, what you chose the first time is kept without asking again — to change it, see Changing the panel’s terminal user.

🔐 Optional — access only through an SSH tunnel (the panel stays invisible on the internet)

By default the panel opens at https://YOUR-IP-WITH-HYPHENS.sslip.io. If you prefer it to have no address at all on the internet — only people with SSH to the VPS reach it —, install with:

Terminal window
./scripts/install.sh --acesso=tunel

In this mode the installer asks which VPS port the panel sits on and finishes by showing the SSH tunnel command and the address http://localhost:PORT/?token=…. The browser will show “Not secure” because it is http://localhost — and here that’s fine: the traffic travels encrypted inside the SSH tunnel. To switch modes later, run the installer again with --acesso=https or --acesso=tunel. With the panel on HTTPS, Phase 02 also closes the SSH tunnel (it isn’t used); when switching to --acesso=tunel, the installer reopens the tunnel on its own.

Common errors when opening the tunnel:

  • bind [127.0.0.1]:9000: Address already in use (when opening the tunnel) — the port that’s taken is on your computer, not the VPS’s: the left number in ssh -L. One of your programs is already using 9000 (common for people who program). Change only that number and open the browser on it: ssh -L 9100:localhost:9000 YOUR_USER@YOUR_IP, then http://localhost:9100/?token=.... Any high free number will do — 9100, 9300, 12345. Understand the two ports of the tunnel. On the next installation, this error won’t even show up if you check beforehand which port is free on your computer and give that number to the installer.
  • “This site can’t be reached” in the browser, with the tunnel open — the address has the wrong number. What goes in the browser is always the left number of your ssh -L, which is your computer’s port. If your tunnel is ssh -L 9100:localhost:9001, the address is http://localhost:9100/?token=... — 9001 is the VPS’s port and never appears in the browser. Check which number your tunnel is listening on with ss -ltnH 'sport = :9100' (Linux/WSL).
  • channel 2: open failed: connect failed: Connection refused (the tunnel opens, but the browser says it couldn’t connect) — that’s the other side: the right number doesn’t match the port the panel is on in the VPS. Check with sudo ./scripts/show-token.sh, which prints the command already with the right port.

Open the panel at the https://…sslip.io address the installer showed at the end (it already carries the setup token in the link; if you need it, the token also appears highlighted in the terminal) and follow the wizard. The wizard opens in the language of your browser (see Language of the interface below).

┌──────────────────────────────────────────────────────────────────────┐
│ Setup wizard — https://YOUR-IP.sslip.io/?token=… │
├──────────────────────────────────────────────────────────────────────┤
│ 1. Welcome and token → validates the setup token and unlocks │
│ the server's live terminal │
│ 2. Server health → CPU, RAM, disk, network — the checks │
│ run LIVE in the embedded terminal │
│ 3. Security → Lynis scan → plan → hardening │
│ (with automatic rollback in 5 min) │
│ 4. Administrator account → the panel's username + strong password │
└──────────────────────────────────────────────────────────────────────┘

Language of the interface. The panel is available in English and Portuguese. On the first visit it goes through your browser’s list of languages and uses the first one it has: Portuguese for any pt variant (such as pt-BR), English for en; if none matches, English. To change it, use the Language selector on the setup wizard and sign-in screens, in the panel header (next to the user menu, always visible once you are signed in; with the side menu layout, at the bottom of the sidebar, above the user menu), or under Settings → Appearance. The choice is saved in that browser only (it is not part of your account), so another browser or device starts again from its own language. The messages that the server itself writes (see the note at the top of this page) are still in Portuguese.

The Security step is the one that demands the most attention: it changes how you sign in to the VPS. Before applying the phases, read the Security wizard.

The installer is idempotent: it can be run again without breaking anything (rebuild + restart). The panel runs 100% in Docker (docker compose up -d), with state persisted in the paas_data volume and access to the Docker socket to manage Caddy, Stalwart and your projects.

🎫 The installer's final banner — and how to recover the token later

When the installation finishes, the terminal plays a “beep” and shows a banner like this. (This is real installer output, in Portuguese. In short: “TWS Panel installed and running successfully”, the link to open in the browser, a note that the connection uses HTTPS, and the setup token box. The token is highlighted only now, so keep it until you finish the wizard; it stops working after you create the admin account.)

██████████████████████████████████████████████████████████████████████████████
██ ██
██ ✅ TWS PANEL INSTALADO E RODANDO COM SUCESSO! ██
██ ██
██████████████████████████████████████████████████████████████████████████████
👉 PRÓXIMO PASSO: abra o painel no navegador
Abra no navegador (de qualquer computador — não precisa de túnel nem de porta):
https://203-0-113-10.sslip.io/?token=<seu-token-de-48-caracteres>
A conexão é criptografada (HTTPS, certificado Let's Encrypt): a senha de
administrador que você vai criar e a senha do sudo, se o terminal pedir,
trafegam protegidas. O sslip.io só traduz o nome para o IP desta VPS — o
tráfego vai direto do seu navegador para cá.
┌──────────────────────────────────────────────────────────────────────────┐
│ ⚑ SETUP TOKEN ⚑ │
│ │
│ <seu-token-de-48-caracteres> │
│ │
│ ⚠ Ele aparece SÓ AGORA em destaque. Guarde-o até concluir o wizard. │
│ ⚠ Após criar a conta admin no fim do wizard, ele é invalidado. │
└──────────────────────────────────────────────────────────────────────────┘

(Where the sample shows <seu-token-de-48-caracteres>, the real banner shows your actual 48-character token.)

What to do: copy the full link (https://…sslip.io/?token=...) and paste it into the browser. It already carries the token embedded — you don’t need to type anything. Installed with --acesso=tunel? The banner shows, in its place, the tunnel command and the http://localhost:… address (see SSH tunnel access).

Closed the terminal and lost the banner? Don’t panic. On the VPS, run either one:

Terminal window
sudo ./scripts/show-token.sh # shows the full link + token again
sudo docker exec tws-panel cat /data/setup-token # shows only the token

Common errors:

  • The installer said the https://…sslip.io address didn’t respond within 3 minutes — the certificate wasn’t issued. The most common cause is ports 80 and 443 blocked by a provider firewall (in the hosting panel): open both and restart the panel with sudo docker compose restart. To see what the proxy says: sudo docker logs paas-caddy --tail 50. In the meantime, tunnel access works (the banner shows the command).
  • Errors when opening the tunnel (only with tunnel access) — they are explained in SSH tunnel access.
  • permission denied while trying to connect to the Docker daemon socket — you forgot sudo in front of the command. This is on purpose: the panel doesn’t put anyone in the docker group (see the reason).
  • setup token não encontrado... (Portuguese for “setup token not found…”) — either show-token.sh was run on a machine without the panel (run it on the right VPS, from inside /opt/tws-panel), or the wizard was already completed and the token was deleted on purpose: to reopen it with a new token, sudo ./scripts/reset-setup.sh.
  • The token no longer works in the browser — after you create the admin account (the last step of the wizard), the token is invalidated forever. From then on, access is through the sign-in screen, with your panel username and password.

On first access, the Dashboard shows the Get the panel ready checklist, with what is still left to configure (protections, two-factor authentication, panel domain, email, notifications and backups). Part 2 walks through each of them, in order.

To publish a project: register the project, point the DNS, and the panel takes care of the build, the proxy and SSL. Production guide in production.md. Everyday commands, the projects folder and changing the terminal’s port or user are in Operation.