8.0 KiB
Server Infrastructure — Implementation Guide
Ansible-based provisioning and deployment for homelab web servers.
Architecture
flowchart TB
subgraph provision ["Provisioning (manual / rare)"]
Desktop1["Ubuntu Desktop\n(control node)"]
Desktop1 -->|ansible-playbook site.yml| Adama
Desktop1 -->|ansible-playbook site.yml| Roslin
Desktop1 -->|ansible-playbook site.yml| DesktopTarget
end
subgraph cicd ["CI/CD (every merge to master)"]
Gitea["Gitea push"]
Gitea --> Tests["Act: unit tests"]
Tests --> Deploy["Act: deploy job"]
Deploy --> AnsibleDeploy["ansible-playbook deploy-apps.yml"]
AnsibleDeploy --> Adama2["adama"]
AnsibleDeploy --> Roslin2["roslin"]
AnsibleDeploy --> Desktop2["desktop"]
end
| Pipeline | When | Playbook | Where it runs |
|---|---|---|---|
| Provision | New VM, OS change, firewall, Docker install | site.yml |
Desktop — run manually |
| Deploy | Green unit tests on master |
deploy-apps.yml |
Gitea Act runner on desktop |
Both pipelines share the same inventory (inventory/hosts.yml).
Servers
| Name | IP | Role |
|---|---|---|
| adama | 10.0.0.77 | Ubuntu Server VM (Proxmox) |
| roslin | 10.0.0.176 | Ubuntu Server VM (Proxmox) |
| desktop | see host_vars/desktop.yml |
Ubuntu Desktop — control node + deployment target |
Hostname on this machine: ryan-development-1
Repo Layout
server-infra/
├── IMPLEMENTATION.md # This file
├── README.md # Quick start
├── ansible.cfg
├── requirements.yml # Ansible Galaxy collections
├── inventory/
│ ├── hosts.yml
│ ├── group_vars/
│ │ ├── all.yml
│ │ └── webservers.yml
│ └── host_vars/
│ └── desktop.yml
├── playbooks/
│ ├── site.yml # Phase 1: provision
│ └── deploy-apps.yml # Phase 2: CI deploy (stub)
├── roles/
│ ├── common/ # Base packages
│ ├── ufw/ # Firewall
│ ├── docker/ # Docker CE + compose plugin
│ └── app-deploy/ # App deploy (stub for Phase 2)
└── scripts/
├── provision.sh # Wrapper with --limit support
└── deploy.sh # Wrapper for deploy playbook
Prerequisites (One-Time Bootstrap)
Ansible needs SSH + sudo on each target before playbooks work.
- Create
westfarnon each VM with sudo membership. - Copy your SSH public key from the desktop:
ssh-copy-id westfarn@10.0.0.77 ssh-copy-id westfarn@10.0.0.176 - Confirm passwordless SSH:
ssh westfarn@10.0.0.77 ssh westfarn@10.0.0.176 - On the desktop (control node), install Ansible:
sudo apt update && sudo apt install -y ansible # or: pip install ansible - Install Galaxy collections:
cd ~/Documents/repos/server-infra ansible-galaxy collection install -r requirements.yml - Update
inventory/host_vars/desktop.ymlwith this machine's LAN IP.
Testing on a Single Server
Use --limit to target one host without touching the others. Helper scripts wrap this.
Ping one host
./scripts/provision.sh adama --check # dry run
ansible adama -m ping
Provision one host
# Dry run (no changes)
./scripts/provision.sh adama --check
# Apply for real
./scripts/provision.sh adama
# Same for other hosts
./scripts/provision.sh roslin
./scripts/provision.sh desktop
Provision all hosts
./scripts/provision.sh
Deploy to one host (Phase 2)
./scripts/deploy.sh adama
./scripts/deploy.sh --check roslin
Under the hood, scripts pass --limit <hostname> to ansible-playbook.
Phase 1: Provision (site.yml)
Applies roles in order to the webservers group:
| Role | Purpose |
|---|---|
common |
apt update, git, python3, pip, curl, ca-certificates |
ufw |
Firewall: SSH from LAN only, HTTP/HTTPS public |
docker |
Docker CE, compose plugin, add westfarn to docker group |
UFW rules
| Port | Source | Purpose |
|---|---|---|
| 22 | 10.0.0.0/24 |
SSH (LAN only) |
| 80 | anywhere | HTTP |
| 443 | anywhere | HTTPS |
| default | deny incoming | Block everything else |
Warning: Test UFW on one host first (./scripts/provision.sh adama). Keep a Proxmox console session open in case SSH rules lock you out.
After Docker install, re-SSH so the docker group membership takes effect.
Phase 2: CI Deploy (deploy-apps.yml)
Not fully implemented yet. Planned flow:
- Gitea push triggers unit tests.
- On success, Act runner on desktop runs
deploy-apps.yml. - Ansible fans out to all
webservershosts.
Planned company_site workflow change
# company_site/.gitea/workflows/deploy.yml (future)
jobs:
deploy:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
with:
ref: ${{ gitea.event.workflow_run.head_sha }}
- name: Deploy to all webservers
run: |
~/Documents/repos/server-infra/scripts/deploy.sh \
--extra-vars "app_ref=${{ gitea.event.workflow_run.head_sha }}"
Planned app-deploy role (post-dockerize)
Per host:
- Clone or pull app repo at pinned SHA.
docker compose pull && docker compose up -d.- Optional health check.
Pre-dockerize interim: role can rsync/systemd like current company_site/scripts/deploy.sh.
Gitea Act Runner
Recommended: Single self-hosted runner on the desktop.
- One build artifact, one orchestration point.
- VMs only run containers; no runner needed on them for deploy fan-out.
- Runner needs: Ansible, this repo checked out, SSH key to all hosts, vault password (later).
Runner requirements on desktop
| Requirement | Why |
|---|---|
| Ansible | Run deploy-apps.yml |
server-infra checkout |
Playbooks + inventory |
| SSH key to all hosts | Including loopback to desktop |
| Docker | Build images before push to hosts (Phase 2) |
SSH Keys for CI Deploy
| Key | Used by | Purpose |
|---|---|---|
| Personal key | You | Manual provisioning |
| Deploy key (runner) | Act → Ansible → hosts | Automated deploy |
Consider a dedicated deploy user with limited sudo (docker only) — future hardening step.
Secrets (Phase 2)
Use Ansible Vault for production secrets. Do not commit plaintext.
ansible-vault create inventory/group_vars/webservers/vault.yml
ansible-playbook playbooks/site.yml --ask-vault-pass
Store vault password for CI in a file readable only by the Act runner (e.g. ~/.ansible-vault-pass, mode 600).
Implementation Order
| # | Task | Status |
|---|---|---|
| 1 | Create server-infra repo |
Done |
| 2 | Inventory with all 3 hosts | Done — update desktop IP |
| 3 | Bootstrap SSH to adama + roslin | Manual |
| 4 | site.yml → common, ufw, docker |
Done |
| 5 | Verify ansible webservers -m ping |
Manual |
| 6 | Test on single server: ./scripts/provision.sh adama |
Manual |
| 7 | Provision all: ./scripts/provision.sh |
Manual |
| 8 | Deploy SSH key for Act runner | Future |
| 9 | Stub deploy-apps.yml + update company_site workflow |
Future |
| 10 | Dockerize company_site |
Future (separate ticket) |
| 11 | Gitea container registry (optional) | Future |
Open Decisions
- Desktop LAN IP — set in
inventory/host_vars/desktop.yml. - Same app on all three? — prod mirror vs adama=prod / roslin=staging / desktop=dev.
- Deploy user —
westfarnvs dedicateddeployfor CI. - Gitea URL — for clone URLs in
app-deployrole. - Reverse proxy — Caddy/nginx on host before containers? Affects Phase 2.
Adding a New VM
- Add host to
inventory/hosts.ymlunderwebservers. - Bootstrap SSH:
ssh-copy-id westfarn@<new-ip>. - Test:
./scripts/provision.sh <hostname> --check. - Provision:
./scripts/provision.sh <hostname>. - Deploys automatically include new host once in
webserversgroup.