A lightweight S3‑compatible storage manager and API with built‑in observability.
Overview • Quickstart • Configuration • API • Observability • Security • Helm • License
Hermes is a small Go service that helps you manage S3‑compatible storage providers (e.g., MinIO, AWS S3), buckets and objects through a simple web UI plus a clean HTTP API. It includes structured logging, request tracing, and basic operational metrics out of the box.
Highlights:
- S3‑compatible providers: MinIO, AWS S3, and other S3 endpoints
- Manage Providers, Buckets, and Objects via REST API and Web UI
- File uploads/downloads, list/delete, copy/move across buckets/providers
- Built‑in auth with roles (viewer/editor/admin). OIDC ready
- Observability: request tracing, structured logs, lightweight metrics
- Works out of the box with SQLite; switch to Postgres without code changes
- Containerized, non‑root runtime, read‑only root filesystem by default
- Helm chart for Kubernetes/OKD/OpenShift
- Go HTTP server using chi router and CORS middleware
- Storage abstraction via minio-go SDK (S3 protocol)
- Persistence via GORM with SQLite (default) or Postgres
- No embedded assets: static UI is served from disk (STATIC_DIR)
- Observability: request traces stored in DB; logs persisted optionally
Key directories:
- cmd/server: main entrypoint
- internal/api: routing, endpoints, auth, tracing
- internal/db: DB init, migrations, logging persistence, bootstrap
- internal/models: GORM models
- web/dist: static web UI assets (served as files)
- deploy/helm/hermes: Helm chart
Choose one of the following ways to run Hermes.
Requirements: podman, podman-compose, curl, jq, Go (optional)
-
Start dependencies: make dev-up
-
Build and run Hermes locally: make run
-
Wait until Hermes is healthy: make wait-server
-
Provision a local MinIO provider via API: make provision-minio
-
Create a demo bucket and upload a sample file: make demo-bucket make demo-upload
Hermes API base will be http://127.0.0.1:8080 (override HTTP_PORT). MinIO defaults: http://127.0.0.1:9000 (console at 9001).
Stop stack: make dev-down
Useful: make dev-logs S=minio make dev-logs S=postgres
Pull the image from GHCR: docker pull ghcr.io/arencloud/hermes:latest
Run with a writable data volume for SQLite (default):
docker run --rm -p 8080:8080
-e DB_DRIVER=sqlite -e DB_PATH=/data/hermes.db
-v hermes-data:/data
ghcr.io/arencloud/hermes:latest
Environment defaults inside the image:
- APP_ENV=prod
- HTTP_PORT=8080
- STATIC_DIR=/app/web/dist
- DB_DRIVER=sqlite
- DB_PATH=/data/hermes.db
The chart is included at deploy/helm/hermes.
-
Add image tag (example vX.Y.Z) and install: helm upgrade --install hermes deploy/helm/hermes
--set image.repository=ghcr.io/arencloud/hermes
--set image.tag=vX.Y.Z -
Expose it with Ingress or OpenShift Route (see values.yaml for toggles):
--set ingress.enabled=true
--set ingress.hosts[0].host=hermes.local
--set ingress.hosts[0].paths[0].path=/ -
Persistence: by default enabled for SQLite at /data; configure PVC in values.yaml.
-
Security: runs as non‑root; container root filesystem is read‑only by default.
Hermes is configured via environment variables (see internal/config/config.go):
- APP_ENV: dev|prod (default: dev)
- HTTP_PORT: listening port (default: 8080)
- DB_DRIVER: sqlite|postgres (default: sqlite)
- DB_PATH: path to SQLite DB when DB_DRIVER=sqlite (default: data/hermes.db)
- DATABASE_URL / DB_DSN: Postgres DSN when DB_DRIVER=postgres (e.g. postgres://user:pass@host:5432/db?sslmode=disable)
- STATIC_DIR: static assets directory (default: web/dist; in container: /app/web/dist)
- MAX_UPLOAD_SIZE_BYTES: per-request upload cap; 0 = unlimited (default: 0). Enforced for multipart uploads to prevent OOM.
Selected runtime toggles from Helm values (deploy/helm/hermes/values.yaml):
- service.port: Service port (default 8080)
- http.containerPort: Container port (maps to HTTP_PORT)
- persistence.*: PVC settings and mount path (/data)
- db.driver, db.sqlitePath, db.dsn: DB options
- extraEnv: extra environment variables (e.g., LOG_LEVEL, LOG_JSON, OIDC_* settings)
- containerSecurityContext.readOnlyRootFilesystem: true by default
SQLite (default):
- Zero-config. The DB file is created automatically.
- For containers, mount a volume at /data and use DB_PATH=/data/hermes.db
Postgres:
- Set DB_DRIVER=postgres and DATABASE_URL to your DSN.
- Helm: configure db.driver=postgres and db.dsn accordingly; disable persistence.
On first run, Hermes auto-migrates the schema and bootstraps:
- One auth config row (mode: local, disabled by default)
- A default admin user if no users exist:
- email: admin@local
- temp password: generated and logged once (look for "default admin created" in logs)
Base paths:
- Health: GET /health → "ok"
- Version: GET /api/version → { name: "hermes", version: "" }
- Main API: /api/v1 (requires authentication for most endpoints)
Auth & Users:
- POST /api/v1/auth/login { email, password }
- GET /api/v1/auth/me
- Admin-only user management:
- GET /api/v1/users/
- POST /api/v1/users/ { email, password, role }
- PUT /api/v1/users/{id}
- DELETE /api/v1/users/{id}
Providers & Buckets:
- GET /api/v1/providers
- POST /api/v1/providers { name, type, endpoint, accessKey, secretKey, region, useSSL }
- GET /api/v1/providers/{id}
- PUT /api/v1/providers/{id}
- DELETE /api/v1/providers/{id}
- GET /api/v1/providers/{id}/buckets
- POST /api/v1/providers/{id}/buckets { name, region }
Objects:
- GET /api/v1/providers/{id}/buckets/{name}/objects?prefix=&recursive=
- POST /api/v1/providers/{id}/buckets/{name}/upload (multipart form: file, key)
- GET /api/v1/providers/{id}/buckets/{name}/download?key=
- DELETE /api/v1/providers/{id}/buckets/{name}/objects?key=
- POST /api/v1/providers/{id}/buckets/{name}/copy { srcKey, dstBucket, dstKey?, dstProviderId? } (NDJSON progress)
- POST /api/v1/providers/{id}/buckets/{name}/move { srcKey, dstBucket, dstKey?, dstProviderId? } (NDJSON progress)
Observability & Logs:
- GET /api/v1/obs/metrics → lightweight metrics snapshot
- GET /api/v1/obs/summary → summarized request stats
- GET /api/v1/obs/errors → recent 4xx/5xx traces
- GET /api/v1/trace/recent, GET /api/v1/trace/{id}
- GET /api/v1/logs/recent, GET /api/v1/logs/download
- GET /api/v1/logs/level, PUT /api/v1/logs/level
- Web UI and assets available under /
OpenAPI:
- GET /api/v1/openapi.json (requires editor/admin). Minimal OpenAPI 3.0 spec
- Local auth (email/password). Default first admin is created on empty DB.
- Roles: viewer, editor, admin. Certain endpoints are restricted (e.g., users/* requires admin; openapi.json requires editor/admin).
- OIDC support is planned/available in codebase; configure via extraEnv values (e.g., issuer, client ID/secret) when enabling.
Hermes attaches a per-request trace with an X-Trace-Id header.
- Traces are stored in the database (Trace and TraceEvent rows)
- Structured logs include traceId, method, path, status, timing, and sizes
- Simple counters exposed via /api/v1/obs/metrics and /api/v1/obs/summary
- Container runs as non‑root, with a read‑only root filesystem by default (Helm values)
- CORS is permissive by default for demo convenience; lock down in front proxy for production
- Health and static UI are public; operational endpoints require auth
Prerequisites: Go 1.24+
-
Build: make build
-
Run (SQLite, local assets): make run
-
Test: make test
Server binary is built at ./server (git-ignored).
Multi-arch image is published to GHCR by the Release workflow when tags vX.Y.Z are pushed:
- ghcr.io/arencloud/hermes:
- ghcr.io/arencloud/hermes:v.
- ghcr.io/arencloud/hermes:v
- ghcr.io/arencloud/hermes:latest
Runtime defaults in the image:
- STATIC_DIR=/app/web/dist
- DB_PATH=/data/hermes.db (mount /data)
- Static assets and the logo are baked into the image at /app/web/dist and /app/img/logo
Versioning and Web UI:
- The server exposes GET /api/version returning the application version.
- The Web UI reads /api/version and displays it in the Observability section, ensuring the UI shows the same version as the running image.
- The version is injected at build time via Go ldflags and Docker ARG VERSION. Release builds pass the tag (e.g., v0.1.3), so /api/version matches the image tag.
See deploy/helm/hermes for full chart. Key values in values.yaml:
- image.repository, image.tag
- http.containerPort, service.port
- persistence.* for SQLite PVC
- db.driver/sqlitePath/dsn
- ingress and openshift.route toggles
- security contexts for pod and container
Notes are printed after install/upgrade via templates/NOTES.txt.
- I can’t log in: Check logs for the default admin credentials on first run; ensure DB is writable when using SQLite.
- Static UI not loading: Confirm STATIC_DIR path and that web/dist exists. In container, it’s baked into /app/web/dist.
- Postgres connection errors: Verify DATABASE_URL (or DB_DSN) and network reachability; set db.driver=postgres in Helm.
- MinIO provider errors: Ensure MinIO is healthy (make wait-minio), correct endpoint/credentials, and useSSL flag.
- Uploading large files on Kubernetes/OKD/OpenShift causes 413/restarts: set NGINX ingress annotations
nginx.ingress.kubernetes.io/proxy-body-size: "0"andnginx.ingress.kubernetes.io/proxy-request-buffering: "off". For OpenShift Route, there’s no per-route body size limit by default, but ensure any third-party ingress allows large bodies. If pods restart during long uploads, relax probe timings in Helm values: increaselivenessProbe.timeoutSecondsandfailureThreshold, and do the same for readiness; optionally enablestartupProbe. Review CPU/memory limits to avoid throttling/OOM under sustained uploads. Optionally cap in-app viaMAX_UPLOAD_SIZE_BYTESor Helmupload.maxBodyBytes.
Issues and PRs are welcome. Please run make test and include context for changes. Keep the container non‑root and secure‑by‑default.
Hermes is released under the Apache License 2.0. See LICENSE for details.