---
title: "Self-Hosting and Hybrid Cloud: My Personal Infrastructure with Gitea and Scaleway"
date: 2025-11-20
authors:
  - name: Damien
    link: https://gitea.arnodo.fr/Damien
tags:
  - Homelab
  - DevOps
  - Self-Hosting
  - Network Engineering
---

A write-up on building my personal infrastructure: migrating from GitHub to self-hosted Gitea, and deploying ephemeral network labs on Scaleway. All powered by Proxmox and Wireguard.

<!--more-->

## Context and motivations

As a network engineer working in automation and orchestration, I've always needed an environment to experiment and learn in. Until recently, I used GitHub for my code and ContainerLab locally/on AWS via [DevPod](/documentation/devpod/) for my network simulations.

But several needs emerged:

- **Learning**: Deploying and managing a full Git server with CI/CD
- **Sovereignty**: Hosting my data in France (or at least in Europe), controlling my infrastructure
- **Flexibility**: Being able to spin up network labs on demand without saturating my local machine
- **Automation**: Scripting the complete provisioning of my environments

## The overall architecture

![Architecture](global_architecture.en.svg)

### Tech stack

**Homelab**:
- **Proxmox VE**: Hypervisor for VMs and LXC
- **LXC Containers**: Gitea, runners, various services
- **Ansible**: Configuration management and updates
- **Grafana/Prometheus/Loki**: Monitoring and observability

**Public exposure**:
- **Scaleway Dedibox**: Dedicated instance with a fixed IP
- **Nginx Proxy Manager**: Reverse proxy with automatic SSL
- **Wireguard VPN**: Secure tunnel between Scaleway and the homelab

**Scaleway Cloud**:
- **Object Storage**: Hosting for the Hugo blog (S3-compatible)
- **Instances**: Ephemeral network labs provisioned on demand
- **Scaleway CLI**: Full automation

## Part 1: migrating from GitHub to self-hosted Gitea

### Why Gitea?

