Homelab Setup Part 1: How I run my vServer
Running and managing a virtual server (vServer) can be both fun and rewarding. Over time, I’ve streamlined my setup to reduce repetitive tasks and make deployments as seamless as possible. This is the first part of a series where I’ll share how I use Ansible, Docker, and some self-hosted apps to keep everything running smoothly while automating key tasks.
This setup might not be the most advanced or sophisticated, but as a wise man once said, “I love it when a plan comes together.” I’m not using any fancy kubernetes clusters (not that I would know how to do it in first place), it’s simply a straightforward and reliable solution that simply gets the job done for me. Whether you’re setting up your first vServer or looking for an efficient way to manage deployments, this approach balances simplicity and functionality effectively with an manageable amount of efforts.
Overview
Here’s a high-level overview of my setup:
- General configuration: Using ansible to configure the base system and ensures consistent environment setup.
- Applications: All my apps are deployed via docker, keeping them isolated and easy to manage.
- Security Considerations: Basic settings focusing on enhancing the overall security posture.
Automating Server Configuration with Ansible
Ansible is a great tool for automating server configuration. I’ve created a playbook that contains but not limited to:
- Installs essential packages (e.g., Docker, Docker Compose, and system utilities).
- Sets up a firewall using
ufw. - Ensures system updates are applied regularly.
- Applying various configurations (e.g. logrotate).
- and other stuff.
The main idea is not to configure anything locally on the server, but only via dedicated tasks within the ansible playbook.
Here is an example of some of these tasks:
---
- hosts: vserver
become: true
tasks:
- name: Update and upgrade apt packages
apt:
update_cache: yes
upgrade: dist
- name: Configure UFW firewall
ufw:
rule: allow
port: 2222
proto: tcp
- name: Logorate traefik access logs
tags:
- traefik
dest: /etc/logrotate.d/traefik-access-logs
content: |
/docker/reverse-proxy/logs/access.log {
weekly
rotate 52
compress
missingok
notifempty
copytruncate
}
To apply the configuration, I simply run:
ansible-playbook -i inventory.ini vserver_init.yml
This ensures my vServer is always in a consistent state. This approach can be easily applied to any vServer, which I’ve done multiple times as I switched hosting providers in the past.
Note: Yes, I know changing the default ssh port does not improve the overall security posture,
however it at least prevents the generic ssh bf attempts against the default 22 port.
Applications
All applications are deployed via docker in their respective /docker/<app>/compose.yaml file.
My configuration how I deploy this blog is as follows:
---
services:
blog:
image: git.<private>.com/neok/blog:latest
restart: unless-stopped
networks:
- reverse-proxy-net
deploy:
replicas: 2
labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`<this blog host>`)"
- "traefik.http.routers.blog.entrypoints=websecure"
- "traefik.http.routers.blog.tls.certresolver=myresolver"
- "traefik.http.routers.blog.middlewares=auth"
- "com.centurylinklabs.watchtower.enable=true"
networks:
reverse-proxy-net:
external: true
There are few interesting things worth highlighting. As shown above I’m using Traefik as main reverse proxy in front of any application I host. I never expose any ports from these applications directly, but only via Traefik.
For this I created a dedicated reverse-proxy-net network which I can simply add to any application compose.yaml file including the required labels.
As additional label, I enabled watchtower monitoring for this service. The following section will go in some more details about the purpose and configuration.
Traefik
To manage incoming traffic and secure my applications, I use Traefik as a reverse proxy. Since I use Cloudflare to protect and hide my vServer, I configured Traefik to include Cloudflare’s custom headers in the access logs. This setup ensures the logs accurately reflect visitor IPs rather than Cloudflare’s proxy IPs. Here’s how I set it up:
---
services:
reverse-proxy:
image: traefik:v3.1
container_name: reverse-proxy
restart: unless-stopped
command:
- "--providers.docker"
- "--accesslog=true"
- "--accesslog.filepath=/logs/access.log"
- "--accesslog.format=json"
- "--accesslog.fields.defaultmode=drop"
- "--accesslog.fields.names.RequestMethod=keep"
- "--accesslog.fields.names.DownstreamStatus=keep"
- "--accesslog.fields.names.OriginStatus=keep"
- "--accesslog.fields.names.RequestPath=keep"
- "--accesslog.fields.names.ClientUsername=keep"
- "--accesslog.fields.names.RequestAddr=keep"
- "--accesslog.fields.headers.names.Authorization=keep"
- "--accesslog.fields.headers.names.Cf-Ipcountry=keep"
- "--accesslog.fields.headers.names.Cf-Connecting-Ip=keep"
- "--accesslog.fields.headers.names.X-Custom-Uptime=keep"
- "--providers.docker.network=reverse-proxy-net"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.dnschallenge=true"
- "--certificatesresolvers.myresolver.acme.dnschallenge.provider=cloudflare"
- "[email protected]"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
ports:
- "80:80"
- "443:443"
env_file:
- .env
volumes:
- letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock
- ./logs:/logs
networks:
- reverse-proxy-net
volumes:
letsencrypt:
networks:
reverse-proxy-net:
external: true
I’m not sure if this is the best way, but this configuration provides an access log format that includes the most relevant details for my needs. Since Traefik is my main reverse proxy, I’ve set it up to automatically manage SSL certificates. This is done using the powerful combination of the Cloudflare DNS challenge and Let’s Encrypt, ensuring seamless certificate issuance and renewal.
Watchtower
Watchtower is a tool that monitors Docker containers and updates them when new images are available. I’ve set up Watchtower as a container itself:
---
services:
watchtower:
image: containrrr/watchtower
restart: unless-stopped
command:
- "--label-enable"
- "--interval"
- "30"
- "--rolling-restart"
- "--no-pull"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
With Watchtower, I don’t need to manually update containers. I use the --no-pull setting since I rely solely on locally built images, ensuring Watchtower doesn’t attempt to pull images from external registries. Additionally, the --rolling-restart setting is enabled, which ensures zero downtime when updating containers. This is particularly useful for apps like this blog, where two containers are deployed to maintain availability during updates. Not, that it would matter for my blog visitors..
Security Considerations
To ensure my vServer is secure and resilient, I’ve implemented few foundational security measures. I’m well aware that these will not protect against any known attack vector especially for an persistent attacker. As well as I’m aware that these are no groundbreakingm never seen concpets. However, for me following measuers are mandatory and the minial requirements for any public facing server.
HTTPS-Only Access
All applications on my server are accessible exclusively via HTTPS. Using Let’s Encrypt certificates, Traefik automatically handles certificate issuance and renewal, ensuring secure communication between clients and the server.
SSH Hardening
While not groundbreaking, I’ve implemented well-known SSH best practices, such as disabling password-based authentication and permitting access only via public key authentication, disabled root user login etc.
Automated Updates
To keep my server secure, all critical updates—whether for the operating system or applications—are applied automatically. This reduces the attack surface by ensuring vulnerabilities are (hopefully) patched promptly, and not introduced (looking at you xz…).
Intrusion Detection with CrowdSec and Fail2Ban
To protect against common scanners and brute force attempts, I use CrowdSec and Fail2Ban. These tools monitor access logs for open protocols like HTTP (via Traefik logs) and SSH, blocking suspicious activity and potential attackers.
Cloudflare Integration
I leverage Cloudflare’s free services to add an extra layer of protection. Cloudflare provides DDoS mitigation, proxies incoming requests to hide my server’s real IP address, and enhances overall security. This integration complements my server’s defenses.
Dockerized Applications
One of the key security measures in my setup is the use of rootless Docker containers for all applications exposed to external users. While this approach can be controversial, it adds an additional layer of isolation at least for my conscious, acting as a “jail” for potential threat actors. By running these applications in rootless containers, even if an attacker manages to exploit an application vulnerability, they are confined to the container environment. This significantly reduces the risk of the attacker gaining access to the underlying host system.
Simplified Backups
For backups, I rely on my hosting provider’s built-in solution, which offers a straightforward way to recover data in case of emergencies. While this approach suffices for now, I plan to implement custom backup solutions for specific app databases in the future as my needs evolve
Conclusion
By combining Ansible, Docker, Traefik and Watchtower, I’ve created an efficient and automated workflow for managing my vServer. This setup reduces manual intervention, keeps my apps up-to-date, and ensures a consistent development and deployment experience. This write-up concludes part one of my upcoming series of my homelab setup. Stay tuned (or not) for the future parts describing my private homelab environment and my approach to create custom applications. It is also very likely that I forgot about the one or the other thing here, so I may write some deep dives into specific topics.