smikic.com
buddingtended June 27, 2026

Automating My VPS Setup with Ansible

NOTE

This post documents how I fully automated my VPS setup with Ansible, from Docker and Nginx to SSL, OAuth2, and GitHub Runners with security and reproducibility in mind.

Key takeaways
  • Ansible roles that are idempotent and modular let you rebuild the entire server from scratch in minutes.
  • Layering OAuth2 Proxy in front of sensitive containers beats per-app authentication schemes.
  • A single ansible-playbook command is a better interface than a runbook full of manual steps.

Infrastructure overview

Everything runs inside containers for isolation. The core stack:

  • Docker — all services containerized
  • Nginx — reverse proxy and TLS termination
  • Certbot — automatic SSL certificates via Let's Encrypt
  • OAuth2 Proxy + Keycloak — centralized authentication in front of sensitive apps
  • Grafana + Prometheus + Node Exporter — monitoring stack
  • GitHub Actions Runner — self-hosted CI
  • UFW + Fail2Ban — firewall and brute-force protection
  • Watchtower — automated Docker image updates

Why Ansible?

Using Ansible means:

  • Everything is defined as code
  • No configuration drift between re-provisions
  • Re-deployments are predictable and safe to run repeatedly

All roles are modular and idempotent. A full server rebuild takes minutes.

Security-first setup

Sed quis custodiet ipsos custodes?

OAuth2 Proxy + Keycloak

Sensitive apps sit behind oauth2-proxy, authenticating through Keycloak. Each app config includes a simple OAuth toggle:

yaml
nginx_apps:
  - domain: subdomain.smikic.com
    port: 69420
    oauth: true

This generates a custom Nginx config with an embedded OAuth2 block. Only authenticated users reach the app.

Let's Encrypt + Certbot

SSL certificates are automatically requested and renewed using Certbot with the Nginx plugin. Ansible handles everything — only a domain and email are required.

Firewall rules via UFW

UFW is configured dynamically:

  • Only required ports are open
  • OAuth2-protected apps get UFW rules only if the toggle is enabled
  • Node Exporter binds only to localhost

Fail2Ban is installed and enabled for SSH brute-force protection.

Monitoring with Grafana and Prometheus

Monitoring runs on a dedicated Docker network:

  • Prometheus scrapes Node Exporter and itself
  • Grafana visualizes metrics and authenticates through Keycloak SSO
  • Node Exporter exposes host metrics with hardened volume mounts
  • Watchtower keeps images up to date automatically

Self-hosted GitHub Actions runner

NOTE

Ansible installs all dependencies, configures the runner, and registers it as a systemd service — so it survives reboots and re-provisions without any manual steps.

Deployment is a single command

Once inventory and vars are set, provisioning the whole VPS is:

bash
ansible-playbook -i inventory site.yml

Re-running this command at any time is safe. Ansible only makes the changes that are needed.

What's next

  • Automatic backup and restore routines for volumes and secrets
  • Alertmanager integration with Prometheus for alerting
  • Extended monitoring coverage
  • Potentially exploring zero-trust networking
Stefan Mikic
Stefan Mikicdata eng

Data is my veggies — healthy, versatile, and sometimes hard to digest, but in the end, it always brings value.