- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| caddy | ||
| forgejo | ||
| .gitignore | ||
| deploy.py | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
Bazinga Infra
A pyinfra-based provisioning and deployment system for self-hosted services on a VPS.
Architecture
This stack uses:
- pyinfra for initial VPS provisioning and service deployment
- Podman for container runtime
- Quadlets (systemd-integrated container definitions) for service lifecycle management
- Systemd for orchestration and supervision of all services
Each service is defined as a .container quadlet in /etc/containers/systemd/, which systemd automatically converts to a native unit file. This means all services can be managed like any other systemd service: systemctl start, systemctl status, automatic restarts on failure, and simple enable/disable.
Services
Reverse Proxy (Caddy)
Why Caddy?
- Automatic HTTPS with ACME (Let's Encrypt)
- Zero-config TLS by default
- Simple config syntax for routing and proxying
- Built-in support for plug-and-play site config fragments
- Lightweight and suitable for small VPS footprints
How it runs:
- Deployed as a Podman quadlet:
caddy.container - Systemd service:
caddy.service - Configuration:
/etc/caddy/Caddyfile(main) +/etc/caddy/conf.d/*.caddy(per-service snippets) - Persists TLS certificates and runtime state in Podman named volumes
- Exposed on ports 80/443 (public)
Management:
systemctl status caddy
systemctl restart caddy
Git Hosting (Forgejo)
Why Forgejo?
- Lightweight fork of Gitea
- SQLite database (no separate DB service needed)
- Git over SSH and HTTPS
- Web UI for repository management
- Large File Storage (LFS) support
- Minimal resource footprint for small teams
How it runs:
- Deployed as a Podman quadlet:
forgejo.container - Systemd service:
forgejo.service - Runs on container network:
caddy.network - Configuration via environment variables (FORGEJO__*)
- Data persists in one Podman named volume:
forgejo-data - SSH exposed on host port 2222 (mapped from container port 22)
- Clone with:
git clone ssh://git@code.confraria.dev:2222/owner/repo.git
- Clone with:
- Web UI accessible through Caddy on port 443
- Quadlet allows automatic restarts and graceful shutdown
Management:
systemctl status forgejo
systemctl logs forgejo
podman volume list | grep forgejo # Check named volumes
Container Network (caddy.network)
Why a shared network?
- Services need to communicate internally (e.g., Caddy ↔ Forgejo)
- Podman networks provide DNS service discovery by container name
- Cleaner than exposing services on the host network
- Allows easy addition of new services without port conflicts
How it runs:
- Defined as a Podman quadlet:
caddy.network - Created and managed by systemd
- Services reference it in their
.containerfiles viaNetwork=caddy.network - Caddy can reach Forgejo via
reverse_proxy forgejo:3000in the Caddyfile
Provisioning Workflow
Step 1: Initial VPS Bootstrap
Run against a freshly provisioned VPS (as root or a user with passwordless sudo):
cd bazinga-infra
uv sync
uv run pyinfra inventory.py deploy.py
This:
- Creates an admin user with SSH key and passwordless sudo
- Hardens SSH (disables root login, password auth)
- Installs base packages:
podman,tmux,lnav,htop,rclone,restic
Step 2: Deploy Caddy
After bootstrap, update inventory.py to use the new admin user, then:
uv run pyinfra --sudo admin@host caddy/deploy.py
This:
- Creates
/etc/caddyand/srv/caddydirectories - Places the Caddyfile and quadlets
- Registers
caddy.networkandcaddy.servicewith systemd - Starts Caddy
Step 3: Deploy Forgejo
uv run pyinfra --sudo admin@host forgejo/deploy.py
This:
- Places the Forgejo quadlet
- Creates the Caddy snippet to proxy traffic to Forgejo
- Registers
forgejo.servicewith systemd - Starts Forgejo
- Reloads Caddy
Adding a New Service
To add a new service to the stack:
-
Create service files:
servicename/deploy.py– pyinfra deployment scriptservicename/servicename.container– Podman quadletservicename/servicename.caddy– Caddy reverse proxy snippet (if public)
-
In
deploy.py:- Create any host directories and permissions needed
- Use
files.put()to deploy the.containerand.caddyfiles - Use
systemd.daemon_reload()to register the quadlet - Use
server.service()to start/restart the service
-
In
servicename.container:- Set
Network=caddy.networkto join the internal network - Define
PublishPortonly for public-facing ports (e.g., SSH, custom APIs) - Use
Volume=for named volumes (persistent data) or bind mounts - Set environment variables for configuration
- Use
AutoUpdate=registryto auto-pull new images
- Set
-
In
servicename.caddy:- Define the domain and reverse proxy to the container by name
- Example:
reverse_proxy servicename:8080
Systemd Integration
All services are managed via systemd. Common commands:
# View service status
systemctl status caddy
systemctl status forgejo
# View logs
journalctl -u caddy -f # Follow live logs
journalctl -u caddy --since "1 hour ago"
# Restart a service
systemctl restart caddy
# Enable/disable autostart
systemctl enable forgejo
systemctl disable forgejo
# Check all running services
systemctl list-units --type service --state running
Data Persistence
All persistent data is stored outside containers in one of these ways:
-
Podman named volumes: For application data (e.g., Forgejo repos and database)
- Located in
/var/lib/containers/storage/volumes/ - Backed up as a unit
- Example:
forgejo-data
- Located in
-
Host bind mounts: For configuration files
- Located in
/etc/or/srv/ - Tracked in the repo or backed up separately
- Example:
/etc/caddy/Caddyfile,/srv/forgejo/data
- Located in
Backup strategy: Include all Podman named volumes and /srv/ in your backup routine.
Planned Services
- Monitoring and alerting
- Media or streaming platform
- Document management or notes
- File sharing or sync service
- Home automation integrations