Volver a publicaciones

Automatización

Forgejo y GitHub Actions: tu infraestructura en Git y un repo público profesional

13 min de lectura

#git#forgejo#github-actions#ci#gitops#buenas-practicas

Portada: ramas de Git que se unen en una marca de verificación sobre fondo oscuro con el título Forgejo y GitHub Actions

Puntos clave

  • Un repositorio en Forgejo guarda todos los docker-compose: cada cambio de infraestructura es un commit.
  • Forgejo en Docker con el panel solo en LAN/VPN y SSH en un puerto alternativo.
  • Los secretos viven en .env fuera de Git; en el repo solo va .env.example con las claves vacías.
  • Si un secreto llega a Git, primero se rota y después se limpia el historial.
  • Un repo público serio tiene CI con matriz de sistemas, Dependabot, plantillas y políticas claras.

Mi NAS es una colección de carpetas con un docker-compose.yml cada una. Sin control de versiones, cada cambio se pierde: no queda rastro de qué se tocó ni cuándo. Mi solución fue convertir esa carpeta en un repositorio Git y alojarlo en mi propio servidor, Forgejo.

Ahora cada cambio de infraestructura es un commit con su mensaje. Puedo ver el historial de un servicio y volver atrás con un comando. En este capítulo cuento cómo lo monté, cómo protejo los secretos y cómo preparo un repositorio público para que parezca, y sea, un proyecto profesional.

Qué vas a montar

  • Forgejo en Docker: tu propio servidor Git, con el panel solo en casa.
  • Un repositorio con todos tus docker-compose como fuente de verdad.
  • Un flujo de cambios sencillo y una convención de commits.
  • Una forma segura de tratar los secretos (y qué hacer si se escapa uno).
  • La lista de piezas de un repo público profesional, con un ejemplo real.

Requisitos

  • Docker y Docker Compose en el NAS o en un servidor.
  • Caddy delante como proxy (ver la serie “Mi NAS paso a paso”). En los ejemplos, git.example.com.
  • Tu red de casa es 192.168.1.0/24 y la VPN 10.8.0.0/24. Son valores de ejemplo: usa los tuyos.

Paso 1: Forgejo en Docker

Forgejo es un servidor Git ligero, derivado de Gitea. Con SQLite y 256 MB de memoria va sobrado para uso personal.

services:
  forgejo:
    image: codeberg.org/forgejo/forgejo:9      # fija la versión mayor
    container_name: forgejo
    restart: unless-stopped
    environment:
      USER_UID: "1000"                          # usuario dueño de ./data en el host
      USER_GID: "1000"
      FORGEJO__database__DB_TYPE: sqlite3       # sin servidor de base de datos aparte
      FORGEJO__server__DOMAIN: git.example.com
      FORGEJO__server__SSH_DOMAIN: git.example.com
      FORGEJO__server__ROOT_URL: https://git.example.com/
      FORGEJO__server__HTTP_PORT: "3000"
      FORGEJO__server__START_SSH_SERVER: "true" # SSH propio de Forgejo
      FORGEJO__server__SSH_PORT: "2022"         # puerto que se anuncia al clonar
      FORGEJO__server__SSH_LISTEN_PORT: "2022"  # puerto en el que escucha
      FORGEJO__service__DISABLE_REGISTRATION: "true" # nadie puede crearse cuenta
      FORGEJO__service__REQUIRE_SIGNIN_VIEW: "true"  # sin sesión no se ve nada
    volumes:
      - ./data:/data                            # repos, base de datos y configuración
    ports:
      - "127.0.0.1:3001:3000"                   # web: solo la alcanza el proxy
      - "2022:2022"                             # SSH: solo en la LAN, no lo abras en el router
    security_opt:
      - no-new-privileges:true
    mem_limit: 256m
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/healthz"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

Las decisiones importantes:

  • SSH en un puerto alternativo. El 22 ya lo usa el SSH del propio servidor. Forgejo trae su servidor SSH y lo pongo en otro puerto (aquí 2022, como ejemplo).
  • Registro cerrado y sin vista anónima. Es un servidor personal: solo entra quien yo cree.
  • La web solo en 127.0.0.1. Desde la red no se llega; solo Caddy.
docker compose up -d
docker ps --filter name=forgejo    # espera a ver "healthy"

La primera vez que abras https://git.example.com, Forgejo muestra el asistente de instalación. Revisa que los valores coinciden con el compose y crea ahí la cuenta de administrador.

El panel, solo desde casa o la VPN

En Caddy, el bloque del servicio responde 403 a cualquier IP que no sea de casa, de la VPN o del propio servidor:

git.example.com {
    # Todo lo que no venga de LAN, VPN o localhost recibe un 403
    @external not remote_ip 192.168.1.0/24 10.8.0.0/24 127.0.0.1/8
    respond @external 403

    reverse_proxy localhost:3001
}

Clonar por SSH con el puerto alternativo

Para no escribir el puerto en cada comando, añade esto a ~/.ssh/config en tu equipo:

Host git.example.com
    User git
    Port 2022
    IdentityFile ~/.ssh/id_ed25519

Sube tu clave pública en Forgejo (ajustes de usuario, claves SSH) y clona con normalidad:

git clone git@git.example.com:usuario/infra.git

Paso 2: la infraestructura como repositorio

En mi NAS, la carpeta que contiene todos los stacks es la raíz de un repositorio llamado homelab-infra. Cada subcarpeta es un servicio con su docker-compose.yml. El historial de Git documenta cada cambio.

Lo que no entra en el repo: datos pesados, bases de datos, logs y secretos. Este .gitignore en la raíz lo resuelve:

# Datos de los servicios: se respaldan, no se versionan
**/data/
**/postgres/
**/redis/
**/logs/

# Secretos: nunca al repo
**/.env
*.pem
*.key
*.jks

# Copias y volcados
*.bak
*.dump

Y un .gitattributes para que los finales de línea no cambien según edites por SMB desde Windows o por SSH desde Linux:

* text=auto eol=lf

El flujo de un cambio

Siempre el mismo, en tres pasos:

# 1. Editar el compose del stack
cd /volume1/docker
nano observability/docker-compose.yml

# 2. Guardar el cambio con un mensaje claro
git add observability/docker-compose.yml
git commit -m "chore(observability): grafana 12.3.0 -> 12.3.1"

# 3. Aplicarlo
cd observability && docker compose up -d

Convención de mensajes

Uso un formato fijo tipo(stack): descripción. Así el historial se lee de un vistazo:

Tipo Cuándo
feat(stack) Servicio o funcionalidad nueva
fix(stack) Corrección de configuración o de un fallo
chore(stack) Subida de versión sin cambios funcionales
security(stack) Actualización de seguridad urgente
refactor(stack) Reorganización sin cambio de comportamiento
docs Documentación

Volver atrás

git log --oneline observability/docker-compose.yml       # historial de un stack
git checkout <hash> -- observability/docker-compose.yml  # recuperar una versión anterior
git commit -m "fix(observability): rollback por fallo"
cd observability && docker compose up -d

Documentación generada desde el repo

Mis notas de cada servicio incluían una copia del compose. Con el tiempo, las copias se quedaban viejas. Ahora tengo una comprobación semanal que avisa si una nota no coincide con el repo, y un script que regenera las notas desde los compose. La idea es que la documentación sea una vista del repo, no una copia a mano.

Este es un script mínimo con la misma idea:

#!/usr/bin/env bash
# Regenera una nota Markdown por stack a partir de su docker-compose.yml
set -euo pipefail
REPO="/volume1/docker"          # raíz del repositorio
DOCS="/volume1/docs/servicios"  # carpeta de notas generadas
mkdir -p "$DOCS"

