Security: - rate limit /api/webhook routes per-IP and cap concurrent site syncs - global SSE connection cap (256) with new sse_gate - validate ?tail= and cap JSON log responses at 4 MiB - strip ANSI/CSI/OSC and control bytes from streamed log lines - redact webhook secret from request log middleware - scrub host details from /api/health for non-admin viewers - drop container_id from /api/system/stats/top for non-admins - generate webhook secrets via crypto/rand; require >=32 chars on insert - verify iid path consistency in streamContainerLogs - LimitReader on site webhook body; reject malformed non-empty bodies Concurrency / correctness: - stats collector: Stop() no longer hangs without Start(), semaphore acquired in parent loop so ctx cancellation short-circuits the queue, in-flight tick cancellable via shared base context, zero-ts guard - webhook handler: replace fire-and-forget goroutine with WaitGroup-tracked workers + Drain() wired into graceful shutdown - $derived(() => ...) mis-idiom fixed in ContainerStats / InstanceCard / ProjectCard (returned function instead of value) - SystemResourcesCard: rename `window` and `t` locals to avoid shadowing globalThis.window and the i18n `t` import Quality / performance: - replace O(n^2) insertion sort with sort.Slice in stats top - runMigrations only swallows duplicate-column / already-exists errors - PruneStatsSamplesBefore wrapped in a transaction - collapse N+1 in unusedImageStats / pruneImages to one ListAllInstances pass; surface DB errors instead of silently treating them as inactive - run Docker Info + DiskUsage in parallel via errgroup - container log SSE emits `: ping` heartbeat every 20 s - imageMatches case-insensitive on registry host (RFC behaviour) - log warning on invalid stage tag pattern instead of silent skip - reject malformed non-empty site webhook payloads Frontend / i18n: - shared formatBytes utility replaces three local copies - statsInterval store drives dynamic "no samples / collection disabled" copy across ContainerStats and SystemResourcesCard - top consumers row now shows owner_name (project/stage or site name) - drop seven `as any` casts on the Settings type; add cloudflare_api_token write-only field - move "Service status", "Docker daemon", "Docker unreachable", "Proxy unreachable", "reachable", and "Docker daemon is not reachable." strings into en/ru i18n bundles
Tinyforge
Self-hosted deployment platform with a web dashboard. Deploy Docker containers from registries with zero-downtime blue-green strategy, host static sites and Deno APIs directly from Git repositories, and manage reverse proxy configuration — all from a single binary.
Features
Container Deployments
- Registry polling and webhook receiver for automatic deployments
- Blue-green deploys with health checks and automatic rollback
- Multi-stage projects (dev, staging, prod) with tag pattern matching
- Real-time deploy logs via SSE streaming
Static Sites
Deploy static sites and Deno-powered APIs directly from Git repositories:
- Git providers: Gitea/Forgejo, GitHub, and GitLab (public and private repos)
- Static mode: Serves HTML/CSS/JS via nginx container
- Deno mode: Full-stack with TypeScript API backend + static frontend — API routes are auto-discovered from
/apifolder using a naming convention (API_get_users,API_post_items, etc.) - Markdown rendering: Optionally converts
.mdfiles to styled HTML - Branch & folder picker: Select any branch and subfolder as the deployment root
- Auto-sync: Trigger redeployment on push or tag events, or manually
- Per-site secrets: Encrypted environment variables injected at runtime
Infrastructure
- NPM / Traefik integration for automatic reverse proxy and SSL configuration
- Cloudflare DNS sync for automatic DNS record management
- Volume management: Create, browse, upload, and download Docker volumes
- Stale container cleanup: Detect and remove unused containers
- Image management: List and prune unused Docker images
- Database backups: Scheduled and manual backups with one-click restore
- Config export/import: YAML-based seed configuration for reproducible setups
Auth & Security
- Local auth with bcrypt password hashing
- OIDC/SSO support for single sign-on
- Encrypted credential storage (AES-256-GCM)
- Role-based access: Admin and user roles
Prerequisites
- Docker with Docker Compose
- A Docker network for deployed containers (e.g.
staging-net) - Nginx Proxy Manager (optional, for automatic proxy configuration)
- Wildcard DNS pointing to your server (for subdomain-based routing)
Quick Start
-
Create the Docker network (containers will be attached to this):
docker network create staging-net -
Create a
.envfile (see.env.example):cp .env.example .env # Edit .env and set ENCRYPTION_KEY and ADMIN_PASSWORD # Generate a key: openssl rand -hex 32 -
Start Tinyforge:
docker compose up -d -
Open the dashboard at
http://localhost:8080and log in withadmin/ yourADMIN_PASSWORD.
Configuration
Environment Variables
| Variable | Required | Description |
|---|---|---|
ENCRYPTION_KEY |
Yes | AES-256 key for encrypting stored credentials. Use openssl rand -hex 32 |
ADMIN_PASSWORD |
Yes (first launch) | Password for the default admin user |
SEED_FILE |
No | Path to YAML seed config (default: ./tinyforge.yaml) |
DATA_DIR |
No | SQLite database directory (default: ./data) |
LISTEN_ADDR |
No | HTTP listen address (default: :8080) |
NPM_URL |
No | Override NPM API URL (otherwise uses value from settings) |
POLLING_INTERVAL |
No | Registry polling interval, Go duration string e.g. 5m (default from settings) |
Seed Config
On first launch, Tinyforge imports a YAML seed file to pre-configure registries, projects, and settings. See tinyforge.example.yaml for the full format.
Webhook Integration
After setup, find your webhook URL at Settings > Webhook URL in the dashboard. Configure your CI/CD (Gitea Actions, GitHub Actions) to POST to this URL on image push:
curl -X POST https://your-domain/api/webhook/<secret> \
-H "Content-Type: application/json" \
-d '{"image": "registry.example.com/org/app:v1.2.3"}'
OIDC Setup
- Go to Settings > Auth in the dashboard
- Switch auth mode to OIDC
- Enter your provider's Issuer URL, Client ID, and Client Secret
- Set the Redirect URL to
https://your-domain/api/auth/oidc/callback
Development
# Build frontend
cd web && npm install && npm run build && cd ..
# Run backend (requires ENCRYPTION_KEY and ADMIN_PASSWORD env vars)
go run ./cmd/server
# Or use Make
make build
make dev
Architecture
CI/Registry --> Webhook/Poller --> Deployer --> Docker + NPM
|
Git Repo ----> Static Sites -------> Docker + NPM
|
Event Bus --> SSE --> Web Dashboard
- Backend: Go 1.24, chi router, SQLite (pure Go), Docker SDK
- Frontend: SvelteKit 2, Tailwind CSS 4, TypeScript
- Deployment: Single binary with embedded SPA, multi-stage Dockerfile