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.
- 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:
nginx_apps:
- domain: subdomain.smikic.com
port: 69420
oauth: trueWhen 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
ansible-playbook -i inventory site.ymlThat'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

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