Forgejo: Tu propio servidor Git

Forgejo: Tu propio servidor Git
TL;DR: El artículo narra la experiencia de configurar un servidor de Git usando Forgejo, un fork de Gitea, en un entorno Docker con PostgreSQL y SSH. Se describe cómo desplegar la aplicación mediante un archivo docker-compose.yml, realizar la configuración inicial y ajustar parámetros de seguridad, como desactivar el registro público. Posteriormente, se agrega Drone para la integración continua, incluyendo cómo crear una aplicación OAuth en Forgejo y ajustar las variables de entorno en el contenedor de Drone. Finalmente, se introduce un runner de Drone para ejecutar las acciones automáticas, utilizando un archivo .drone.yml para definir las tareas, lo que permite ejecutar pipelines en el nuevo servidor de Git.

Hoy me ha dado por empezar un nuevo proyecto en Angular, y por algún motivo me ha apetecido revivir mi vieja instancia de Gitea y alojarlo allí (para sorpresa de nadie, al final me he enfrascado con esto durante horas y no le he dedicado ni una línea al proyecto de Angular todavía)

Un amigo que estuvo montando algo similar recientemente, me mencionó Forgejo, un fork de Gitea que parece que ha tomado bastante inercia y no tiene mala pinta, así que he decidido probarlo, ya que me gusta bastante más su razón de ser.

Manos a la obra

Empezaremos por el despliegue del contenedor que albergará la aplicación. En este caso he decidido probar a usarlo sobre una base de datos PostgreSQL.

networks:
  forgejo:
    external: false
  # proxy-network:
    # external: true

services:
  server:
    image: codeberg.org/forgejo/forgejo:7
    container_name: forgejo
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - FORGEJO__database__DB_TYPE=postgres
      - FORGEJO__database__HOST=db:5432
      - FORGEJO__database__NAME=forgejo
      - FORGEJO__database__USER=forgejo
      - FORGEJO__database__PASSWD=forgejo
    restart: always
    ports:
      - 127.0.0.1:2222:22
    networks:
      - forgejo
      # - proxy-network
    volumes:
      - ./forgejo:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
      # - /home/git/.ssh:/data/git/.ssh
    depends_on:
      - db

  db:
    image: postgres:14
    restart: always
    environment:
      - POSTGRES_USER=forgejo
      - POSTGRES_PASSWORD=forgejo
      - POSTGRES_DB=forgejo
    networks:
      - forgejo
    volumes:
      - ./postgres:/var/lib/postgresql/data

docker-compose.yml

En mi caso particular dado que uso un proxy de nginx, no he publicado el puerto 3000 que tiene por defecto para acceder a la aplicación y también he conectado el contenedor a mi red proxy-network. Lo que sí es relevante es la configuración del puerto 127.0.0.1:2222:22 para poder usar SSH, lo explicaré posteriormente.

Si todo ha ido bien, deberíamos ver la pantalla de configuración inicial:

Aquí solo hay que retocar los parámetros que creamos oportunos. En mi caso sólo he cambiado el dominio del servidor a mi dominio público, y el título de la aplicación. El resto recomiendo no tocarlo (como el usuario git y el puerto 22) si queremos seguir con esta configuración.

Pulsamos en "Instalar Forgejo" para que haga su magia y automáticamente nos llevará a la pantalla de inicio de sesión:

Pulsamos el botón de Registro en la esquina superior derecha y rellenaremos el pequeño formulario para registrarnos. Al ser el primer usuario, tendremos permisos administrativos.

Como se trata de un servidor para un homelab que está expuesto a internet, he decidido desactivar la opción de registro. En caso de querer agregar alguno se puede hacer manualmente como administrador. Para ello, iremos a la carpeta donde se encuentran los volúmenes de docker y editaremos el fichero forgejo/gitea/conf/app.ini, concretamente cambiaremos el parámetro DISABLE_REGISTRATION poniendo su valor a true , también he aprovechado para desactivar el login mediante OpenID, con el parámetro ENABLE_OPENID_SIGNIN ya que no voy a usarlo y me chirría ver la opción al hacer login.