[Gitea](https://gitea.io/) is a lightweight Git server, perfect for self-hosting:

- **Lightweight**: Ideal for an LXC on Proxmox
- **GitHub Actions compatible**: Migrate workflows without rewriting them
- **Full-featured**: Issues, PRs, CI/CD, webhooks
- **Open-source**: Active community

### Deployment with Proxmox Helper Scripts

Rather than configuring everything manually, I use the excellent [Proxmox Helper Scripts](https://community-scripts.github.io/ProxmoxVE/), which automate the creation of preconfigured LXCs.

**Installing Gitea**:

```bash
# On the Proxmox node, run the script
bash -c "$(wget -qLO - https://github.com/community-scripts/ProxmoxVE/raw/main/ct/gitea.sh)"
```

The script:
- Creates a Debian 12 LXC
- Installs Gitea and sqlite
- Configures the systemd services
- Sets up the environment with the correct permissions

**Post-installation configuration**:
- Web access: `http://<IP_LXC>:3000`
- Initial setup via the interface
- Site URL: `https://gitea.arnodo.fr`

### Network architecture: Wireguard + Nginx Proxy Manager

**The problem**: Gitea runs inside my homelab (private IP), but I want to access it from the Internet.

**The solution**:
1. A **Scaleway Dedibox** with a fixed public IP and Nginx Proxy Manager
2. A **Wireguard VPN** between the Dedibox and the homelab
3. The reverse proxy routes `gitea.arnodo.fr` to the LXC's private IP through the tunnel

**Wireguard configuration (homelab side)**:

```ini
[Interface]
Address = 10.0.0.1/24
PrivateKey = <private_key>
ListenPort = 51820

[Peer]
# Dedibox Scaleway
PublicKey = <dedibox_public_key>
AllowedIPs = 10.0.0.2/32
Endpoint = <dedibox_public_ip>:51820
PersistentKeepalive = 25
```

**Nginx Proxy Manager**:
- Proxy Host: `gitea.arnodo.fr`
- Forward Hostname/IP: `10.0.0.x` (Gitea LXC IP over Wireguard)
- Forward Port: `3000`
- SSL: automatic Let's Encrypt
- Websockets: Enabled

### Management with Ansible

To keep Gitea up to date and manage configurations, I use Ansible.

**Update playbook** (`update-gitea.yml`):

```yaml
---
- name: Update Gitea
  hosts: gitea
  become: yes
  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600

    - name: Upgrade Gitea and system packages
      apt:
        upgrade: dist
        autoremove: yes
        autoclean: yes

    - name: Restart Gitea service
      systemd:
        name: gitea
        state: restarted
        enabled: yes

    - name: Check Gitea version
      command: gitea --version
      register: gitea_version

    - debug:
        msg: "{{ gitea_version.stdout }}"
```

**Execution**:

```bash
ansible-playbook -i inventory.ini update-gitea.yml
```

### Monitoring with Grafana

Gitea exposes Prometheus metrics (https://docs.gitea.com/administration/config-cheat-sheet#metrics-metrics)  
Configuration:

**In Gitea (`app.ini`)**:

```ini
[metrics]
ENABLED = true
TOKEN = <secret_token>
```

**Prometheus scrape config**:

```yaml
scrape_configs:
  - job_name: 'gitea'
    metrics_path: /metrics
    bearer_token: '<secret_token>'
    static_configs:
      - targets: ['<gitea_lxc_ip>:3000']
```

**Grafana dashboard** (https://grafana.com/docs/grafana-cloud/monitor-infrastructure/integrations/integration-reference/integration-gitea/#gitea-integration-for-grafana-cloud):
- Number of repositories, users
- HTTP requests (rate, latency)
- CI/CD runner status
- LXC CPU/RAM usage

### Migrating code from GitHub

Simple and fast:  
Use Gitea's import feature (Settings > New Migration > GitHub), which also migrates issues and releases.

### CI/CD: Deploying Hugo to Scaleway Object Storage

My Hugo blog deploys automatically to Scaleway Object Storage on every push.

**Installing the Gitea Runner** (https://docs.gitea.com/usage/actions/act-runner)

Create the system user for the runner:

```bash
useradd -r -m -d /var/lib/gitea-runner -s /bin/bash gitea-runner
```

Here's a small script that can help install the runner directly inside an LXC:

```bash
sudo apt install -y jq curl tar # si pas déjà

LATEST=$(curl -s 'https://gitea.com/api/v1/repos/gitea/act_runner/releases' | jq -r '.[0].tag_name')
echo "Latest act_runner: $LATEST"

# construire URL de binaire (nommage used: act_runner-<os>-<arch>)
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
# Certains serveurs distribuent binaire sans tar; adapter si archive
URL="https://gitea.com/gitea/act_runner/releases/download/${LATEST}/act_runner-${LATEST#v}-${OS}-${ARCH}"

# essayer télécharger binaire
curl -fL "$URL" -o /tmp/act_runner || {
  echo "Téléchargement direct échoué — vérifier le nom exact sur la page release." >&2
  exit 1
}

sudo mv /tmp/act_runner /usr/local/bin/act_runner
sudo chmod +x /usr/local/bin/act_runner
```

and to verify:

```bash
/usr/local/bin/act_runner --version
```

>[!NOTE]
> It's important to register the runner so it's recognized by Gitea.  
> For more information on configuring the runner, check the official Gitea documentation.  
> https://docs.gitea.io/fr/docs/usage/actions/runner/

**Gitea Actions workflow** (`.gitea/workflows/deploy.yml`):

```yaml
name: Build and Deploy Hugo

on:
  pull_request:
    types: [closed]
    branches:
      - main

jobs:
  build_and_deploy:
    if: github.event.pull_request.merged == true
    name: Deploy Hugo Website
    runs-on: self-hosted

    container:
      image: debian:bookworm-slim

    steps:
      - name: Install dependencies
        run: |
          apt-get update
          apt-get install -y git curl ca-certificates wget

      - name: Install Hugo
        run: |
          wget https://github.com/gohugoio/hugo/releases/download/v0.152.2/hugo_extended_withdeploy_0.152.2_linux-amd64.deb -O /tmp/hugo.deb
          dpkg -i /tmp/hugo.deb

      - name: Checkout code
        run: |
          git clone --recurse-submodules https://gitea.arnodo.fr/Damien/Notebook.git /tmp/workspace
          cd /tmp/workspace
          git checkout ${{ github.sha }}

      - name: Build site
        run: /usr/local/bin/hugo
        working-directory: /tmp/workspace

      - name: Deploy to Scaleway
        run: /usr/local/bin/hugo deploy --force --maxDeletes -1
        working-directory: /tmp/workspace
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.SCW_ACCESS_KEY }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.SCW_SECRET_KEY }}

```

**Hugo configuration** (`hugo.yaml`):

```yaml
deployment:
  targets:
    - name: "notebook-arnodo-fr"
      URL: "s3://notebook-arnodo-fr?endpoint=https://s3.fr-par.scw.cloud&region=fr-par"
```

**Scaleway Object Storage configuration**:
1. Create a `notebook-arnodo-fr` bucket
2. Enable "Static Website Hosting" mode
3. Generate API credentials (Access Key + Secret Key)
4. Add them as secrets in Gitea (Settings > Secrets > Actions)

**Deployment**:

```bash
git add .
git commit -m "New blog post"
git push origin main
```

The workflow triggers automatically, Hugo builds the site, and deploys it to Scaleway Object Storage. The site is instantly available via the CDN.

## Part 2: network labs on Scaleway

### The problem

Running ContainerLab with several Arista EOS instances locally is:
- **Resource-hungry**: 4-8 GB RAM per cEOS container
- **Local-only**: No access from outside
- **Conflict-prone**: With other Docker/K8s services

### The solution: on-demand Scaleway instances

**Concept**:
- Spin up a Scaleway instance whenever I need a lab
- Automatically install ContainerLab/the VPN via cloud-init
- Destroy the instance after use
- Billed by the hour (< €1 for a few hours of lab time)

### Automation script: Scaleway CLI

I built a Bash script that manages the entire lifecycle of a lab instance.

**Features**:
- **Creation**: Instance + Security Group (SSH from my IP only)
- **Start/Stop**: Instance management
- **Deletion**: Full cleanup (instance, volumes, IP, SG)

**Script structure** (`scaleway-instance.sh`):

```bash
#!/bin/bash
# Configuration
INSTANCE_NAME="NetLab"
ZONE="fr-par-1"
IMAGE="debian_bookworm"
VOLUME_SIZE=20  # GB
USER_DATA_FILE="user_data.txt"
SECURITY_GROUP_NAME="${INSTANCE_NAME}-SG"

# Détecte l'IP publique actuelle
get_public_ip() {
  curl -4 -s ifconfig.me
}

# Crée un Security Group limitant SSH à l'IP publique
create_or_update_security_group() {
  PUBLIC_IP=$(get_public_ip)
  # Crée le SG avec inbound SSH uniquement depuis PUBLIC_IP/32
  # ...
}

# Actions : create, start, stop, delete
case "$1" in
  start)
    # Démarre l'instance existante
    scw instance server start "$INSTANCE_ID" --wait
    ;;
  stop)
    # Arrête l'instance
    scw instance server stop "$INSTANCE_ID" --wait
    ;;
  delete)
    # Supprime instance + volumes + IP + SG
    scw instance server terminate "$INSTANCE_ID" --with-ip --with-block
    scw instance security-group delete "$SG_ID"
    ;;
  *)
    # Crée une nouvelle instance
    create_instance "$1"  # Type d'instance (DEV1-S, GP1-XS, ...)
    ;;
esac
```

### Cloud-init: automatic configuration

The `user_data.txt` file contains the cloud-init instructions to automatically provision the instance.

**Example** (`user_data.txt`):

```yaml
#cloud-config
package_update: true
package_upgrade: true

packages:
  - git
  - curl
  - docker.io
  - docker-compose

runcmd:
  # Installation de ContainerLab
  - bash -c "$(curl -sL https://get.containerlab.dev)"
  
  # Clone d'un repo avec des topologies
  - git clone https://gitea.arnodo.fr/Damien/network-labs.git /root/labs
  
  # Démarrage d'une topologie par défaut
  - cd /root/labs && containerlab deploy -t spine-leaf.clab.yml
```

### Practical usage

**Create a lab**:

```bash
# Crée une instance DEV1-S avec 20 GB de stockage
./scaleway-instance.sh DEV1-S 20

# Attend quelques minutes pour cloud-init
# Récupère l'IP publique
scw instance server list name=NetLab -o json | jq -r '.servers[0].public_ip.address'

# SSH vers l'instance
ssh root@<IP_PUBLIQUE>

# ContainerLab est déjà lancé !
containerlab inspect
```

**Destroy the lab**:

```bash
./scaleway-instance.sh delete
```

### Raycast integration

To make things even simpler, I created a Raycast script that lets me manage my instances directly from my Mac.

**Raycast script**:

```bash
#!/bin/bash
# @raycast.schemaVersion 1
# @raycast.title Scaleway Instance
# @raycast.mode silent
# @raycast.icon 🖥️
# @raycast.argument1 { "type": "text", "placeholder": "Action or instance type" }
# @raycast.argument2 { "type": "text", "placeholder": "Volume size", "optional": true }
# @raycast.packageName NetLab

/path/to/scaleway-instance.sh "$1" "$2"
```

**Usage**:
- `⌘ + Space` → "Scaleway Instance DEV1-S" → Creates the instance
- `⌘ + Space` → "Scaleway Instance delete" → Deletes the instance

### Use case: BGP/EVPN lab with Arista

**ContainerLab topology** (`spine-leaf.clab.yml`):

```yaml
name: evpn-lab

topology:
  nodes:
    spine1:
      kind: ceos
      image: ceos:latest
    spine2:
      kind: ceos
      image: ceos:latest
    leaf1:
      kind: ceos
      image: ceos:latest
    leaf2:
      kind: ceos
      image: ceos:latest

  links:
    - endpoints: ["spine1:eth1", "leaf1:eth1"]
    - endpoints: ["spine1:eth2", "leaf2:eth1"]
    - endpoints: ["spine2:eth1", "leaf1:eth2"]
    - endpoints: ["spine2:eth2", "leaf2:eth2"]
```

**Workflow**:
1. Create the Scaleway instance
2. Cloud-init deploys the topology
3. Configure BGP/EVPN via Ansible or manually
4. Test, experiment
5. Destroy the instance

**Cost**: DEV1-S instance (2 vCPU, 2GB) = ~€0.015/hour. 4 hours of lab time = €0.06.

## Digital sovereignty: why it matters

This hybrid infrastructure reflects a personal conviction about digital sovereignty.

### The context

In my work as a network engineer, I see how important it is to be in control of your own infrastructure.

Choosing Scaleway (part of the Iliad group, French) and self-hosting Gitea means:
- **Supporting the European tech ecosystem**
- **Ensuring GDPR compliance**: French jurisdiction
- **Reducing latency**: Datacenters in Paris
- **Understanding**: Owning your entire chain end-to-end

### Learning by doing

As a network professional (Arista, BGP/EVPN, automation), self-hosting lets me:
- Apply Infrastructure as Code principles
- Deeply understand CI/CD mechanics
- Experiment without limits
- Reproduce professional environments

## Assessment

### What works well

**Self-hosted Gitea**:
- Very fast and stable
- Proxmox Helper Scripts = 5-minute install
- Ansible handles updates cleanly
- Grafana monitors everything

**Wireguard + Nginx Proxy Manager**:
- Secure exposure of the homelab
- Excellent performance
- Simple configuration

**Scaleway labs**:
- Provisioning in 3 minutes
- Total flexibility (size, duration)
- Predictable costs (hourly billing)

**CI/CD Hugo → Scaleway Object Storage**:
- Push-to-deploy in 2 minutes
- Free (a few cents/month for storage)
- Built-in CDN = ultra-fast site

### The challenges

**Initial complexity**:
- Wireguard + reverse proxy = learning curve
- First Proxmox/LXC setup = a few hours

**Maintenance**:
- Responsibility for updates (thankfully Ansible helps!)
- Monitoring to set up yourself
- Backups to automate

**Dependencies**:
- If the Dedibox goes down, Gitea becomes unreachable
- Solution: failover with a second Dedibox or VPS (coming soon)

## Next steps

- **High availability**: A second Dedibox for failover
- **Automatic backup**: Scripts to back up Gitea to Scaleway Object Storage
- **More automation**: Terraform to provision the entire Scaleway infrastructure
- **Arista MCP**: Building an MCP server to interact with network equipment via local LLMs
- **Netbox integration**: Webhook from Netbox to a network validation pipeline

## Conclusion

This hybrid setup, Proxmox homelab plus Scaleway cloud, gives me control over sensitive data (code, configurations stay in the homelab), cloud resources for one-off needs, and everything hosted in France with European providers.

Self-hosting hasn't saved me money (spoiler: I pay about as much as before, if not more). What it gave me is a deeper understanding of the systems I work with.

For a network or DevOps engineer, it's a good environment to reproduce professional use cases and build up skills.

## Resources

### Documentation
- [Gitea](https://docs.gitea.io/)
- [Proxmox Helper Scripts](https://community-scripts.github.io/ProxmoxVE/)
- [Scaleway CLI](https://www.scaleway.com/en/cli/)
- [ContainerLab](https://containerlab.dev/)
- [Nginx Proxy Manager](https://nginxproxymanager.com/)

### My repos
- [Blog Hugo](https://gitea.arnodo.fr/Damien/blog)
- [Network Labs](https://gitea.arnodo.fr/Damien/arista-evpn-vxlan-clab) (ContainerLab topologies)
- [Scaleway Scripts](https://gitea.arnodo.fr/Damien/scaleway-automation)

### Community
- [Proxmox Forum](https://forum.proxmox.com/)
- [r/homelab](https://reddit.com/r/homelab)
- [ContainerLab Slack](https://containerlab.dev/)
