Coolify Says "Server Is Not Reachable" After You Hardened Your VPS

You did the responsible thing. You made a normal user instead of living as root, you turned on 2FA, you enabled the firewall. Then Coolify stopped talking to your server.

This is the most common way people break a working Coolify box, and I have to own part of it. I teach this hardening in my Self Hosting 2.0 course, and my own Coolify install guide ends by telling you to lock down SSH keys and 2FA. Follow that literally and you kill the Coolify panel!

So I took a real server, broke it five separate ways on purpose, and wrote down the exact error each one gives. Then I hardened the same box properly and proved Coolify still connects. Everything below is output from that run, not advice.

TL;DR

Coolify logs into your server over SSH, as root, on port 22, using one key file it holds on disk. Five common hardening steps cut that one line. The fix is almost never to undo your security: it is to keep root reachable by key only, exempt root from 2FA with a Match User root block, and use ufw allow on port 22 instead of ufw limit.

What you'll have at the end
  • The exact error string each of the five breaks produces, so you can name yours in about a minute
  • A server where root password login is refused and your own user needs a 2FA code
  • A firewall and fail2ban that protect you without rate-limiting your own panel
  • Coolify still showing reachable and validated on that same hardened box
  • A recovery path for the case where you are already locked out
Before you start
  • What you need: a VPS running Coolify v4 and root SSH access. Every command here runs on that server, as root.
  • Tested on: Ubuntu 24.04. Paths and the socket-activation behaviour below are specific to it.
  • What can go wrong: every command in this guide edits live SSH. A bad edit locks you out of your own server. Before you change anything, open a second terminal and log in. Leave it open. That one habit is the difference between a 30-second undo and a provider console session.

Start here: which one did you do?

Coolify shows you this, and it is almost useless on its own:

Coolify error toast reading: Server is not reachable. Please validate your configuration and connection. Error: root@host.docker.internal: Permission denied (publickey).
The real message from Coolify 4.1.2: Server is not reachable. Please validate your configuration and connection. Error: [email protected]: Permission denied (publickey).

First question: can you still get into the server yourself? Everything in this section runs on the server, so you need a way in. If your own SSH is dead too, do not start here. Jump to if you are already locked out, get back in, then come back.

Almost certainly you can still get in, because the hardening you did was aimed at root, not at you. Log in as the normal user you made, the one you copied your keys to:

ssh deploy@YOUR_SERVER_IP

Everything below needs root. You are not logging in as root, you are becoming root after you are already inside, which still works even with PermitRootLogin no set. That distinction is the whole reason you are not locked out:

sudo -i

Now you have a root shell. Coolify keeps its private key in a folder on the server. This prints the file it is using, plus a .lock file you can ignore:

ls /data/coolify/ssh/keys/

Now make the same connection Coolify makes, using that key. Replace the filename with the one you just saw. -vvv turns on verbose output, and IdentitiesOnly=yes stops SSH from quietly trying your own keys instead, which would hide the problem. If it works, the last line is OK:

ssh -i /data/coolify/ssh/keys/ssh_key@YOUR_KEY_NAME -o IdentitiesOnly=yes -vvv root@localhost 'echo OK'

Then read the server's own log. The client message alone cannot tell two of these five apart, and the log is the only thing that can:

tail -30 /var/log/auth.log

Now match what you got:

What you see, and what the server log saysCause
Permission denied (publickey)
log says ROOT LOGIN REFUSED FROM ...
Break 1
root login disabled
Permission denied (publickey)
log says Connection closed by authenticating user root ... [preauth], with no REFUSED line
Break 5
Coolify's key gone from root
Permission denied (keyboard-interactive), and partial success in the -vvv output Break 2
2FA applies to root
Connection refused, but only sometimes
log says [UFW BLOCK]
Break 3
ufw limit on port 22
Connection refused, every single time
log says nothing at all
Break 4
SSH moved off port 22
You may have done more than one of these

The table gives you one cause per error, but most people who land here did their whole hardening checklist in one sitting: new user, 2FA, firewall, all before the first restart. So expect to go round more than once.

