Back to training

From zero to hardened VPS: a day-by-day timeline

Most VPS guides either skip the preparation or skip the hardening. This one does neither. What follows is a realistic timeline for standing up a VPS that is properly secured, properly configured, and ready to run software. Each phase is a day of focused work, not eight hours of sitting at a terminal.

The target: a Debian or Ubuntu LTS server running Apache with a valid SSL certificate, locked down, monitored, and backed up. Everything beyond that (WordPress, Nextcloud, Mattermost, whatever you actually want to run) builds on this foundation.

This guide assumes you are starting from scratch with no existing VPS. If you already have a server running but want to harden it, skip to T+1. If you are mid-setup and something has gone wrong, get in touch and we can pick up from wherever you are.

PhaseT−2

Contracts, scope, and preparation

Before any server exists, get the paperwork done and the plan agreed. If you are working with Techster Consulting, this is when we sign the engagement letter and agree the Statement of Work. Even if you are doing this alone, write down what you want to run before you spend a penny. Scope creep during setup is the most common reason people end up with a messy, half-configured server.

What to decide at this stage

  • What software you intend to run (website, file storage, chat, git hosting, etc.)
  • How much RAM and storage you genuinely need. A basic website and a few self-hosted apps run comfortably on 2 vCPU / 4 GB RAM / 40 GB SSD. Do not over-provision yet; you can resize later.
  • Which region to host in. For GDPR purposes, choose a provider with data centres in the EU. Germany, France, and Finland are common choices.
  • Your domain name, if you do not already have one. You will need it at T+3.

Recommended providers

  • 🇬🇧 Fasthosts: UK-native, Tier-IV data centres in Worcester and London, 100% renewable energy, ISO 27001 certified. VPS 1 (1 vCPU, 2 GB RAM, 60 GB NVMe) from £2/month for the first year, then £3/month. UK-based support, excellent latency for UK clients. Watch out for renewal price jumps on promotional plans.
  • 🇬🇧 Krystal: smaller UK-owned provider, B Corp certified, London data centre. Plans from around £9/month. UK-based support team with fast response times. Good choice if you want a genuinely independent UK host with strong ethics credentials.
  • 🇬🇧 IONOS UK: German-owned but with UK data centres, strong GDPR compliance, 99.99% uptime SLA, unlimited traffic at 1 Gbit/s. Entry VPS from £1/month promotional, settling around £3 to £5/month. Includes a personal consultant on every plan.
  • 🇩🇪 Hetzner: consistently the best value in Europe. CPX21 or CX22 (2 vCPU, 4 GB RAM, 40 GB SSD) around £4 to £7/month. No frills, excellent reliability, transparent pricing, no introductory rate tricks. Data centres in Nuremberg, Falkenstein, and Helsinki.
  • 🇫🇷 OVHcloud: French provider, strong EU data sovereignty story, VPS Starter or Value range at similar pricing to Hetzner. Good if a French data centre matters for your compliance requirements.
  • 🇩🇪 Contabo: more RAM per pound than almost anyone else. Good for memory-heavy workloads (Nextcloud, Mattermost, Grafana stacks). Support is slower than Hetzner. Data centres in Germany, US, and Asia.

If your clients are UK-based and latency or data residency within the UK matters, Fasthosts or Krystal are the natural first choices. If cost is the primary driver and EU residency is sufficient, Hetzner is hard to beat.

Choose Ubuntu 24.04 LTS or Debian 12 (Bookworm) as your operating system. Both have long support windows, large communities, and all the packages you will need. Avoid non-LTS releases on production servers.

PhaseT−1

Accounts, SSH keys, and the setup plan

This is the last day before the server exists. Use it to generate your SSH key pair and create your provider account. Do not skip the SSH key step; you will need it at T+0.

Generate your SSH key pair

On your local machine (Linux, macOS, or Windows WSL):

bash
ssh-keygen -t ed25519 -C "your@email.com"

When prompted for a file location, accept the default (~/.ssh/id_ed25519). Set a strong passphrase. This generates two files: id_ed25519 (private key, never share this) and id_ed25519.pub (public key, this goes on the server).

View your public key:

bash
cat ~/.ssh/id_ed25519.pub

Copy the output. You will paste it into your provider's control panel when creating the server.

Create your provider account

Register at your chosen provider. Use a strong, unique password and enable two-factor authentication on the account immediately. Your provider account controls your server; it is a high-value target.

Plan your firewall rules before you need them

