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.

Why I did this

I got tired of setting up servers by hand. Every time I needed a new VPS, I'd spend an afternoon installing Docker, configuring Nginx, setting up SSL, and then forgetting half the steps six months later. So I automated the whole thing with Ansible. Now I can tear down a server and rebuild it in under ten minutes.

What's running on it

Everything is containerized. Here's the full stack:

  • Docker - all services in containers
  • Nginx - reverse proxy and TLS termination
  • Certbot - SSL via Let's Encrypt
  • OAuth2 Proxy + Keycloak - auth in front of anything sensitive
  • Grafana + Prometheus + Node Exporter - monitoring
  • GitHub Actions Runner - self-hosted CI
  • UFW + Fail2Ban - firewall and brute-force protection
  • Watchtower - auto-updates Docker images

The Ansible setup

Each service is its own role. The playbook is idempotent - I can run it ten times and it only changes what needs changing.

Auth: OAuth2 Proxy + Keycloak

Instead of every app handling its own auth, I put OAuth2 Proxy in front of them. Each app config has a simple toggle:

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

When oauth: true, the playbook generates an Nginx config with an embedded OAuth2 block. Only authenticated users get through.

SSL

Certbot handles SSL automatically. I just give it a domain and email. Ansible does the rest.

Firewall

UFW is configured dynamically based on which apps are deployed. OAuth2-protected apps only get firewall rules if the toggle is on. Node Exporter binds to localhost only. Fail2Ban catches SSH brute-force attempts.

Monitoring

Prometheus scrapes Node Exporter and itself. Grafana visualizes everything and authenticates through Keycloak SSO. Watchtower keeps images updated without me thinking about it.

GitHub Actions Runner

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

Deploying

bash
ansible-playbook -i inventory site.yml

That's it. The whole server from zero.

What's still on the list

  • Backup and restore routines for volumes and secrets
  • Alertmanager for Prometheus alerts
  • Zero-trust networking, maybe
Stefan Mikic
Stefan Mikicdata gardener

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