Fix one, then run the same test command again. If the error changes, that is progress, not a failed fix: you just uncovered the next one underneath. Keep going until the test prints OK. Only then go back to Coolify and validate the server.

Before you retry that login twenty times

If you set up fail2ban, it is watching. I locked myself out during this test, retried from my laptop, and my own jail banned me for an hour. Port 22 went from Permission denied to timing out completely, which looks like a totally different problem. Diagnose from the server, not by hammering the door.

Why Coolify needs root SSH at all

Coolify manages every server over SSH. Not just remote ones: the machine Coolify itself runs on is just another server in its list, reached the same way.

How Coolify actually reaches your server

The same path is used even when Coolify manages the machine it is running on.

Container

The Coolify app

Runs in Docker on your VPS. Holds one private key on disk.

/data/coolify/ssh/keys/ssh_key@<id>
Host

Your server, as root

The matching public key sits in root's authorized_keys.

/root/.ssh/authorized_keys

Coolify is a machine holding one root key. It cannot type a password, it cannot enter a 2FA code, and it cannot switch to your new user. Every hardening step that blocks root, adds a second factor, moves the port, or rate-limits port 22 cuts this exact line.

One key, one user, one port. Every break in this guide cuts this line.

You can see it in the panel itself. Open your server in Coolify and look at the User and Port fields: root and 22.

Coolify server configuration page showing Server is reachable and validated, with User set to root and Port set to 22.
A healthy server: Server is reachable and validated, user root, port 22.

Coolify is a machine holding one root key. It cannot type a password, it cannot enter a code from your phone, and it does not know about the nice new user you made. That is the whole story.

One useful thing I confirmed: the Coolify installer does not touch your sshd_config. I compared the file before and after installing and there was no difference at all. It only appends its public key to /root/.ssh/authorized_keys. So nothing here is Coolify breaking itself. It is always something done afterwards.

Break 1: you disabled root login

The most common one. Every VPS hardening tutorial says to do it. The line goes in your main SSH config file:

/etc/ssh/sshd_config
PermitRootLogin no

Then you apply it, and that is the moment Coolify dies:

systemctl restart ssh

The panel drops the green Proxy Running badge and tells you the server has to be validated before you can use it:

Coolify server page showing You can't use this server until it is validated, with a large Validate Server button.
Same server, after one config line. Note the Proxy Running badge is gone.

On the server you get Permission denied (publickey). The verbose log is the confusing part: it shows the correct key being offered and still refused. So the key is fine. The log line that actually names the cause is this one:

In /var/log/auth.logsshd[13684]: ROOT LOGIN REFUSED FROM ::1 port 45408

The fix

Disabling root passwords is not the same as disabling root keys. You want the first, not the second. Change the line to this:

/etc/ssh/sshd_config
PermitRootLogin prohibit-password
PasswordAuthentication no

Apply it the same way:

systemctl restart ssh

Root can still log in with a key, never with a password. That kills password brute-forcing, which is what the tutorial was trying to achieve in the first place. Coolify's own documentation recommends the same value.

A confusing detail

After you set prohibit-password, the command sshd -T prints it back as without-password. Those are the same setting. The man page calls without-password a deprecated alias. Your edit did work.

Break 2: you turned on 2FA

You installed libpam-google-authenticator (the package that adds phone-code login to SSH), scanned the QR code, and added the lines the tutorial gave you:

/etc/ssh/sshd_config
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive

Now every SSH session needs a code from your phone. Coolify does not have a phone.

This one is easier to identify, because the error is different:

What you'll seeroot@localhost: Permission denied (keyboard-interactive).

And with -vvv you see the giveaway. The key works, and then the server asks for something else:

In the -vvv outputAuthenticated using "publickey" with partial success.
debug1: Authentications that can continue: keyboard-interactive

partial success appears in no other failure in this guide. If you see it, this is your problem.

The fix

Keep 2FA for yourself, exempt root. You do that with a Match block, which is an "only for this user" section of the config. Everything after a Match line applies only to whoever it matches, so a Match block has to go at the very end of the file. Put it in the middle and every setting below it silently becomes root-only:

/etc/ssh/sshd_config — the Match block goes last
AuthenticationMethods publickey,keyboard-interactive

Match User root
    AuthenticationMethods publickey

Apply it, then check it resolved the way you meant. sshd -T -C user=NAME asks sshd what rules it would actually apply to that user, which is the only way to be sure a Match block did what you think:

systemctl restart ssh
sshd -T -C user=root | grep -i authenticationmethods
sshd -T -C user=deploy | grep -i authenticationmethods

With 2FA still installed and enforced, that gives two different answers. Root is key-only, your own user still has to produce a code:

What you'll seeauthenticationmethods publickey
authenticationmethods publickey,keyboard-interactive

Your user gets a code prompt. Coolify gets in with its key.

Break 3: you added a firewall limit rule

This is the sneaky one. Coolify's own troubleshooting docs say a LIMIT rule on port 22 accounts for about 90% of "connection unstable" reports. Almost every hardening tutorial tells you to run it. On the server, as root:

ufw limit 22/tcp

That looks harmless. It is not, because limit expands into a rate limit that blocks a source IP after 6 connections in 30 seconds, and Coolify opens a lot of connections.

I ran 20 connections from inside the Coolify container to the host, three times: with no firewall, with the limit rule, and after switching to allow.

What ufw limit 22/tcp does to Coolify

20 connections from the Coolify container to the host, same command each time.

No firewallbaseline 20 / 20
ufw limit 22/tcpwhat the tutorials tell you 5 / 20
ufw allow 22/tcpthe fix 20 / 20

The failures start at exactly attempt 6, which matches the rule limit expands into: --seconds 30 --hitcount 6.

Measured on the lab server, 2026-07-27.

The error here is Connection refused, not a permission problem. And here is why it fools people: the block clears after the window. Wait half a minute, test once by hand, and it works perfectly. So you conclude the server is fine and the panel is buggy.

The fix

Swap the limit rule for a plain allow rule:

ufw delete limit 22/tcp
ufw allow 22/tcp

You still want brute-force protection, just not one that counts Coolify as an attacker. Use fail2ban for that, which bans on failed authentications rather than on connection rate. There is a ready-to-paste config in the safe recipe below.

Break 4: you moved the SSH port

You set Port 2222 and never told Coolify. It keeps dialling 22, so it gets:

What you'll seessh: connect to host localhost port 22: Connection refused

The tell for this one is what is missing. Nothing arrives on port 22 at all, so sshd never logs a rejection. An empty log during a failure is itself the diagnosis.

The fix

Tell Coolify the new port. Open your server in Coolify, change the Port field next to User, press Save, then validate the server:

Coolify server General settings panel showing the Name, IP Address/Domain, User and Port fields, with User set to root and Port set to 22, and a Save button at the top.
The fields that matter are User and Port, top right. Change the port here, press Save, then validate.

And open the new port in your firewall, or you have simply moved the problem:

ufw allow 2222/tcp

The Ubuntu 24.04 trap: your port change may not have happened

This one deserves its own heading, because it makes people think the port change failed for some other reason. On Ubuntu 24.04, sshd is socket-activated: systemd holds the network port open and hands connections to sshd, instead of sshd opening the port itself. So the port number lives in a systemd unit called ssh.socket as well as in sshd_config, and the systemd one wins.

I tested exactly this. Set Port 2222 in sshd_config, change nothing else, restart, and look at what is actually listening:

sshd -T | grep -i '^port'
ss -ltn | grep -E ':(22|2222)'

The two answers disagree, and that is the whole trap. sshd agrees with your edit. The listener never moved:

What you'll seeport 2222
LISTEN 0  4096  0.0.0.0:22   0.0.0.0:*
LISTEN 0  4096     [::]:22      [::]:*

Nothing is listening on 2222 at all. Restarting ssh.socket as well changes nothing, because the socket unit still carries the old port:

systemctl cat ssh.socket | grep ListenStream
What you'll seeListenStream=0.0.0.0:22
ListenStream=[::]:22