Reiniciamos el servidor y llegados a este punto deberíamos tener acceso a la aplicación y al panel administrativo. Ya se puede usar como un servidor de Github sin mayor problema, y si era tu intención usarlo con HTTPS podríamos decir que ya hemos llegado al final de la guía, pero yo quiero endulzarlo un poco más 👇

Configuración de acceso mediante SSH

Para configurar el acceso por SSH he recurrido a la documentación de Gitea, donde explican varias formas de hacerlo. He optado por el primer método (SSHing Shim) y lo explicaré aquí de manera resumida.

En nuestro host (el servidor donde corre Docker con nuestra instancia de Forjego) deberemos crear un usuario git que en resumidas cuentas hará de puente entre nuestro host y el contenedor. Es importante crearlo con con carpeta en home ya que de lo contrario no podremos ejecutar el resto de comandos de la documentación. Para ello, en nuestro host lanzaremos el comando sudo useradd -m git

Ahora explicaré los puntos clave sobre el fichero docker-compose:

services:
  server:
  ...
    environment:
    ...
      - USER_UID=1001
      - USER_GID=1001
      
      # UID y GID del usuario git
      # Puedes consultarlos haciendo sudo cat /etc/passwd en tu host
    ...
    ports:
      - 127.0.0.1:2222:22 
      
      # Redirecciona el puerto 22 del servicio al 2222 de nuestro host
    ...
    volumes:
    ...
      - /home/git/.ssh:/data/git/.ssh 
      
      # Monta la carpeta ssh del usuario git en un volumen 
      # para tener acceso en el contenedor
    ...
  ...

Es importante dejarlos configurados debidamente, de lo contrario no funcionará.

Una vez creado el usuario git le crearemos una clave con el siguiente comando:

sudo -u git ssh-keygen -t rsa -b 4096 -C "Gitea Host Key"

Ahora hay que modificar el archivo authorized_keys del usuario git para que se comporte de la misma forma que en el contenedor:

sudo -u git cat /home/git/.ssh/id_rsa.pub | sudo -u git tee -a /home/git/.ssh/authorized_keys
sudo -u git chmod 600 /home/git/.ssh/authorized_keys

Tras eso, el archivo debería verse de forma similar a:

# SSH pubkey from git user
ssh-rsa <Gitea Host Key>

# other keys from users
command="/usr/local/bin/gitea --config=/data/gitea/conf/app.ini serv key-1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty <user pubkey>

/home/git/.ssh/authorized_keys

(No pasa nada si no vemos ninguna clave de usuario, ya que no hemos agregado ninguna)

Por último, habrá que crear un comando gitea fake que se encargará de redireccionar los comandos del host al contenedor. Con un usuario con permisos administrativos hay que ejecutar:

cat <<"EOF" | sudo tee /usr/local/bin/gitea
#!/bin/sh
ssh -p 2222 -o StrictHostKeyChecking=no git@127.0.0.1 "SSH_ORIGINAL_COMMAND=\"$SSH_ORIGINAL_COMMAND\" $0 $@"
EOF
sudo chmod +x /usr/local/bin/gitea

Con esto debería ser suficiente. Ahora podemos acceder a nuestra instancia de Forgejo, pulsar en nuestro avatar en la esquina superior derecha, posteriormente en Configuación y en Claves SSH/GPG para agregar nuestra clave pública.

Una vez configurada, podemos probar a hacer una conexión SSH desde cualquier sitio a nuestro host y éste debería dar la siguiente respuesta:

Esto significa que en nuestro host, seguimos teniendo el puerto 22 operativo para SSH, pero sin embargo cuando conectamos con el usuario git la conexión va directa al contenedor de Docker.

Llegados a este punto, tenemos un servidor Git completamente funcional, con acceso mediante HTTPS y SSH.

Agregando Drone para Integración Continua

Como muchos echarán en falta las Github Actions, Drone es una buena alternativa para ejecutar tareas de integración contínua. Forgejo también tiene las Forgejo Actions, pero no he tenido tiempo de revisarme la documentación para plantear una forma sencilla y en un contenedor (por seguir la línea de la guía), así que prefiero hacerlo con Drone que es un viejo conocido.

Para empezar, hay que crear una aplicación OAuth en el panel de control de Forgejo. En la URL hay que poner el domino donde se desplegará Drone, seguido de un /login:

Esto nos dará un Client ID y Client Secret que deberemos usar posteriormente.

Se puede levantar el contenedor de varias formas, pero he preferido anexionarlo al docker-compose ya existente:

  drone:
    image: drone/drone:2
    container_name: drone
    restart: always
    networks:
      - drone-network
    volumes:
      - /var/lib/drone:/data
    environment:
      DRONE_GITEA_SERVER: https://try.gitea.io
      DRONE_GITEA_CLIENT_ID: 05136e57d80189bef462
      DRONE_GITEA_CLIENT_SECRET: 7c229228a77d2cbddaa61ddc78d45e
      DRONE_RPC_SECRET: super-duper-secret
      DRONE_SERVER_HOST: drone.company.com
      DRONE_SERVER_PROTO: https
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - server

Debermos completar las variables de entorno con nuestra información:

  • DRONE_GITEA_SERVER es el dominio donde se encuentra Forgejo
  • DRONE_GITEA_CLIENT_ID y DRONE_GITEA_CLIENT_SECRET son los valores que obtuvimos al crear la aplicación OAuth en Forgejo
  • DRONE_RPC_SECRET es una clave compartida a la que podemos asignar cualquier valor, recomiendo generar una con openssl rand -hex 16
  • DRONE_SERVER_HOST y DRONE_SERVER_PROTO será el dominio por donde accederemos a Drone y su protocolo.

En mi caso particular he retirado el mapeo de puertos y he añadido el contenedor a la red proxy-network ya que tengo la aplicación tras un reverse-proxy.

Volvemos a recrear todo con un buen docker compose up -d --force-recreate y nos dirigimos a la URL que hemos configurado para Drone para ver algo como esto:

Al pulsar Continuar, debería redirigir a nuestra instancia de Forgejo para finalizar la configuración:

Tras eso, rellenamos nuestros datos y ya tendríamos Drone configurado.

Agregando runners a Drone

Si estáis familiarizados con las Github Actions, sabréis que se necesitan runners para correr las acciones. En drone sucede lo mismo, así que vamos agregar uno para que se encargue de ello.

Para hacerlo de una forma sencilla agregaremos este nuevo contenedor a nuestro docker-compose

  runner:
    image: drone/drone-runner-docker:1
    container_name: runner
    restart: always
    networks:
      - drone-network
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      DRONE_RPC_PROTO: https
      DRONE_RPC_HOST: drone.company.com
      DRONE_RPC_SECRET: super-duper-secret
      DRONE_RUNNER_CAPACITY: 2
      DRONE_RUNNER_NAME: my-first-runner
    ports:
      - "3000:3000"
    depends_on:
      - drone

De nuevo, aconsejo repasar todas las variables de entorno con las que ya hemos introducido anteriormente para que se vincule correctamente.

Para asegurar que todo ha ido bien, he creado un pequeño proyecto de React y lo he subido a un nuevo repositorio en Forgejo. Luego he accedido a Drone para sincronizarlo y activarlo.

De forma similar a como se hace en Github Actions, aquí hemos de añadir un fichero .drone.yml que se encargará las acciones. Este es el que he subido junto con el código:

kind: pipeline
name: default

steps:
- name: test
  image: node
  commands:
  - npm install
  - npm run build

La sintaxis es un poco diferente, pero en la documentación de Drone lo explican todo bastante bien.

Tras subir los cambios, drone detectará el fichero .drone.yml y ejecutará las acciones pertinentes de forma automática. Podéis consultar los resultados tanto en el contenedor de Drone:

Listo, ya tenemos integración continua en nuestro servidor.

Espero que sea de ayuda y no haya sido demasiado pesado, yo dejo esto aquí como referencia para el futuro (siempre se me olvidan este tipo de cosas), y me dejo en el tintero la integración de Forgejo Actions y Renovate (un sustituto de dependabot), que dará para otro artículo.

Anexo: Forgejo Actions