for compose in "$REPO"/*/docker-compose.yml; do
  stack="$(basename "$(dirname "$compose")")"
  {
    printf '# %s\n\n' "$stack"
    printf '> Nota generada desde el repo. No editar a mano.\n\n'
    printf '```yaml\n'
    cat "$compose"
    printf '\n```\n'
  } > "$DOCS/$stack.md"
  echo "OK $stack"
done

Si quieres que se ejecute solo, llámalo desde un hook post-commit del repositorio: cada commit refresca las notas.

Paso 3: secretos fuera de Git

La regla es simple: los valores reales viven en .env, con permisos restringidos, y nunca se suben. En el repo va un .env.example con las mismas claves y sin valores. Sirve para reconstruir el .env desde cero en una restauración.

# .env.example — se versiona. Copia a .env y rellena los valores.
CLOUDFLARE_API_TOKEN=
POSTGRES_PASSWORD=
cp .env.example .env
chmod 600 .env                 # solo el dueño puede leerlo
openssl rand -hex 32           # genera un secreto aleatorio para pegarlo en .env

Antes de cada commit, reviso que no se cuela nada sensible. Este hook lo hace por mí (guárdalo como .git/hooks/pre-commit y dale permisos de ejecución):

#!/usr/bin/env bash
# Bloquea el commit si hay ficheros que parecen secretos en el área de preparación
set -euo pipefail
if git diff --cached --name-only | grep -E '(^|/)\.env$|\.(pem|key|jks|bak|dump)$'; then
  echo "ERROR: hay ficheros sensibles preparados para commit. Revísalos." >&2
  exit 1
fi

El caso real: un secreto en el historial

Me pasó: material sensible acabó dentro del repositorio de infraestructura. Lo quité del árbol, pero seguía en el historial, y cualquiera con una copia del repo podía recuperarlo.

El orden correcto es este:

  1. Rota el secreto primero. Revócalo en el servicio que lo emitió y genera uno nuevo. Ponlo en el .env y reinicia el servicio. Da igual lo rápido que limpies: el valor antiguo ya puede estar copiado en otro clon, en una copia de seguridad o en una caché.
  2. Después, limpia el historial con git filter-repo:
# Trabaja sobre un clon nuevo, nunca sobre tu única copia
git clone git@git.example.com:usuario/infra.git infra-limpio
cd infra-limpio

# Elimina el fichero de todos los commits
git filter-repo --invert-paths --path caddy/.env

# filter-repo quita el remoto por seguridad: vuelve a añadirlo y fuerza la subida
git remote add origin git@git.example.com:usuario/infra.git
git push --force --all
git push --force --tags
  1. Vuelve a clonar en cada equipo que tuviera una copia antigua, y añade el patrón al .gitignore para que no se repita.

Limpiar el historial sin rotar el secreto es solo cosmético. Rotarlo es lo que te protege.

Paso 4: un repositorio público profesional

Mi infraestructura es privada, pero mis proyectos públicos van a GitHub. Allí un repo no solo guarda código: también es tu carta de presentación. Como ejemplo real uso DozeForge, una aplicación de escritorio (Tauri con Rust y SvelteKit) que publiqué en abierto.

CI con GitHub Actions

El workflow de DozeForge se ejecuta en cada push a main, en cada pull request y en cada tag v*. Tiene varios jobs separados, y cada uno responde a una pregunta:

  • Rust, en tres sistemas. Una matriz con ubuntu-22.04, windows-latest y macos-latest. En cada uno: cargo fmt -- --check (formato), cargo clippy --all-targets -- -D warnings (cualquier aviso rompe la build) y cargo test --all-targets. Con fail-fast: false, si falla Windows, sigues viendo el resultado de macOS y Linux.
  • Frontend. npm ci, npm run check (tipos y accesibilidad) y npm test (Vitest).
  • Humo en Android. Arranca un emulador con una matriz de API (31, 33 y 34) y prueba los comandos ADB de los que depende la app. Es orientativo: lleva continue-on-error: true porque los emuladores en CI fallan a veces sin motivo.
  • Instaladores. Solo si Rust y frontend pasan (needs), construye el instalador de cada sistema y lo guarda como artefacto.
  • Release. Solo en tags v*: reúne los instaladores en un borrador de release de GitHub.

Dos detalles que ahorran tiempo y dinero: la caché de dependencias (cache: "npm" en actions/setup-node@v4 y Swatinem/rust-cache@v2 para Cargo) y el bloque concurrency con cancel-in-progress: true, que cancela la ejecución anterior si subes otro commit a la misma rama.

Un workflow mínimo para copiar

Si tu proyecto es de Node, este es un buen punto de partida. Usa las mismas versiones de las actions que DozeForge:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

# Si llega otro commit a la misma rama, cancela la ejecución anterior
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

# Permisos mínimos: el workflow solo necesita leer el código
permissions:
  contents: read

jobs:
  test:
    name: Node (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "npm"               # reutiliza la caché de npm entre ejecuciones
      - run: npm ci                  # instala exactamente lo del lockfile
      - run: npm run check --if-present
      - run: npm run test --if-present
      - run: npm run build --if-present

Guárdalo como .github/workflows/ci.yml. --if-present evita que falle si tu proyecto aún no tiene alguno de esos scripts.

Dependabot

Dependabot abre pull requests cuando hay versiones nuevas de tus dependencias. DozeForge vigila tres ecosistemas cada semana: npm, Cargo y las propias GitHub Actions. Una versión mínima para un proyecto Node:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 5    # no más de 5 PR abiertas a la vez
    labels: ["dependencies"]

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    labels: ["ci"]

Como cada PR pasa por el CI, sabes si la actualización rompe algo antes de fusionarla.

Plantillas, políticas y licencia

  • Plantillas de issues en formato formulario (YAML): el de bug pide dispositivo, sistema operativo y pasos. Con blank_issues_enabled: false nadie abre un issue vacío, y los enlaces de contacto mandan las preguntas a Discussions y los fallos de seguridad al canal privado.
  • Plantilla de PR con un checklist: npm run check, npm test y cargo test pasan, hay test para lo nuevo, etc.
  • CONTRIBUTING.md: cómo preparar el entorno y qué comprobaciones ejecutar antes de abrir una PR (las mismas que el CI).
  • SECURITY.md: cómo reportar una vulnerabilidad en privado (con la opción de GitHub de reporte privado), nunca en un issue público.
  • CODE_OF_CONDUCT.md: las normas de convivencia.
  • CHANGELOG.md siguiendo Keep a Changelog y versionado semántico.
  • LICENSE (MIT en este caso) y NOTICE, que da crédito al software de terceros que se incluye en el instalador y a su licencia.

Checklist de repo profesional

Pieza Para qué sirve En DozeForge
CI en cada push y PR Detectar fallos antes de fusionar Rust, frontend, Android y builds
Matriz de sistemas Probar donde lo usarán tus usuarios Ubuntu, Windows y macOS
Linters como puerta Mantener el código limpio fmt --check, clippy -D warnings
Caché de dependencias CI más rápido npm y Cargo
Dependabot Dependencias al día sin esfuerzo npm, Cargo y Actions, semanal
Plantillas de issue y PR Reportes útiles y revisiones ordenadas Formularios YAML y checklist
CONTRIBUTING Que otros sepan cómo colaborar Entorno y comprobaciones
SECURITY Canal privado para vulnerabilidades Reporte privado de GitHub
CODE_OF_CONDUCT Normas de convivencia Sí
CHANGELOG Qué cambia en cada versión Keep a Changelog + SemVer
LICENSE y NOTICE Qué se puede hacer con el código y créditos MIT + NOTICE
Releases por tag Publicar versiones reproducibles Tag v* → borrador de release

Problemas frecuentes

Síntoma Causa probable Solución
git clone por SSH se queda colgado Falta el puerto alternativo Añade Port en ~/.ssh/config
Forgejo arranca pero la web da 403 Estás fuera de la LAN/VPN Conéctate a la VPN o revisa las redes del matcher
Diffs enormes sin cambios reales Finales de línea CRLF/LF mezclados .gitattributes con eol=lf
.env aparece en git status Falta el patrón en .gitignore Añade **/.env
git push falla tras filter-repo Se eliminó el remoto a propósito git remote add origin ... y --force
El CI solo falla en Windows Rutas o finales de línea Mira el log de ese job; fail-fast: false ayuda

Conclusión

Tener la infraestructura en Git cambió mi forma de trabajar: ya no toco nada sin dejar rastro, y volver atrás es un comando. La lección más cara fue la del secreto: borrarlo no basta, hay que rotarlo. Y en lo público, las piezas del checklist son lo que separa un proyecto de fin de semana de uno en el que otros confían.

En el próximo capítulo daré el siguiente paso: automatizar los despliegues, para que un commit no solo quede registrado, sino que también se aplique solo.

My NAS is a collection of folders, each with its own docker-compose.yml. Without version control, every change is lost: there is no trace of what was touched or when. My fix was to turn that folder into a Git repository and host it on my own server, Forgejo.

