Automatización
Forgejo y GitHub Actions: tu infraestructura en Git y un repo público profesional
13 min de lectura
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/24y la VPN10.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:
- Rota el secreto primero. Revócalo en el servicio que lo emitió y genera uno nuevo. Ponlo en el
.envy 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é. - 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
- Vuelve a clonar en cada equipo que tuviera una copia antigua, y añade el patrón al
.gitignorepara 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-latestymacos-latest. En cada uno:cargo fmt -- --check(formato),cargo clippy --all-targets -- -D warnings(cualquier aviso rompe la build) ycargo test --all-targets. Confail-fast: false, si falla Windows, sigues viendo el resultado de macOS y Linux. - Frontend.
npm ci,npm run check(tipos y accesibilidad) ynpm 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: trueporque 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: falsenadie 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 testycargo testpasan, 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.mdsiguiendo Keep a Changelog y versionado semántico.LICENSE(MIT en este caso) yNOTICE, 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.
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.




