Target OS: Ubuntu 26.04 LTS. The steps below apply to any recent Ubuntu LTS.
Order matters. Don't lock down SSH until you've confirmed key login works in a second terminal, and don't install anything on ports 80 / 443 / 3000 if you plan to run Dokploy.
On
sudo: these commands are written assuming a root shell (common during initial provisioning). Once you've created your non-root user and disabled root login (steps 2–3), you should be logged in as that user and prefixing privileged commands withsudo— that's the correct, safer habit. For a long privileged sequence (like the Docker + Dokploy installs),sudo -ito get a root shell, do the work, thenexit.
apt update && apt upgrade -yadduser username
usermod -aG sudo username# on your LOCAL machine — prefer an ed25519 key (shorter, faster, strong)
ssh-keygen -t ed25519 -C "label" # if you don't have one yet
ssh-copy-id -i ~/.ssh/id_ed25519.pub username@server_ipPoint
-iat the.pubfile specifically. Without-i,ssh-copy-idcopies every public key in your agent and~/.ssh/*.pub(e.g. an old rsa key lands on the server too).
Then on the server edit /etc/ssh/sshd_config:
vim /etc/ssh/sshd_configPermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Restart the daemon — but first open a new terminal and confirm ssh username@server_ip
works while your current session is still alive, so you can't lock yourself out:
systemctl restart sshOften skipped, occasionally painful later.
# timezone + clock sync
timedatectl set-timezone Europe/Paris # adjust as needed
timedatectl set-ntp true
# swap — Docker builds can OOM a small VPS; skip only if you have plenty of RAM
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstabapt install unattended-upgrades # note the spelling
dpkg-reconfigure unattended-upgrades
vim /etc/apt/apt.conf.d/50unattended-upgrades # keep only the -security originOptional — let it reboot for kernel updates during a quiet window:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
Promoted from "another time" — it's quick and complements UFW's rate limiting.
apt install fail2banCreate /etc/fail2ban/jail.local:
[sshd]
enabled = true
maxretry = 5
bantime = 1hsystemctl enable --now fail2ban
fail2ban-client status sshdapt install ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp # SSH
ufw limit 22/tcp # rate-limit SSH (max ~6 connections / 30s)
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw enable
ufw status verbose # confirm SSH still works before logging outDo NOT add
ufw allow 3000. See the Dokploy section — the dashboard should not be exposed to the internet, and UFW can't fully control it anyway.
Install Docker manually before running the Dokploy installer. Dokploy's script can install Docker for you, but letting it do so tends to cause problems — install Docker first so it's already present and correctly configured when the Dokploy installer runs (it detects an existing Docker and skips that part).
Approach:
- Follow the official guide, which stays current: https://docs.docker.com/engine/install/ubuntu/ — use the "install using the apt repository" method.
- Do not use Ubuntu's
docker.iopackage — it's outdated. Install from Docker's own apt repository so you get patches and current versions. - Install the full set: engine, CLI, containerd, plus the buildx and compose plugins.
- Verify with
sudo docker run --rm hello-world. - Add your user to the
dockergroup so you can rundockerwithoutsudo(sudo usermod -aG docker username, then log out / back in). Note thedockergroup is effectively root-equivalent, so only add trusted users.
If you see warnings like:
Warning: Target Packages (stable/binary-amd64/Packages) is configured multiple times in
/etc/apt/sources.list.d/docker.list:1 and /etc/apt/sources.list.d/docker.sources:1
…it means the Docker repo is defined twice — usually because an older one-line
docker.list exists alongside the newer docker.sources (e.g. you ran two different guides,
or the install steps twice). These are warnings, not errors. Fix them by keeping only one
definition (the modern .sources is preferred) and deleting the other:
sudo rm /etc/apt/sources.list.d/docker.list
sudo apt updateKeep exactly one Docker repo definition — don't delete both, or apt won't find Docker packages.
Open-source self-hosted PaaS (an alternative to Vercel / Netlify / Heroku) that builds and deploys git repos for you. It runs on Docker Swarm and bundles Traefik, which manages ports 80 and 443 and handles Let's Encrypt SSL automatically — so don't put Nginx or anything else on those ports.
Requirements: ≥ 2 GB RAM, ≥ 30 GB disk, and ports 80, 443, 3000 free (install fails otherwise). Docker should already be installed from the section above.
Docker (and Swarm) write their own iptables rules that bypass UFW. A published container
port becomes reachable from the internet even though ufw status shows nothing allowing it —
UFW reports it as blocked while it is in fact open. So the firewall section above does not
protect the Dokploy dashboard on port 3000 on its own.
Pick one of these to actually keep 3000 private:
- Recommended: set up a domain + HTTPS for the dashboard (Dokploy → Domains), then remove
the public 3000 publication entirely:
docker service update --publish-rm "published=3000,target=3000,mode=host" dokploy - Reach the dashboard over an SSH tunnel instead of opening it:
# from your local machine ssh -L 3000:localhost:3000 username@server_ip # then browse http://localhost:3000
The official one-liner (it will detect the Docker you installed above and skip installing it, then set up Swarm + the Dokploy stack):
curl -sSL https://dokploy.com/install.sh | shPiping a script to a shell runs arbitrary code as root — if that bothers you, download and read
install.sh first, or use the Manual Installation guide. For pinning versions, updating,
setting the advertise address, and other options, see the official docs:
https://docs.dokploy.com/docs/core/installation
If you're running as your sudo user rather than root, pipe to
sudo sh. Note that env vars set in your shell don't pass into the root-run script, so run the install in a root shell (sudo -i) if you need to set any.
- Wait ~15s, then reach the dashboard at
http://your_server_ip:3000(via the SSH tunnel from above is cleanest). - Create the admin account immediately — anyone who hits that page first owns your instance.
- Configure a domain + Let's Encrypt for the dashboard, verify HTTPS works, then disable the
ip:portaccess per option 1 above. - Deploy apps via Dokploy → Projects → Git repository.
- Dokploy/Traefik own 80 + 443. Let Traefik terminate SSL; don't run a second reverse proxy.
- Backups, S3 destinations, and scheduled jobs are built in — worth configuring once you have something running.
Rust toolchain plus a few CLI tools. Install these as your normal user (not root) — Rust goes
in ~/.cargo / ~/.rustup and shouldn't be installed system-wide via sudo.
Install via rustup (the official installer; check current instructions at
https://rustup.rs). It installs rustc, cargo, and rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# then load cargo into the current shell (rustup adds this to your profile too)
. "$HOME/.cargo/env"You may need a C linker/toolchain for building some crates:
sudo apt install -y build-essentialInstalls Rust binaries from prebuilt release artifacts instead of compiling from source — much
faster than cargo install. Official one-liner (see
https://github.com/cargo-bins/cargo-binstall for current instructions):
curl -L --proto '=https' --tlsv1.2 -sSf \
https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bashcargo binstall zellijbinstall falls back to building from source if no prebuilt binary matches your target, so it
always resolves one way or another.
Also available through binstall:
cargo binstall ripgrep # provides the `rg` command(Alternatively sudo apt install ripgrep, though the apt version tends to lag upstream.)
Helix doesn't distribute via crates.io in a way that's reliable for binstall. The old
ppa:maveonair/helix-editor PPA that older guides reference is now deprecated and often
lacks a release for recent Ubuntu versions — don't use it. Helix is now packaged in Ubuntu's
own repositories, so on 26.04 the simplest route is plain apt:
sudo apt install helix # provides the `hx` commandIf that package isn't available or is too old for your taste, the official AppImage from the releases page is the maintained fallback. Always check current options at https://docs.helix-editor.com/install.html.
After install, run hx --health to check language-server/grammar status. Language servers
(e.g. rust-analyzer, added with rustup component add rust-analyzer) are installed
separately per language.