Now every infrastructure change is a commit with its message. I can see a service’s history and roll back with one command. In this chapter I explain how I set it up, how I protect secrets and how I prepare a public repository so it looks, and is, a professional project.

What you will build

  • Forgejo in Docker: your own Git server, with the panel reachable only from home.
  • A repository with all your docker-compose files as the source of truth.
  • A simple change workflow and a commit convention.
  • A safe way to handle secrets (and what to do if one leaks).
  • The list of pieces of a professional public repo, with a real example.

Requirements

  • Docker and Docker Compose on the NAS or a server.
  • Caddy in front as a proxy (see the “Mi NAS paso a paso” series). In the examples, git.example.com.
  • Your home network is 192.168.1.0/24 and the VPN 10.8.0.0/24. These are example values: use yours.

Step 1: Forgejo in Docker

Forgejo is a lightweight Git server, derived from Gitea. With SQLite and 256 MB of memory it is more than enough for personal use.

services:
  forgejo:
    image: codeberg.org/forgejo/forgejo:9      # pin the major version
    container_name: forgejo
    restart: unless-stopped
    environment:
      USER_UID: "1000"                          # user that owns ./data on the host
      USER_GID: "1000"
      FORGEJO__database__DB_TYPE: sqlite3       # no separate database server
      FORGEJO__server__DOMAIN: git.example.com
      FORGEJO__server__SSH_DOMAIN: git.example.com
      FORGEJO__server__ROOT_URL: https://git.example.com/
      FORGEJO__server__HTTP_PORT: "3000"
      FORGEJO__server__START_SSH_SERVER: "true" # Forgejo's own SSH server
      FORGEJO__server__SSH_PORT: "2022"         # port shown in clone URLs
      FORGEJO__server__SSH_LISTEN_PORT: "2022"  # port it listens on
      FORGEJO__service__DISABLE_REGISTRATION: "true" # nobody can sign up
      FORGEJO__service__REQUIRE_SIGNIN_VIEW: "true"  # nothing visible without a session
    volumes:
      - ./data:/data                            # repos, database and configuration
    ports:
      - "127.0.0.1:3001:3000"                   # web: only the proxy reaches it
      - "2022:2022"                             # SSH: LAN only, do not open it on the router
    security_opt:
      - no-new-privileges:true
    mem_limit: 256m
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/healthz"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

The important decisions:

  • SSH on an alternative port. Port 22 is already used by the server’s own SSH. Forgejo ships its own SSH server and I put it on another port (here 2022, as an example).
  • Closed registration and no anonymous view. It is a personal server: only the people I create can get in.
  • The web only on 127.0.0.1. It cannot be reached from the network; only Caddy.
docker compose up -d
docker ps --filter name=forgejo    # wait until you see "healthy"

The first time you open https://git.example.com, Forgejo shows the installation wizard. Check that the values match the compose file and create the administrator account there.

The panel, only from home or the VPN

In Caddy, the service block answers 403 to any IP that is not from home, the VPN or the server itself:

git.example.com {
    # Anything not coming from LAN, VPN or localhost gets a 403
    @external not remote_ip 192.168.1.0/24 10.8.0.0/24 127.0.0.1/8
    respond @external 403

    reverse_proxy localhost:3001
}

Cloning over SSH with the alternative port

To avoid typing the port in every command, add this to ~/.ssh/config on your computer:

Host git.example.com
    User git
    Port 2022
    IdentityFile ~/.ssh/id_ed25519

Upload your public key in Forgejo (user settings, SSH keys) and clone as usual:

git clone git@git.example.com:usuario/infra.git

Step 2: infrastructure as a repository

On my NAS, the folder that holds every stack is the root of a repository called homelab-infra. Each subfolder is a service with its docker-compose.yml. The Git history documents every change.

What does not go into the repo: bulky data, databases, logs and secrets. This .gitignore at the root takes care of it:

# Service data: backed up, not versioned
**/data/
**/postgres/
**/redis/
**/logs/

# Secrets: never in the repo
**/.env
*.pem
*.key
*.jks

# Copies and dumps
*.bak
*.dump

And a .gitattributes so line endings do not change depending on whether you edit over SMB from Windows or over SSH from Linux:

* text=auto eol=lf

The workflow for a change

Always the same, in three steps:

# 1. Edit the stack's compose file
cd /volume1/docker
nano observability/docker-compose.yml

# 2. Save the change with a clear message
git add observability/docker-compose.yml
git commit -m "chore(observability): grafana 12.3.0 -> 12.3.1"

# 3. Apply it
cd observability && docker compose up -d

Message convention

I use a fixed format type(stack): description. That way the history can be read at a glance:

Type When
feat(stack) New service or feature
fix(stack) Configuration fix or bug fix
chore(stack) Version bump with no functional changes
security(stack) Urgent security update
refactor(stack) Reorganisation with no behaviour change
docs Documentation

Rolling back

git log --oneline observability/docker-compose.yml       # history of one stack
git checkout <hash> -- observability/docker-compose.yml  # restore an older version
git commit -m "fix(observability): rollback after failure"
cd observability && docker compose up -d

Documentation generated from the repo

My notes for each service included a copy of the compose file. Over time, the copies went stale. Now I have a weekly check that warns me if a note does not match the repo, and a script that regenerates the notes from the compose files. The idea is that documentation is a view of the repo, not a hand-made copy.

This is a minimal script with the same idea:

#!/usr/bin/env bash
# Regenerates one Markdown note per stack from its docker-compose.yml
set -euo pipefail
REPO="/volume1/docker"          # repository root
DOCS="/volume1/docs/servicios"  # folder for generated notes
mkdir -p "$DOCS"

