- Jinja 52.2%
- Python 47.8%
- Import install_weewx from tasks/weewx.py - Add as Phase 5 (after Apache, before Docker) - Renumber subsequent phases (6→8) |
||
|---|---|---|
| files | ||
| group_data | ||
| tasks | ||
| .gitignore | ||
| deploy.py | ||
| inventory.py | ||
| pyproject.toml | ||
| README.md | ||
Infrastructure
Personal server infrastructure managed with pyinfra.
Prerequisites
- Python 3.11+
- SSH access to the target server (as
rootfor initial bootstrap) - OrbStack (optional, for local testing)
Setup
# Create a virtual environment
python3 -m venv .venv
source .venv/bin/activate
# Install dependencies (pyinfra + jinja2 + paramiko)
pip install -e .
Usage
Full provision (all 7 phases)
pyinfra inventory.py deploy.py --sudo -y
Target a specific host or group
# Development VM (OrbStack)
pyinfra inventory.py deploy.py --limit "192.168.139.47" --sudo -y
# Production
pyinfra inventory.py deploy.py --limit "my-server.local" --sudo -y
Run individual task files
# Bootstrap base system
pyinfra inventory.py tasks/bootstrap.py --sudo
# Security hardening
pyinfra inventory.py tasks/security.py --sudo
# Mail (requires secrets.py)
pyinfra inventory.py tasks/mail.py --sudo
# Web server (Apache + ModSecurity + Certbot)
pyinfra inventory.py tasks/web.py --sudo
# Docker
pyinfra inventory.py tasks/docker.py --sudo
# Docker services (FreshRSS, Forgejo)
pyinfra inventory.py tasks/services.py --sudo
# GoBackup (requires secrets.py)
pyinfra inventory.py tasks/backup.py --sudo
# weeWX (standalone)
pyinfra inventory.py tasks/weewx.py --sudo
Secrets
# Create your secrets file (one time only)
cp group_data/secrets.example.py group_data/secrets.py
# Edit group_data/secrets.py with your real credentials
Cloudflare setup
This server requires Cloudflare for HTTP/HTTPS. UFW blocks all traffic to ports 80/443 except from Cloudflare IP ranges.
DNS configuration
| Record | Type | Target | Proxy |
|---|---|---|---|
git.aralnoth.cc |
A | <server-ip> |
✅ Proxied (orange cloud) |
rss.aralnoth.cc |
A | <server-ip> |
✅ Proxied (orange cloud) |
weather.aralnoth.cc |
A | <server-ip> |
✅ Proxied (orange cloud) |
All three must be Proxied (orange cloud). If any is set to DNS-only (gray cloud), traffic will bypass Cloudflare and be blocked by UFW.
SSL/TLS
Set SSL/TLS encryption mode to Full (not Flexible, not Full Strict) in Cloudflare dashboard → SSL/TLS. This lets Cloudflare proxy HTTPS to your server using the Let's Encrypt certificates you obtain via certbot.
Alternatively, set Flexible if you don't want to run certbot on the server (Cloudflare handles SSL entirely). In that case, Apache only needs port 80. Choose whichever you prefer.
Recommended security settings (Cloudflare dashboard)
- SSL/TLS → Edge Certificates: Always Use HTTPS → ON
- Security → Settings: Challenge Passage → 30 minutes
- Security → WAF: Custom rules for rate-limiting SSH/HTTP if desired
- Speed → Optimization: Auto Minify → CSS/JS/HTML as you prefer
Ports not proxied (direct access)
These ports bypass Cloudflare and connect directly to your server:
- SSH (22): Direct server access
- Forgejo SSH (2222): Git operations over SSH
Operational commands
# Check today's warnings and errors
journalctl -p warning..alert -S today
# Check specific services
journalctl -u ssh -u fail2ban -u apache2 -u docker -S today
# Fail2ban status
fail2ban-client status
fail2ban-client status sshd
fail2ban-client status forgejo-ssh
fail2ban-client status recidive
# Verify everything is working
pyinfra inventory.py tasks/verify.py --sudo
# Logwatch (daily summary)
logwatch --detail Med --mailto root
Deploy phases
| Fase | Tarea | Qué hace |
|---|---|---|
| 1 | bootstrap |
Hostname, admin user, SSH hardening, paquetes base |
| 2 | security |
UFW, fail2ban (8 jails), unattended-upgrades, sysctl, AppArmor, rkhunter, auditd, logrotate |
| 3 | mail |
msmtp MTA para correos de logwatch / rkhunter |
| 4 | web |
Apache, ModSecurity + OWASP CRS, Certbot, 3 virtual hosts |
| 5 | docker |
docker.io + docker-compose (repos Debian) |
| 6 | services |
FreshRSS + Forgejo con Docker Compose |
| 7 | backup |
GoBackup con respaldos a S3 + R2 + local |
Cloudflare (mandatory)
HTTP/HTTPS ports (80/443) solo aceptan tráfico desde los rangos de IP de Cloudflare. Si quitas Cloudflare, el servidor web dejará de ser accesible. SSH y Forgejo SSH (2222) no pasan por Cloudflare — se acceden directamente.
Post-deploy manual steps
# 1. Obtain SSL certificates (required for HTTPS sites)
certbot --apache -d git.aralnoth.cc -d rss.aralnoth.cc -d weather.aralnoth.cc
# 2. Enable SSL sites after certbot
a2ensite forgejo-le-ssl.conf freshrss-le-ssl.conf weewx-le-ssl.conf
systemctl reload apache2
# 3. Install and configure weeWX (if needed)
pyinfra inventory.py tasks/weewx.py --sudo
Project structure
├── pyproject.toml # Dependencies and tool config
├── inventory.py # Hosts and group definitions
├── deploy.py # Main entry point (all 7 phases)
├── group_data/
│ ├── all.py # Public variables
│ ├── secrets.py # 🔒 Sensitive credentials (gitignored)
│ └── secrets.example.py # Template for secrets
├── tasks/
│ ├── bootstrap.py # Hostname, user, SSH, base packages
│ ├── security.py # UFW, fail2ban, sysctl, AppArmor, auditd, ...
│ ├── mail.py # msmtp MTA
│ ├── web.py # Apache, ModSecurity + OWASP CRS, Certbot
│ ├── docker.py # Docker Engine + Compose
│ ├── services.py # FreshRSS + Forgejo Compose
│ ├── backup.py # GoBackup installation and config
│ └── weewx.py # weeWX weather station (standalone)
└── files/
├── backup/ # GoBackup templates
├── docker/ # Docker Compose files
├── ssh/ # SSH config templates
├── fail2ban/ # Fail2ban jail configs
├── apt/ # APT unattended-upgrades configs
├── sysctl/ # Kernel hardening
├── mail/ # msmtp config templates
├── apache/ # Apache virtual hosts
├── weewx/ # weeWX config template
└── logrotate/ # Log rotation configs
Configuration
Edit group_data/all.py to set:
| Variable | Description |
|---|---|
HOSTNAME |
Server hostname |
ADMIN_USER |
Unprivileged admin username |
ADMIN_SSH_KEY |
Public SSH key for the admin user |
SSH_PORT |
SSH port (22 by default) |
TIMEZONE |
Server timezone |
BASE_PACKAGES |
APT packages to install |
Edit group_data/secrets.py (copy from secrets.example.py):
| Variable | Description |
|---|---|
SMTP_* |
Mail credentials for msmtp |
AWS_* |
AWS S3 backup keys |
R2_* |
Cloudflare R2 backup keys |
TELEGRAM_* |
Telegram notification bot |
BACKUP_ENCRYPTION_PASSWORD |
GoBackup encryption password |
WEEWX_* |
weeWX API credentials |