To really move the port you have to override the socket unit too, then reload systemd:

/etc/systemd/system/ssh.socket.d/override.conf
[Socket]
ListenStream=
ListenStream=2222

The empty ListenStream= line is not a typo. It clears the inherited value first, and without it you end up listening on both ports. Then apply and confirm:

systemctl daemon-reload
systemctl restart ssh.socket
ss -ltn | grep -E ':(22|2222)'

Honestly, my advice is to leave SSH on port 22. Moving it stops log noise, not attackers, and on 24.04 it costs you two config files that have to agree.

Break 5: you moved your keys to the new user

This is the exact move that started this guide. You created a proper sudo user, copied your keys over, and cleaned up root's authorized_keys. Sensible on any normal server. On a Coolify box you just deleted the one credential Coolify had.

And here is the part that sends people round in circles on GitHub: this produces the identical client error to Break 1.

Break 1 — root login disabled
# ssh -i /data/coolify/ssh/keys/ssh_key@... root@localhost
root@localhost: Permission denied (publickey).

# tail /var/log/auth.log
sshd[13684]: ROOT LOGIN REFUSED FROM ::1 port 45408
Break 5 — Coolify's key removed from root
# ssh -i /data/coolify/ssh/keys/ssh_key@... root@localhost
root@localhost: Permission denied (publickey).

# tail /var/log/auth.log
sshd[16437]: Connection closed by authenticating user root ::1 port 34874 [preauth]

Same command. Same error. Two completely different causes. The client cannot tell them apart, so you have to read the server log: only the first one says ROOT LOGIN REFUSED.

The single most useful fact on this page. The client error is a dead end; the log is not.

The fix

Put Coolify's key back on root. Coolify's FAQ tells you to cat a .pub file at /data/coolify/ssh/keys/[email protected]. On 4.1.2 that file does not exist. The installer creates that name and the app replaces it within about a minute with ssh_key@<id>, so following the official fix gives you No such file or directory.

Instead, derive the public key from the private key Coolify actually holds. Run this as root on the server. ssh-keygen -y reads a private key and prints its matching public key, and the >> appends that line to root's authorized keys:

KEY=$(ls /data/coolify/ssh/keys/ | grep -v '\.lock$' | head -1)
ssh-keygen -y -f "/data/coolify/ssh/keys/$KEY" >> /root/.ssh/authorized_keys

Then prove it worked before you go back to the panel:

ssh -i "/data/coolify/ssh/keys/$KEY" -o IdentitiesOnly=yes root@localhost 'echo OK'

Can Coolify just run as a non-root user?

Yes, and this surprised me. I expected the forum answer ("experimental, don't"). It is not. Coolify 4.1.2 ships real non-root support: there is an isNonRoot() check in its remote-process helper and a whole module that rewrites commands with sudo.

The catch is what the user needs. Without the docker group it cannot do anything useful:

What you'll seepermission denied while trying to connect to the docker API at unix:///var/run/docker.sock

Add it to the docker group and Coolify works. But docker group membership is not a small permission. The daemon runs as root, so anyone who can talk to it owns the host. On my test box the non-root user read the shadow file, which holds every password hash on the system, in one command:

Proof, not an instruction$ docker run --rm -v /etc:/host alpine head -2 /host/shadow
root:*:20075:0:99999:7:::
daemon:*:20075:0:99999:7:::

So the honest answer is not "don't" and not "yes, it's safer". It is: supported, and it buys you less security than it looks like. For a single-server setup I would rather have root key-only, which is simpler to reason about.

The hardening that does not break Coolify

This is the whole recipe. It is what the lab server is running now.

Do this first. It is not optional.

Open a second terminal and log into the server before you touch anything. Leave it open. Do not close it until the very last check passes.

An SSH session that is already open is not re-checked when the rules change. That open terminal keeps working even if your new config locks out every new connection, and it is how you undo a mistake in 30 seconds instead of hunting for your provider's web console. I prove this in the recovery section.

Step 1: write the SSH rules to a drop-in file