for compose in "$REPO"/*/docker-compose.yml; do
  stack="$(basename "$(dirname "$compose")")"
  {
    printf '# %s\n\n' "$stack"
    printf '> Note generated from the repo. Do not edit by hand.\n\n'
    printf '```yaml\n'
    cat "$compose"
    printf '\n```\n'
  } > "$DOCS/$stack.md"
  echo "OK $stack"
done

If you want it to run on its own, call it from a post-commit hook in the repository: every commit refreshes the notes.

Step 3: secrets outside Git

The rule is simple: real values live in .env, with restricted permissions, and are never pushed. The repo holds a .env.example with the same keys and no values. It lets you rebuild the .env from scratch during a restore.

# .env.example — versioned. Copy it to .env and fill in the values.
CLOUDFLARE_API_TOKEN=
POSTGRES_PASSWORD=
cp .env.example .env
chmod 600 .env                 # only the owner can read it
openssl rand -hex 32           # generates a random secret to paste into .env

Before every commit, I check that nothing sensitive slips in. This hook does it for me (save it as .git/hooks/pre-commit and make it executable):

#!/usr/bin/env bash
# Blocks the commit if files that look like secrets are staged
set -euo pipefail
if git diff --cached --name-only | grep -E '(^|/)\.env$|\.(pem|key|jks|bak|dump)$'; then
  echo "ERROR: sensitive files are staged for commit. Review them." >&2
  exit 1
fi

The real case: a secret in the history

It happened to me: sensitive material ended up inside the infrastructure repository. I removed it from the tree, but it was still in the history, and anyone with a copy of the repo could recover it.

This is the right order:

  1. Rotate the secret first. Revoke it in the service that issued it and generate a new one. Put it in the .env and restart the service. No matter how fast you clean up: the old value may already be copied in another clone, a backup or a cache.
  2. Then clean the history with git filter-repo:
# Work on a fresh clone, never on your only copy
git clone git@git.example.com:usuario/infra.git infra-limpio
cd infra-limpio

# Remove the file from every commit
git filter-repo --invert-paths --path caddy/.env

# filter-repo removes the remote on purpose: add it back and force-push
git remote add origin git@git.example.com:usuario/infra.git
git push --force --all
git push --force --tags
  1. Clone again on every machine that had an old copy, and add the pattern to .gitignore so it does not happen again.

Cleaning the history without rotating the secret is only cosmetic. Rotating it is what protects you.

Step 4: a professional public repository

My infrastructure is private, but my public projects go to GitHub. There, a repo does not just store code: it is also your business card. As a real example I use DozeForge, a desktop application (Tauri with Rust and SvelteKit) that I released as open source.

CI with GitHub Actions

DozeForge’s workflow runs on every push to main, on every pull request and on every v* tag. It has several separate jobs, and each one answers a question:

  • Rust, on three systems. A matrix with ubuntu-22.04, windows-latest and macos-latest. On each one: cargo fmt -- --check (formatting), cargo clippy --all-targets -- -D warnings (any warning breaks the build) and cargo test --all-targets. With fail-fast: false, if Windows fails you still see the macOS and Linux results.
  • Frontend. npm ci, npm run check (types and accessibility) and npm test (Vitest).
  • Android smoke test. It boots an emulator with an API matrix (31, 33 and 34) and tries the ADB commands the app relies on. It is advisory: it has continue-on-error: true because CI emulators sometimes fail for no reason.
  • Installers. Only if Rust and frontend pass (needs), it builds the installer for each system and stores it as an artifact.
  • Release. Only on v* tags: it gathers the installers into a draft GitHub release.

Two details that save time and money: dependency caching (cache: "npm" in actions/setup-node@v4 and Swatinem/rust-cache@v2 for Cargo) and the concurrency block with cancel-in-progress: true, which cancels the previous run if you push another commit to the same branch.

A minimal workflow to copy

If your project uses Node, this is a good starting point. It uses the same action versions as DozeForge:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

# If another commit reaches the same branch, cancel the previous run
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

# Minimal permissions: the workflow only needs to read the code
permissions:
  contents: read

jobs:
  test:
    name: Node (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "npm"               # reuses the npm cache between runs
      - run: npm ci                  # installs exactly what the lockfile says
      - run: npm run check --if-present
      - run: npm run test --if-present
      - run: npm run build --if-present

Save it as .github/workflows/ci.yml. --if-present keeps it from failing if your project does not have one of those scripts yet.

Dependabot

Dependabot opens pull requests when new versions of your dependencies come out. DozeForge watches three ecosystems every week: npm, Cargo and the GitHub Actions themselves. A minimal version for a Node project:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 5    # no more than 5 open PRs at once
    labels: ["dependencies"]

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    labels: ["ci"]

Since every PR goes through CI, you know whether the update breaks something before merging it.

Templates, policies and license

  • Issue templates as forms (YAML): the bug one asks for the device, operating system and steps. With blank_issues_enabled: false nobody opens an empty issue, and the contact links send questions to Discussions and security issues to the private channel.
  • PR template with a checklist: npm run check, npm test and cargo test pass, there is a test for new code, and so on.
  • CONTRIBUTING.md: how to set up the environment and which checks to run before opening a PR (the same ones CI runs).
  • SECURITY.md: how to report a vulnerability privately (with GitHub’s private reporting option), never in a public issue.
  • CODE_OF_CONDUCT.md: the community rules.
  • CHANGELOG.md following Keep a Changelog and semantic versioning.
  • LICENSE (MIT in this case) and NOTICE, which credits the third-party software bundled in the installer and its license.

Professional repo checklist

Piece What it is for In DozeForge
CI on every push and PR Catch failures before merging Rust, frontend, Android and builds
OS matrix Test where your users will run it Ubuntu, Windows and macOS
Linters as a gate Keep the code clean fmt --check, clippy -D warnings
Dependency cache Faster CI npm and Cargo
Dependabot Dependencies up to date with no effort npm, Cargo and Actions, weekly
Issue and PR templates Useful reports and tidy reviews YAML forms and checklist
CONTRIBUTING So others know how to contribute Environment and checks
SECURITY Private channel for vulnerabilities GitHub private reporting
CODE_OF_CONDUCT Community rules Yes
CHANGELOG What changes in each version Keep a Changelog + SemVer
LICENSE and NOTICE What can be done with the code, and credits MIT + NOTICE
Releases by tag Publish reproducible versions v* tag → draft release

Common problems

Symptom Likely cause Fix
git clone over SSH hangs The alternative port is missing Add Port in ~/.ssh/config
Forgejo starts but the web returns 403 You are outside the LAN/VPN Connect to the VPN or check the matcher’s networks
Huge diffs with no real changes Mixed CRLF/LF line endings .gitattributes with eol=lf
.env shows up in git status The pattern is missing from .gitignore Add **/.env
git push fails after filter-repo The remote was removed on purpose git remote add origin ... and --force
CI only fails on Windows Paths or line endings Read that job’s log; fail-fast: false helps

Conclusion

Having my infrastructure in Git changed the way I work: I no longer touch anything without leaving a trace, and rolling back is one command. The most expensive lesson was the secret: deleting it is not enough, you have to rotate it. And for public work, the pieces on the checklist are what separate a weekend project from one that others trust.

In the next chapter I will take the next step: automating deployments, so a commit is not just recorded but also applied on its own.

Preguntas frecuentes

¿Por qué Forgejo y no un repositorio privado en GitHub?

Porque mi infraestructura describe mi red: puertos, servicios y rutas. Prefiero que ese mapa no salga de casa. Forgejo es ligero, corre en el NAS y funciona igual que cualquier Git.

Si borro el .env del repo con un commit, ¿ya está solucionado?

No. El fichero sigue en el historial y cualquiera con una copia puede recuperarlo. Rota el secreto primero, después limpia el historial y fuerza la subida.

¿Necesito todo el checklist en un proyecto pequeño?

No desde el primer día. Empieza por CI, licencia y README. Añade Dependabot, plantillas y SECURITY.md cuando el proyecto sea público y reciba visitas.

Fuentes

Proyecto relacionado

DozeForge — Control total de tu Android por ADB (sin root)

App de escritorio que te da el control real de tu Android desde el PC, sin rootear. La conectas por ADB y optimizas batería, memoria, red y decenas de ajustes del sistema desde un solo sitio: caza las apps que te drenan la batería (aunque se escondan tras Google Play Services), quita bloatware de fábrica y aplica perfiles de un clic — y todo es reversible con rollback atómico. Por dentro: Tauri 2 + Rust + SvelteKit 5, telemetría real (dumpsys/sysfs) y primitivas públicas de Android.

Compartir:LinkedInX
ivanjonasfc@dev: ~ — zshsesión en vivo

❯ neofetch

Perfilivanjonasfc@dev

Iván Jonás Fernández Correa
user
ivanjonasfc@dev
role
Full-Stack & DevOps
loc
Gijón, Asturias
títulos
DAM + ASIR
stack
Java · Python · TS · Docker · Linux · IA
busco
Full-Stack · DevOps · Sistemas
status
Disponible para trabajar

❯ uptime --human

Años en tecnología
6+
Proyectos en producción
6
Proyectos entregados
10+
Servicios self-hosted
~30

❯ figlet "Iván Jonás"

# Hola, soyIván Jonás

Desarrollador Full-Stack & DevOpsFull-Stack Developer & DevOps

Desarrollador Full-Stack & DevOps · Gijón, Asturias · ● Disponible para trabajar

↓ desliza, toca o escribe para abrir la sesión

❯ cat about.md

Desarrollo aplicaciones web y móviles de principio a fin (Java/Spring Boot, Python/FastAPI, TypeScript/React y Flutter) y me ocupo de su puesta en producción y operación con Docker, Linux, CI/CD y monitorización. Actualmente administro la infraestructura IT de una pyme y mantengo un homelab de unos 30 servicios en el que evalúo nuevas tecnologías, incluida IA local y privada (Ollama, RAG, MCP). Técnico Superior en DAM y ASIR.

❯ ls ~

Iván en números

Mi actividad en GitHub, mi trayectoria y mi infraestructura · datos en vivo.

Community

0

followers

followerfollowerfollowerfollowerfollower+8

Lenguajes

TypeScript32%
Dart14%
Python12%
HTML7%
SCSS5%

Tech Stack

Frontend

React
Next.js
Angular
Astro
Ionic
Flutter
TypeScript
Tailwind
shadcn/ui
React
Next.js
Angular
Astro
Ionic
Flutter
TypeScript
Tailwind
shadcn/ui

Backend

NestJS
FastAPI
Spring Boot
Python
Java
PostgreSQL
Redis
Qdrant
pgvector
MCP
RAG
LLMs
NestJS
FastAPI
Spring Boot
Python
Java
PostgreSQL
Redis
Qdrant
pgvector
MCP
RAG
LLMs

Tools

Docker
Linux
Nginx
Cloudflare
Forgejo
n8n
WireGuard
Prometheus
Grafana
Git
Windows Server
Active Directory
Loki
Tempo
Docker
Linux
Nginx
Cloudflare
Forgejo
n8n
WireGuard
Prometheus
Grafana
Git
Windows Server
Active Directory
Loki
Tempo

