How to bootstrap a fresh Ubuntu VPS for Ansible deployments
Set local values
export SERVER_IP="<your-server-ip>"
export ADMIN_USER="deploy" # default used by bootstrap.sh; override if needed
export SSH_PORT="14341" # non-standard port; bootstrap.sh hardens sshd to use this
Log in as root
ssh root@"$SERVER_IP"
Update the server
apt update
apt upgrade -y
reboot
Reconnect as root
ssh root@"$SERVER_IP"
Create an admin user
adduser <your-admin-user>
usermod -aG sudo <your-admin-user>
Copy the root SSH key to the admin user
mkdir -p /home/<your-admin-user>/.ssh
cp /root/.ssh/authorized_keys /home/<your-admin-user>/.ssh/authorized_keys
chown -R <your-admin-user>:<your-admin-user> /home/<your-admin-user>/.ssh
chmod 700 /home/<your-admin-user>/.ssh
chmod 600 /home/<your-admin-user>/.ssh/authorized_keys
Test admin SSH access
ssh <your-admin-user>@<your-server-ip>
Install base packages
sudo apt update
sudo apt install -y \
ca-certificates \
curl \
git \
nginx \
certbot \
python3 \
python3-apt \
sudo \
rsync \
acl \
build-essential \
ufw \
fail2ban \
logrotate \
unattended-upgrades \
apache2-utils
Enable Nginx
sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl status nginx --no-pager
Test Nginx locally
curl -I http://127.0.0.1
Configure the firewall
sudo ufw allow "${SSH_PORT}/tcp"
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
sudo ufw status verbose
The platform uses SSH port 14341 (not 22).
ufw allow OpenSSHopens port 22 — use the explicit port number instead.
Install Node.js 24
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" \
| sudo tee /etc/apt/sources.list.d/nodesource.list > /dev/null
sudo apt update
sudo apt install -y nodejs
Verify Node.js
node --version
npm --version
Install pnpm 11
sudo npm install -g pnpm@11
pnpm --version
Create deployment directories
sudo mkdir -p /srv
sudo mkdir -p /var/www/_letsencrypt/.well-known/acme-challenge
sudo mkdir -p /var/log/apps
sudo chown -R www-data:www-data /var/www/_letsencrypt
sudo chmod -R 755 /var/www/_letsencrypt
Verify Certbot
certbot --version
Do not use
certbot --nginxwhen Ansible owns the Nginx configuration.
certbot certonly --webroot --help
Ansible users: The
acleron-platformAnsible playbook connects asrootby default (ansible_user=rootininventory.ini). Disabling root SSH login will break Ansible until you switch to a non-root deploy user and updateinventory.ini. Either skip the steps below and come back to them after your first successful deploy, or updateansible_userto your admin user before disabling root access.
Harden SSH
sudo nano /etc/ssh/sshd_config
Use these SSH settings
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Port 14341
Restart SSH
sudo systemctl restart ssh
Test SSH before closing the root session
ssh -p 14341 <your-admin-user>@<your-server-ip>
Enable unattended security updates
sudo dpkg-reconfigure unattended-upgrades
Enable fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
sudo systemctl status fail2ban --no-pager
Verify the server baseline
python3 --version
sudo -V | head -n 1
nginx -v
certbot --version
git --version
rsync --version | head -n 1
node --version
pnpm --version
systemctl --version | head -n 1
sudo ufw status
Create DNS records
A <your-domain.com> <your-server-ip>
A www.<your-domain.com> <your-server-ip>
Verify DNS from the local machine
dig +short <your-domain.com>
dig +short www.<your-domain.com>
Create a GitHub deploy key
su - <your-admin-user>
ssh-keygen -t ed25519 \
-C "<your-admin-user>@<your-server-ip>" \
-f ~/.ssh/github_deploy_key
Print the deploy key
cat ~/.ssh/github_deploy_key.pub
Configure GitHub SSH access
nano ~/.ssh/config
Add the GitHub SSH config
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/github_deploy_key
IdentitiesOnly yes
Lock SSH permissions
chmod 700 ~/.ssh
chmod 600 ~/.ssh/config
chmod 600 ~/.ssh/github_deploy_key
Test GitHub SSH access
ssh -T git@github.com
Run the full bootstrap script instead
The platform ships bootstrap.sh in the acleron-platform repo. Copy it to the server and run it as root. It accepts two optional env vars:
ADMIN_USER— admin username to create (default:deploy)SSH_PORT— SSH port to harden sshd to (default:14341)
# On your local machine: copy the script to the server
scp acleron-platform/acleron-platform/bootstrap.sh root@"$SERVER_IP":/root/bootstrap.sh
# On the server as root:
chmod +x bootstrap.sh
./bootstrap.sh
# Or with custom values:
ADMIN_USER=myuser SSH_PORT=14341 ./bootstrap.sh
What the script does:
#!/usr/bin/env bash
set -euo pipefail
ADMIN_USER="${ADMIN_USER:-deploy}"
SSH_PORT="${SSH_PORT:-14341}"
# Creates admin user, grants passwordless sudo, copies root's authorized_keys
# Installs: nginx, certbot, git, rsync, ufw, fail2ban, build-essential, apache2-utils
# Creates /srv, /var/www/_letsencrypt, /var/log/apps
# Installs Node.js 24 from NodeSource
# Installs pnpm 11 globally
# Opens UFW: SSH port + Nginx Full; enables UFW
# Configures unattended security upgrades, enables fail2ban
# Hardens SSH: PermitRootLogin no, PasswordAuthentication no, Port $SSH_PORT; restarts sshd
IMPORTANT: Before closing your root session, verify the admin user can SSH in on the new port:
ssh -p 14341 deploy@<your-server-ip>
Test Ansible from the local machine
ansible all -i ansible/inventory.ini -m ping
The platform
inventory.iniusesansible_port=14341. If you changed the default SSH port, make sure the inventory reflects it before running any playbook.
Run the Ansible bootstrap playbook
mise run bootstrap
# Skip Node.js + pnpm installation (packages and hardening only):
mise run bootstrap-no-node
# Or directly:
ansible-playbook -i ansible/inventory.ini ansible/playbooks/bootstrap.yml
Deploy a project with its own infra config
ansible-playbook -i ansible/inventory.ini ansible/playbooks/deploy.yml \
-e project_config=../my-project/infra/project.yml
References: