Compare commits

...

10 commits

Author SHA1 Message Date
Claude
d3ef4d2030
fix: use /bin/sh in postgres init script — Alpine has no bash
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-26 11:47:58 +00:00
Claude
de4b5c49ab
fix: drop service_healthy depends_on — podman-compose doesn't support it
Forgejo restart: unless-stopped handles the retry loop until Postgres is ready.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-26 11:41:27 +00:00
Claude
bd863cdf33
fix: hardcode pg_isready args to avoid podman-compose $$ escaping issue
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-26 11:33:57 +00:00
Claude
22a6ab103c
fix: wait for Postgres to be ready before starting Forgejo
Adds a pg_isready healthcheck to the postgres service and upgrades the
Forgejo depends_on to condition: service_healthy, preventing the
"connection refused" crash on startup.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-26 11:27:10 +00:00
Claude
ea172ae336
feat: lock Forgejo install wizard via env var
Sets FORGEJO__security__INSTALL_LOCK=true so Forgejo skips the first-run
wizard and uses the env var configuration directly.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-26 11:21:09 +00:00
Claude
36b89d7620
fix: use FORGEJO_DB_PASSWORD env var in postgres init script
Replaced hardcoded 'CHANGE_ME' in the SQL init file with a shell script
that reads FORGEJO_DB_PASSWORD from the environment. Also pass the variable
into the postgres service in docker-compose.yml so it is available at init time.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-26 11:11:53 +00:00
Claude
9ba81bd809
fix: drop Caddy --resume, restore app routes from DB on startup
--resume caused Caddyfile changes (e.g. new Forgejo block) to be silently
ignored on restart because Caddy preferred its saved in-memory config.

Instead, Caddy now always starts clean from the Caddyfile, and the HIY
server re-registers every app's Caddy route from the DB on startup
(restore_caddy_routes). This gives us the best of both worlds:
- Caddyfile changes (static services, TLS config) are always picked up
- App routes are restored automatically without needing a redeploy

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-26 10:56:04 +00:00
Claude
97929c11de
fix: add static Caddyfile block for Forgejo (forgejo:3000, not hiy-forgejo)
Forgejo is a docker-compose service, not a HIY-deployed container. HIY's
dynamic routing uses the hiy-<id>:<port> naming convention which doesn't
match. A static block pointing to forgejo:3000 is the correct approach.

FORGEJO_DOMAIN falls back to forgejo.localhost so Caddy starts cleanly
on installs that don't use Forgejo.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-26 10:49:19 +00:00
Claude
06a8cc189a
fix: remove docker.io/ prefix from Forgejo image (Codeberg registry)
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-26 10:46:09 +00:00
Claude
b6e223291a
feat: add Forgejo service + Postgres database provisioning
- docker-compose.yml: Forgejo service on hiy-net, configured via env vars
- postgres-init/01-forgejo.sql: creates forgejo user + database on first Postgres init
- .env.example: document FORGEJO_DB_PASSWORD and FORGEJO_DOMAIN

Routing: add FORGEJO_DOMAIN as an app in HIY pointing to forgejo:3000,
or add a Caddyfile block manually.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-26 10:44:19 +00:00
6 changed files with 89 additions and 1 deletions

View file

@ -11,3 +11,7 @@ HIY_ADMIN_PASS=changeme
# Postgres admin password — used by the shared cluster.
# App schemas get their own scoped users; this password never leaves the server.
POSTGRES_PASSWORD=changeme
# Forgejo (optional — only needed if you add the forgejo service to docker-compose.yml).
FORGEJO_DB_PASSWORD=changeme
FORGEJO_DOMAIN=git.yourdomain.com

View file

@ -68,8 +68,35 @@ services:
POSTGRES_DB: hiy
POSTGRES_USER: hiy_admin
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
FORGEJO_DB_PASSWORD: ${FORGEJO_DB_PASSWORD}
volumes:
- hiy-pg-data:/var/lib/postgresql/data
# SQL files here run once on first init (ignored if data volume already exists).
- ./postgres-init:/docker-entrypoint-initdb.d:ro
networks:
- hiy-net
# ── Forgejo (self-hosted Git) ──────────────────────────────────────────────
forgejo:
image: codeberg.org/forgejo/forgejo:10
restart: unless-stopped
environment:
USER_UID: 1000
USER_GID: 1000
FORGEJO__database__DB_TYPE: postgres
FORGEJO__database__HOST: postgres:5432
FORGEJO__database__NAME: forgejo
FORGEJO__database__USER: forgejo
FORGEJO__database__PASSWD: ${FORGEJO_DB_PASSWORD}
FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN}
FORGEJO__server__ROOT_URL: https://${FORGEJO_DOMAIN}/
FORGEJO__server__SSH_DOMAIN: ${FORGEJO_DOMAIN}
# Skip the first-run wizard — everything is configured via env vars above.
FORGEJO__security__INSTALL_LOCK: "true"
volumes:
- forgejo-data:/data
depends_on:
- postgres
networks:
- hiy-net
@ -89,7 +116,7 @@ services:
- ../proxy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
command: caddy run --config /etc/caddy/Caddyfile --adapter caddyfile --resume
command: caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
networks:
- hiy-net
- default
@ -142,6 +169,7 @@ networks:
volumes:
hiy-data:
forgejo-data:
caddy-data:
caddy-config:
hiy-pg-data:

View file

@ -0,0 +1,10 @@
#!/bin/sh
# Create a dedicated database and user for Forgejo.
# Runs once when the Postgres container is first initialised.
# FORGEJO_DB_PASSWORD must be set in the environment (via docker-compose.yml).
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
CREATE USER forgejo WITH PASSWORD '${FORGEJO_DB_PASSWORD}';
CREATE DATABASE forgejo OWNER forgejo;
EOSQL

View file

@ -31,6 +31,15 @@
reverse_proxy server:3000
}
# ── Static services (not managed by HIY) ──────────────────────────────────────
# Set FORGEJO_DOMAIN in .env (e.g. git.yourdomain.com). Falls back to a
# non-routable placeholder so Caddy starts cleanly even if Forgejo isn't used.
{$FORGEJO_DOMAIN:forgejo.localhost} {
tls {$ACME_EMAIL:internal}
reverse_proxy forgejo:3000
}
# Deployed apps are added here dynamically by hiy-server via the Caddy API.
# Each entry looks like:
#

View file

@ -154,6 +154,14 @@ async fn main() -> anyhow::Result<()> {
builder::build_worker(worker_state).await;
});
// Re-register all app Caddy routes from the DB on startup.
// Caddy no longer uses --resume, so routes must be restored each time the
// stack restarts (ensures Caddyfile changes are always picked up).
let restore_db = state.db.clone();
tokio::spawn(async move {
routes::apps::restore_caddy_routes(&restore_db).await;
});
// ── Protected routes (admin login required) ───────────────────────────────
let protected = Router::new()
.route("/", get(routes::ui::index))

View file

@ -47,6 +47,35 @@ fn caddy_route(app_host: &str, upstream: &str, is_public: bool) -> serde_json::V
}
}
/// Re-register every app's Caddy route from the database.
/// Called at startup so that removing `--resume` from Caddy doesn't lose
/// routes when the stack restarts.
pub async fn restore_caddy_routes(db: &crate::DbPool) {
// Give Caddy a moment to finish loading the Caddyfile before we PATCH it.
let caddy_api = std::env::var("CADDY_API_URL").unwrap_or_else(|_| "http://caddy:2019".into());
let client = reqwest::Client::new();
for attempt in 1..=10u32 {
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
if client.get(format!("{}/config/", caddy_api)).send().await.is_ok() {
break;
}
tracing::info!("restore_caddy_routes: waiting for Caddy ({}/10)…", attempt);
}
let apps = match sqlx::query_as::<_, crate::models::App>("SELECT * FROM apps")
.fetch_all(db)
.await
{
Ok(a) => a,
Err(e) => { tracing::error!("restore_caddy_routes: DB error: {}", e); return; }
};
for app in &apps {
push_visibility_to_caddy(&app.id, app.port, app.is_public != 0).await;
}
tracing::info!("restore_caddy_routes: registered {} app routes", apps.len());
}
/// Push a visibility change to Caddy without requiring a full redeploy.
/// Best-effort: logs a warning on failure but does not surface an error to the caller.
async fn push_visibility_to_caddy(app_id: &str, port: i64, is_public: bool) {