Write down which ports you intend to open. At minimum you need:

  • 22/TCP: SSH (you will change this to a non-standard port at T+1)
  • 80/TCP: HTTP (required for Let's Encrypt certificate issuance)
  • 443/TCP: HTTPS

Everything else stays closed until you have a specific reason to open it.

LaunchT+0

VPS creation and first login

Create the server through your provider's control panel. Select your chosen OS, paste your public SSH key when prompted, and note the server's IPv4 address once it provisions (usually under two minutes).

First login

Most providers provision with a root user. Log in:

bash
ssh root@YOUR_SERVER_IP

You are in. Now do not do anything else yet. The server is currently open to the world on every port. Your first job is hardening, not installing software.

Set the hostname

bash
hostnamectl set-hostname yourservername

Set the timezone

bash
timedatectl set-timezone Europe/London

Check the provider firewall

Most providers offer a network-level firewall (Hetzner calls it a Firewall, OVH calls it a Security Group) that sits in front of your server. Enable it now and configure it to allow only ports 22, 80, and 443 inbound. This is your first line of defence and it costs nothing extra.

Do not skip the provider-level firewall. Even with ufw running on the server itself, a network firewall stops traffic before it reaches your machine. Two layers are better than one.

DayT+1

Basic hardening: users, SSH, firewall, fail2ban

This is the most important day. A server skipping this step is not a server you should put data on. Take your time here.

1. Create a non-root admin user

Running everything as root is dangerous. Create a dedicated admin user:

bash
adduser adminusername
usermod -aG sudo adminusername

Copy your SSH public key to the new user:

bash
mkdir -p /home/adminusername/.ssh
cp /root/.ssh/authorized_keys /home/adminusername/.ssh/
chown -R adminusername:adminusername /home/adminusername/.ssh
chmod 700 /home/adminusername/.ssh
chmod 600 /home/adminusername/.ssh/authorized_keys

Open a second terminal and verify you can log in as the new user before closing the root session:

bash
ssh adminusername@YOUR_SERVER_IP

2. Harden SSH

Edit the SSH daemon configuration:

bash
sudo nano /etc/ssh/sshd_config

Set or confirm these values:

/etc/ssh/sshd_config
Port 2222
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
X11Forwarding no
MaxAuthTries 3
LoginGraceTime 20

Changing the port to something non-standard (2222 is common, pick your own) eliminates the vast majority of automated SSH brute-force attempts. Update your provider firewall to allow the new port and remove port 22.

Restart SSH and verify the new config works before logging out:

bash
sudo systemctl restart sshd

Test from a new terminal: ssh -p 2222 adminusername@YOUR_SERVER_IP

3. Configure ufw (Uncomplicated Firewall)

bash
sudo apt install ufw -y
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 2222/tcp comment 'SSH'
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
sudo ufw enable
sudo ufw status verbose

You now have two firewall layers: the provider-level network firewall and ufw on the server itself. Both enforce the same rules.

4. Install and configure fail2ban

fail2ban monitors log files for repeated failed authentication attempts and bans the offending IP addresses automatically.

bash
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Create a local configuration file (never edit the default; it gets overwritten on updates):

bash
sudo nano /etc/fail2ban/jail.local

Paste:

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

[sshd]
enabled  = true
port     = 2222
logpath  = %(sshd_log)s
bash
sudo systemctl restart fail2ban
sudo fail2ban-client status

5. Install essential utilities

bash
sudo apt update && sudo apt install -y \
  htop \
  curl \
  wget \
  git \
  unzip \
  build-essential \
  ca-certificates \
  gnupg \
  lsb-release \
  net-tools \
  dnsutils \
  ncdu \
  tmux

htop gives you a live view of CPU, memory, and processes. ncdu shows disk usage interactively. tmux keeps sessions alive if your SSH connection drops during a long installation. build-essential provides the C compiler toolchain needed by many packages. The rest are networking and utility staples you will reach for constantly.

DayT+2

System updates and security tooling

Before installing any application software, get the OS fully patched and automatic security updates in place.

1. Full system update

bash
sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y

Reboot if the kernel was updated:

bash
sudo reboot

Reconnect after 30 seconds and confirm the new kernel is running:

bash
uname -r

2. Enable unattended security updates

bash
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades

Select "Yes" when prompted. This enables automatic installation of security patches only, leaving you in control of everything else.

Verify the configuration:

bash
cat /etc/apt/apt.conf.d/20auto-upgrades

You should see:

/etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

3. Install logwatch for daily log summaries

bash
sudo apt install logwatch -y

Configure it to email you a daily summary:

bash
sudo nano /etc/logwatch/conf/logwatch.conf

Set:

/etc/logwatch/conf/logwatch.conf
Output = mail
MailTo = you@yourdomain.com
MailFrom = logwatch@yourservername
Detail = Low
Service = All

You will need a working mail setup for this to send email. As an alternative, run sudo logwatch --output stdout --detail low manually each week to review what your server has been doing.

4. Check for open ports

Verify that only the ports you expect are listening:

bash
sudo ss -tlnp

You should see only SSH (on your custom port), and nothing else at this stage. If you see unexpected services, investigate before continuing.

5. Set up automatic backups

Before installing any application software, configure backups. The sequence matters: back up before you put data on it, not after.

Enable your provider's snapshot feature and set it to daily. This typically costs £1 to £3/month and gives you a full server restore point.

For application-level backups, install a simple cron-based script. Create the backup directory:

bash
sudo mkdir -p /var/backups/local
sudo chown adminusername:adminusername /var/backups/local

Add a daily backup job to your crontab:

bash
crontab -e
crontab
# Daily backup of home directory and /etc at 02:00
0 2 * * * tar -czf /var/backups/local/home-$(date +\%F).tar.gz /home/adminusername
0 2 * * * tar -czf /var/backups/local/etc-$(date +\%F).tar.gz /etc
# Keep only last 7 days
0 3 * * * find /var/backups/local -name "*.tar.gz" -mtime +7 -delete

Offsite backups come later when you have actual data to protect, but the habit starts now.

DayT+3

Web server, Docker, SSL, and your first site

The server is hardened, patched, and backed up. Now you install application software. The order still matters: web server first, SSL second, applications third.

1. Install Apache

bash
sudo apt install apache2 -y
sudo systemctl enable apache2
sudo systemctl start apache2

Enable the modules you will need:

bash
sudo a2enmod rewrite headers ssl proxy proxy_http

2. Configure your domain's DNS

At your domain registrar, create an A record pointing your domain to your server's IPv4 address. If you want to also handle IPv6, add an AAAA record pointing to your server's IPv6 address. DNS propagation takes between a few minutes and 48 hours depending on your registrar and TTL settings. You cannot issue an SSL certificate until the A record resolves correctly.

Verify propagation:

bash
dig +short yourdomain.com

Once it returns your server's IP, proceed.

3. Issue an SSL certificate with Certbot

bash
sudo apt install certbot python3-certbot-apache -y
sudo certbot --apache -d yourdomain.com -d www.yourdomain.com

Certbot will modify your Apache configuration automatically and set up auto-renewal. Verify renewal works:

bash
sudo certbot renew --dry-run

Certificates expire after 90 days. Certbot installs a systemd timer that renews automatically. Confirm it is active:

bash
sudo systemctl status certbot.timer

4. Harden Apache

Edit the Apache security configuration:

bash
sudo nano /etc/apache2/conf-available/security.conf

Set or confirm:

/etc/apache2/conf-available/security.conf
ServerTokens Prod
ServerSignature Off
TraceEnable Off

Add a security headers file:

bash
sudo nano /etc/apache2/conf-available/security-headers.conf
/etc/apache2/conf-available/security-headers.conf
<IfModule mod_headers.c>
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set X-XSS-Protection "1; mode=block"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
</IfModule>
bash
sudo a2enconf security security-headers
sudo systemctl reload apache2

5. Install Docker

Many applications (Mattermost, Vaultwarden, Grafana, Nextcloud) run most reliably in Docker containers. Install it using the official repository, not the version in the Ubuntu/Debian repos (it is usually outdated):

bash
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Add your admin user to the docker group so you do not need sudo for every docker command:

bash
sudo usermod -aG docker adminusername
newgrp docker

Verify:

bash
docker run hello-world

6. Deploy a basic website

Place your site files in the web root:

bash
sudo mkdir -p /var/www/yourdomain.com/html
sudo chown -R adminusername:www-data /var/www/yourdomain.com
sudo chmod -R 755 /var/www/yourdomain.com

Create an Apache virtual host:

bash
sudo nano /etc/apache2/sites-available/yourdomain.com.conf
/etc/apache2/sites-available/yourdomain.com.conf
<VirtualHost *:80>
    ServerName yourdomain.com
    ServerAlias www.yourdomain.com
    Redirect permanent / https://yourdomain.com/
</VirtualHost>

<VirtualHost *:443>
    ServerName yourdomain.com
    ServerAlias www.yourdomain.com
    DocumentRoot /var/www/yourdomain.com/html

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.com/privkey.pem

    <Directory /var/www/yourdomain.com/html>
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/yourdomain.com-error.log
    CustomLog ${APACHE_LOG_DIR}/yourdomain.com-access.log combined
</VirtualHost>
bash
sudo a2ensite yourdomain.com.conf
sudo a2dissite 000-default.conf
sudo apache2ctl configtest
sudo systemctl reload apache2

Visit https://yourdomain.com in a browser. You should see your site over HTTPS with a valid certificate. HTTP requests should redirect to HTTPS automatically.

At this point you have a hardened, patched, firewall-protected VPS running Apache with a valid SSL certificate, automatic renewal, security headers, Docker installed, and daily backups configured. That is a production-ready foundation. Everything else you want to run (WordPress, Nextcloud, Mattermost, Vaultwarden, Grafana) installs on top of this.

What comes next

The timeline above ends with a secure, working web server and Docker ready to go. The next steps depend on what you actually want to run. Installing WordPress takes about 20 minutes on this foundation. Nextcloud takes a similar amount of time. Mattermost, Vaultwarden, and Grafana all have official Docker images with documented setup procedures.

The pattern for each application is the same: pull the Docker image, configure a reverse proxy in Apache, point your domain, issue a certificate. Once you have done it once, the second and third installations are much faster.

Ongoing maintenance

A well-configured server does not demand much. Budget roughly one to two hours per month for:

Unattended upgrades handle security patches automatically. Application updates (Docker containers, WordPress plugins, etc.) you manage yourself on your own schedule.

If any step in this guide is unclear, or if something has gone differently on your server than described here, get in touch. Debugging other people's setups is something we do regularly, and it is usually faster than it looks.

Want help working through this?

We guide you through the whole process, step by step, and hand over a server you understand and control.

Get in touch