Do not edit /etc/ssh/sshd_config directly. Here is why, and it catches a lot of people.

Ubuntu's server image ships a folder called /etc/ssh/sshd_config.d/. Files in there are called drop-ins: small config files that get pulled into the main one. The main file pulls them in with one line, and that line comes before every setting in it. On a stock 24.04 image it is line 12, under the comment header:

The first real setting in /etc/ssh/sshd_configInclude /etc/ssh/sshd_config.d/*.conf

Two things matter about that. First, it sits at the top, so the drop-ins are read before anything else in the main file. Second, SSH keeps the first value it sees for a setting and ignores every later one. Most config files work the opposite way, where the last line wins, which is exactly why this catches people out.

Put those two together and you get the thing everyone trips on: if a drop-in sets something, editing that same setting in the main file does nothing. Your change is in the file, the file is valid, sshd just never reaches it. That is the whole explanation for "I set it and restarted and nothing changed".

It also works in your favour. A setting in your own drop-in is read before the main file, so it wins. That is why we add a file instead of editing one. A stock Ubuntu 24.04 image ships two drop-ins already:

ls /etc/ssh/sshd_config.d/ on a fresh 24.04 image50-cloud-init.conf          PasswordAuthentication no
60-cloudimg-settings.conf   PasswordAuthentication no

One caveat, because the numbers do matter. Drop-ins are read in filename order, so 50- is read before 99-, and first-one-wins applies between them too. A 99- file cannot override something a 50- file already set. Here that is fine: the only thing those two stock files set is PasswordAuthentication no, which is exactly what we want anyway. If you ever need to overrule a stock drop-in, give your file a lower number than theirs.

Create this file:

/etc/ssh/sshd_config.d/99-coolify-safe.conf
PermitRootLogin prohibit-password
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive

Match User root
    AuthenticationMethods publickey

That is the whole trick of this guide in six lines: humans need a key and a code, root needs only a key, and nobody gets in with a password.

Step 2: check the config before you apply it

sshd -t parses the config and prints nothing if it is valid. Never restart SSH without running it first, because a typo here is how you lock yourself out:

sshd -t

Step 3: apply it, then confirm something is still listening

On Ubuntu 24.04 restart the socket, not the service, because the socket is what holds port 22 (the same socket activation from Break 4):

systemctl restart ssh.socket

Now verify two things before you close your rescue terminal. That port 22 is really open, and that your drop-in actually took effect rather than losing to something read earlier:

ss -ltn | grep ':22'
sshd -T | grep -iE 'permitrootlogin|passwordauthentication|authenticationmethods'

The second command prints the rules sshd is really applying, not the ones you wrote. Remember that prohibit-password comes back as its old alias without-password:

What you'll seepermitrootlogin without-password
passwordauthentication no
authenticationmethods publickey,keyboard-interactive

That check matters more than it looks. I skipped it once during this build and ended up with nothing listening on port 22 at all, which from outside looks exactly like a firewall problem. It cost me a provider-console power cycle. If that command prints nothing, do not close your other terminal, and start SSH again with systemctl start ssh.socket.

Step 4: the firewall, with allow and never limit

Default-deny everything coming in, then open only what you need. Note port 8000: that is the Coolify dashboard itself. Leave it out and you will lock yourself out of the panel while "securing" the server:

ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 8000/tcp
ufw enable

If you have moved Coolify behind a domain with HTTPS and no longer reach it on :8000, drop that line. Check how you actually open your panel before you decide.

Step 5: fail2ban, the protection ufw limit was supposed to give you

Install it:

apt update
apt install -y fail2ban

Then create its config. This bans an IP for an hour after 5 failed logins in 10 minutes, and it counts failed authentications, so Coolify reconnecting fast never trips it:

/etc/fail2ban/jail.local
[sshd]
enabled = true
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h

Start it and confirm the jail is actually running:

systemctl enable --now fail2ban
fail2ban-client status sshd

Step 6: now prove it, from outside

Test from your laptop over the internet, not from the server itself. Loopback connections never cross the firewall, so a localhost test can pass on a box that is unreachable from the world.

Hardened and working, at the same time

Same server, same minute. Checks run from a laptop over the public internet.

The security checks

verify from outside
-- 1. key-based root login from the internet
  [PASS]
-- 2. root PASSWORD login is refused
  [PASS]
-- 3. the human user is challenged for 2FA
    Authenticated using "publickey" with partial success.
    deploy@server: Permission denied (keyboard-interactive).
  [PASS]
-- 4. firewall + fail2ban
  [PASS] ufw active
  [PASS] fail2ban active
-- 5. no LIMIT rule on 22
  [PASS]
-- 7. Coolify's key opens a root session
  [PASS]

RESULT: PASS=8 FAIL=0

The panel, at that same moment

Coolify server page at the same moment, showing Server is reachable and validated with user root and port 22.

Root password login refused, 2FA enforced for the human, firewall up, fail2ban running. And Coolify still says reachable and validated.

Eight checks from outside the box, and a validated panel, on the same server in the same minute.

That is the point of the whole guide. Security and a working panel were never the trade everyone assumes they are.

If you are already locked out

First: stop retrying. Every failed attempt feeds fail2ban, and if it bans you, port 22 stops answering entirely and you have made your own diagnosis harder. I did this to myself during this build and lost an hour to it.

If you still have a terminal open from before the change, you are fine. An established SSH session is not re-authenticated when sshd restarts or when the rules change. Only new connections hit the new config. I tested this deliberately: with root login fully disabled, a new connection was refused while the session I had left open kept working as root, and I repaired the server through it.

That is the single most useful habit in this guide. Open a second terminal before you edit sshd_config, every time.

If every session is closed, use your provider's web console. It attaches to the machine's screen directly and does not go through SSH at all, so a broken SSH config cannot lock you out of it. DigitalOcean calls it the Droplet Console, Hetzner has it as Console in the server view, and Vultr as View Console. Log in there with your root password, fix the config, restart SSH, and validate the server in Coolify.

If you never set a root password, that console will not help. Most providers offer a recovery or rescue mode that boots a temporary system with your disk attached, which is the last resort. This is the real reason to keep the second terminal open.

Questions people actually ask

Why does Coolify still say the server is reachable when it clearly is not?

Because it reuses one open connection for many commands, a trick called multiplexing. It keeps a master socket under /data/coolify/ssh/mux/, and an already-open master keeps working after you break authentication. During this test the panel reported reachable and validated for over ten minutes after root SSH was completely dead. It is also why the break so often shows up as "it worked yesterday" rather than immediately.

Is hardening my VPS even worth it?

Yes, and it is not close. While I was running the firewall test, the log picked up scans from three separate addresses within seconds of the firewall coming up, on a server that was about twenty minutes old and announced nowhere. Bots scan the whole address space continuously. The answer to this guide is never "turn security off", it is "point it at the right thing".

Do I still need a non-root user?

For yourself, yes. Do your own work as a normal sudo user with 2FA. Just do not take root's key away, because that key is Coolify's only way in.

Tested on: Ubuntu 24.04.4 LTS · OpenSSH 9.6p1 · Docker 29.6.2 · Coolify 4.1.2 · DigitalOcean droplet, 2 vCPU / 4 GB.
Lab run: 2026-07-27. Last verified: 2026-07-28, when the socket-activation test in Break 4 was re-run on the same box. Every error message, log line and number on this page came from those runs.

Hasan Aboul Hasan giving a thumbs up

Keep that second terminal open.

Hasan Aboul Hasan builds open-source tools and teaches solo developers how to build, host, and sell AI-powered products. Founder of LearnWithHasan.com, creator of SimplerLLM and PyRunner.

Vibe Engineering Blocks — free guide
Free guide

Get the free Vibe Engineering Blocks guide

The exact building blocks I use to ship real products with AI — yours as a free PDF.

Free PDF · double opt-in · unsubscribe anytime.

Have a question? Ask it in the community — it's tagged #guide and linked back here. Reading is open to everyone; posting needs a free account.

Loading questions…