Proyectos Destacados

Soluciones reales a problemas reales. Una selección de mis trabajos más recientes.

Web Rutas Raíces
Cliente real

Web Rutas Raíces

Web de senderismo para cliente real en Asturias. Astro 7 + React 19 + Tailwind 4, mapas Leaflet con perfil de elevación a partir de GPX y versión bilingüe ES/EN. Alta de rutas automatizada con n8n + IA; WebP, CSP y cumplimiento RGPD.

AstroReactLeafletn8n
Infraestructura IT para una pyme
Cliente real

Infraestructura IT para una pyme

Diseño, implantación y soporte continuo de la infraestructura IT de una pequeña empresa que trabajaba desde un único portátil: NAS en RAID 1 con Btrfs y snapshots, permisos por grupos sobre SMB, puestos Windows 11 y una capa Docker para gestión documental, automatización y monitorización.

NASRAID 1BtrfsWindows 11DockerPaperless-ngx
OposApp
TFG

OposApp

Mi Trabajo de Fin de Grado (DAM): app móvil en Flutter que automatiza el seguimiento de convocatorias del BOPA y genera tests de práctica con IA local (Ollama), con privacidad total por diseño.

FlutterSpring BootIA Local (Ollama)PostgreSQLRGPD
English Brain — Tu coach de entrevistas en inglés con IA, autoalojado
Open Source · Activo

English Brain — Tu coach de entrevistas en inglés con IA, autoalojado

Una plataforma privada y autoalojada para preparar entrevistas técnicas en inglés hablando, no escribiendo. Tocas un orbe, hablas, y el coach te transcribe, te corrige la pronunciación fonema a fonema y te programa el repaso con repetición espaciada (py-fsrs). Toda la IA —el modelo de lenguaje, el reconocimiento de voz y la síntesis— corre en tu propio hardware: tu voz y tus respuestas nunca salen de tu red. Por dentro: cliente Flutter (Android, Web y Windows), orquestador FastAPI y una arquitectura GPU híbrida sobre Docker + Caddy.

IA localVozFlutterFastAPISelf-hostedPronunciación
QuantumSec — Kompliance
FCT 2025

QuantumSec — Kompliance

Prácticas FCT en remoto en una empresa de ciberseguridad (plataforma de cumplimiento Kompliance). Llevé su gestor de auditorías de Supabase a una API propia self-hosted con FastAPI: multitenancy validado por JWT, secretos cifrados, logs de auditoría inmutables y un gestor de contraseñas con claves de un solo uso cifradas.

FastAPIReactTypeScriptPostgreSQLSeguridad
DozeForge — Control total de tu Android por ADB (sin root)
Open Source · Beta 1

DozeForge — Control total de tu Android por ADB (sin root)

App de escritorio que te da el control real de tu Android desde el PC, sin rootear. La conectas por ADB y optimizas batería, memoria, red y decenas de ajustes del sistema desde un solo sitio: caza las apps que te drenan la batería (aunque se escondan tras Google Play Services), quita bloatware de fábrica y aplica perfiles de un clic — y todo es reversible con rollback atómico. Por dentro: Tauri 2 + Rust + SvelteKit 5, telemetría real (dumpsys/sysfs) y primitivas públicas de Android.

Android sin rootOptimizaciónTauri 2RustSvelteKit
SubsForge — Subtítulos con IA, 100% en local
Open Source

SubsForge — Subtítulos con IA, 100% en local

App de escritorio para traducir, generar, sincronizar, fusionar, limpiar, doblar, resumir e incrustar subtítulos de vídeo con IA — sin servicios de terceros y funcionando 100% en tu máquina. Nueve herramientas en una: traducción por lotes (nube u Ollama local), transcripción con Whisper, sincronización por detección de voz (VAD), doblaje con voces TTS, hardsub y limpieza de audio. Por dentro: Tauri 2 + Rust como puente y un sidecar de Python (faster-whisper, FFmpeg, ffsubsync, edge-tts) que hace el trabajo pesado sin bloquear nunca la interfaz.

Subtítulos IAWhisperTauri 2RustPython100% local
OrquestaGit — Tu DevOps senior local con IA
Open Source · En desarrollo

OrquestaGit — Tu DevOps senior local con IA

Una app de escritorio que automatiza el trabajo repetitivo de DevOps sobre tus repositorios: escanea su estado, bloquea secretos antes de cada commit, genera pipelines de CI/CD y audita dependencias, todo desde un único panel de control. La IA corre en local con Ollama, así que tu código nunca sale de tu equipo. Construida con Tauri 2 y un motor de orquestación en Python.

DevOpsIA localSeguridadTauri 2PythonCI/CD
Job-radar · Empleo CRM
Open Source

Job-radar · Empleo CRM

CRM web self-hosted que automatiza y centraliza mi búsqueda de empleo: reúne las ofertas, las puntúa con IA (Groq/Gemini/Ollama), genera carta y ajustes de CV, y las sigue en un kanban. Astro SSR + React + PostgreSQL, todo configurable desde la web y replicable con un comando.

Astro SSRReactPostgreSQLIA · LLMsn8nDocker
LifeOS — Suite familiar con IA
En desarrollo

LifeOS — Suite familiar con IA

Suite SaaS familiar autohospedada con IA propia ("Lito"). Backend NestJS + PostgreSQL + BullMQ y app Angular 18 + Ionic. RAG sobre pgvector/Qdrant y LLMs vía Model Context Protocol (MCP). Módulos de chat, gastos con splits en tiempo real, calendario, nube y notas.

NestJSAngularIA / RAGMCPDocker
Infraestructura Pesoz
En Producción

Infraestructura Pesoz

Homelab self-hosted en producción 24/7: un NAS Synology con ~30 servicios en Docker tras Caddy + CrowdSec y un portátil con GPU como servidor de IA y observabilidad en k3s. VPN WireGuard, GitOps con Forgejo y estado en vivo con Uptime Kuma.

Dockerk3sCaddyWireGuardCI/CDObservabilidad
Hooklab
Nuevo

Hooklab

Constructor, firmador y verificador de webhooks 100% en el navegador: crea payloads con variables simuladas, firma y verifica HMAC (GitHub, Stripe, Shopify, Slack), genera con IA local y dispara un POST. Zero-backend, con el estado compartible en la URL.

AstroZero-backendWeb CryptoIA local
Acopio de Código
Nuevo

Acopio de Código

Página web interactiva con snippets de código de varios lenguajes: busca, copia y ten todo a mano. Autohospedada en mi propia infraestructura.

ReactSnippetsSelf-hosted
Vagabond Battery Saver
Open Source

Vagabond Battery Saver

Módulo Magisk para Android root. Optimización agresiva de batería mediante gestión de governors de CPU y perfiles de energía al bloquear la pantalla.

AndroidShell ScriptMagisk
Happines&CO
Educativo

Happines&CO

Sitio web corporativo para empresa de eventos, desarrollado durante el ciclo de DAM. Diseño responsive y estructura semántica limpia.

HTML5CSS3JavaScript
Portfolio Web
Meta

Portfolio Web

Este mismo sitio. Astro 7 + React (islas) + Tailwind, self-hosted con Docker y Caddy. Cada proyecto es una carpeta con su contenido y se da de alta con un script (npm run proyecto:add).

AstroReactTailwindSelf-hosted

Últimas publicaciones

Guías prácticas probadas en mi propia infraestructura.

Portada: escudo con símbolo de prohibido sobre fondo oscuro con el título AdGuard Home
Redes y seguridad

AdGuard Home en Docker: sin publicidad en toda la casa y tus subdominios en local

Monto AdGuard Home en Docker para bloquear publicidad en toda la red, usar DNS cifrado y resolver mis subdominios en casa sin depender del router.

8 min de lectura

#adguard-home#dns#docker#privacidad#homelab
Leer
Portada: candado con flechas de entrada sobre fondo oscuro con el título Caddy como puerta de entrada
Redes y seguridad

Caddy como puerta de entrada del homelab: HTTPS, cabeceras seguras y CrowdSec

Cómo uso Caddy en Docker como única entrada a mi NAS: HTTPS por DNS de Cloudflare, snippets de seguridad, paneles solo LAN y CrowdSec contra IPs abusivas.

11 min de lectura

#caddy#https#docker#crowdsec#seguridad#cloudflare
Leer
Portada: una caja de contenedor con una campana de aviso y un avión de papel sobre fondo oscuro con el título Diun y Telegram
Automatización

Diun y alertas en Telegram: entérate de las imágenes nuevas sin actualizar a ciegas

Uso Diun para saber cuándo hay imágenes Docker nuevas y un único bot de Telegram para recibir los avisos de Diun, CrowdSec y Uptime Kuma.

7 min de lectura

#diun#docker#telegram#alertas#observabilidad
Leer
Portada: ramas de Git que se unen en una marca de verificación sobre fondo oscuro con el título Forgejo y GitHub Actions
Automatización

Forgejo y GitHub Actions: tu infraestructura en Git y un repo público profesional

Monto Forgejo en casa como fuente de verdad de mi infraestructura, protejo los secretos y preparo un repo público con CI, Dependabot y plantillas.

13 min de lectura

#git#forgejo#github-actions#ci#gitops#buenas-practicas
Leer
Portada: logotipo de reproducción sobre fondo oscuro con el título Jellyfin en mi NAS
Homelab

Jellyfin en un NAS: Docker, transcoding por hardware y la TV que no quería conectar

Cómo monto Jellyfin en un NAS con Intel, activo el transcoding por GPU, lo publico con Caddy y arreglo el error -27 de las TV LG antiguas.

7 min de lectura

#jellyfin#docker#nas#caddy#transcoding#synology
Leer
Portada: gráfica de líneas y barras de un panel de métricas sobre fondo oscuro con el título Observabilidad en casa
Homelab

Observabilidad en casa: Grafana, Prometheus y Loki sin ahogar el NAS

Monto métricas, logs y alertas con Prometheus, Grafana y Loki, y cuento por qué acabé moviendo el stack del NAS a un portátil con k3s.

11 min de lectura

#grafana#prometheus#loki#observabilidad#docker#kubernetes
Leer
Portada: línea de latido y barras de disponibilidad sobre fondo oscuro con el título Uptime Kuma
Homelab

Uptime Kuma: vigila tus servicios y enseña su estado en vivo en tu CV

Monto Uptime Kuma en Docker, configuro monitores y avisos útiles, publico solo la página de estado y la muestro en mi portfolio con un widget en JS.

8 min de lectura

#uptime-kuma#monitorizacion#docker#caddy#portfolio
Leer

Trabajemos juntos

¿Buscas un perfil full-stack / DevOps, un colaborador o un freelance? Cuéntame qué necesitas y te respondo en menos de 24 h.

ivanjonasfc@dev:~/contacto

# ¿Qué te trae por aquí?

↳
↳
↳

Sin spam. Tus datos solo se usan para responderte.

Terminal interactiva