Files
Notebook/content/blog/migration-gitlab-gitea/index.en.md
Damien 2d8830960b
All checks were successful
Build and Deploy Hugo / Deploy Hugo Website (pull_request) Successful in 24s
Update documentation for clarity and consistency
Remove emojis and em dash separators
Simplify introductory sentences
Standardize section headings
2026-07-19 15:09:56 +02:00

16 KiB

title, date, authors, tags
title date authors tags
Self-Hosting and Hybrid Cloud: My Personal Infrastructure with Gitea and Scaleway 2025-11-20
name link
Damien https://gitea.arnodo.fr/Damien
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.

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 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

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 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, which automate the creation of preconfigured LXCs.

Installing Gitea:

# 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):

[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):

---
- 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:

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):

[metrics]
ENABLED = true
TOKEN = <secret_token>

Prometheus scrape config:

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:

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:

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:

/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):

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):

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:

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):

#!/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):

#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:

# 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:

./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:

#!/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):

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

My repos

Community