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 OpenSSH opens 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 --nginx when Ansible owns the Nginx configuration.

certbot certonly --webroot --help

Ansible users: The acleron-platform Ansible playbook connects as root by default (ansible_user=root in inventory.ini). Disabling root SSH login will break Ansible until you switch to a non-root deploy user and update inventory.ini. Either skip the steps below and come back to them after your first successful deploy, or update ansible_user to 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.ini uses ansible_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: