Installation
Run OpenArtifact with Docker Compose, on Kubernetes with Helm, or as a single binary against your own PostgreSQL.
OpenArtifact is one Go binary with the web UI embedded. It needs PostgreSQL (the compose file and CI use postgres:18-alpine) and a blob store (local filesystem by default; see storage).
| Method | Use it for |
|---|---|
| Docker Compose | Evaluation, small single-host installs; builds the image from the checkout and bundles PostgreSQL |
| Helm | Kubernetes; optional CloudNativePG database, HA, Ingress/Gateway API |
| Release binary or source build | Bare metal / VMs / systemd, with your own PostgreSQL |
Docker Compose
docker login dhi.io # default runtime is Docker Hardened Images (a free Docker ID is enough)
cp docker/.env.example docker/.env # set POSTGRES_PASSWORD
make docker-up # build and start; stamps version, commit and build timeTo skip the login, build on the anonymous distroless base instead: OPENARTIFACT_RUNTIME=distroless make docker-up.
make docker-up runs docker compose -f docker/docker-compose.yml --env-file docker/.env up -d --build with the build metadata exported (see the table below). Running that docker compose command directly works too, but compose cannot read the VERSION file or run git, so the image then reports version dev and commit/build time unknown (the System page labels them "Not recorded").
The UI and API are on http://localhost:8080 (host port: OPENARTIFACT_PORT). The port is bound to 127.0.0.1 (change with OPENARTIFACT_BIND, e.g. 0.0.0.0; put a TLS reverse proxy in front for anything else). PostgreSQL is not published to the host. Compose refuses to start while POSTGRES_PASSWORD is unset.
Which .env.example:
| File | Used by |
|---|---|
docker/.env.example → docker/.env | Compose (POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB) and make docker-run (DB_*, via --env-file) |
.env.example (repo root) | Full environment-variable reference for the binary; Compose does not read it. See configuration |
Compose also substitutes these variables from docker/.env (they are not listed in docker/.env.example):
| Variable | Default | Effect |
|---|---|---|
AUTH_HMAC_KEY | empty (random key generated once, persisted in the database) | Signs OIDC/SAML login state; optional, a configured value wins over the stored one |
OPENARTIFACT_BIND | 127.0.0.1 | Host address the port mapping binds to |
OPENARTIFACT_PORT | 8080 | Host port mapped to the container's 8080. Containers carry no fixed names, so several projects can run side by side: OPENARTIFACT_PORT=18080 docker compose -p oa-test -f docker/docker-compose.yml --env-file docker/.env up -d --build (add -v to down to drop that project's volumes) |
OPENARTIFACT_VERSION | dev | Build argument; the version reported by /health, the log, -version and the System page. make docker-up sets it from the VERSION file |
OPENARTIFACT_COMMIT | unknown | Build argument; commit reported by the System page. make docker-up sets the 7-character short SHA |
OPENARTIFACT_BUILD_TIME | unknown | Build argument; build time, RFC 3339 in UTC. make docker-up sets it |
OPENARTIFACT_RUNTIME | dhi | Build argument RUNTIME: runtime base image. dhi is Docker Hardened Images (dhi.io/static) and needs docker login dhi.io with a Docker ID; set OPENARTIFACT_RUNTIME=distroless to use the anonymous gcr.io/distroless/static-debian13 instead (same layout, also shell-less) |
OPENARTIFACT_SCANNERS | none | Build argument SCANNERS: vulnerability scanners to bundle into the image (none, trivy, grype, all), copied from digest-pinned upstream images. The default image contains none, so trivy / grype scans fail until you enable them; see Installing Trivy and Grype. Without rebuilding, OPENARTIFACT_TRIVY_INSTALL=auto / OPENARTIFACT_GRYPE_INSTALL=auto make the server download the pinned, checksum-verified release into the scanner_cache volume at runtime (OPENARTIFACT_SCANNER_DOWNLOAD_BASE_URL selects a mirror). Related passthroughs: OPENARTIFACT_SCANNER_OFFLINE, OPENARTIFACT_SCANNER_UPDATE_INTERVAL, OPENARTIFACT_TRIVY_DB_REPOSITORY, OPENARTIFACT_GRYPE_DB_URL. The scanner_cache volume holds the downloaded databases (several GB with both scanners) |
The openartifact service passes only the variables listed in its environment: block. Anything else (BASE_URL, OPENARTIFACT_SECRET_KEY, METRICS_AUTH_TOKEN, OPENARTIFACT_TRUSTED_PROXIES, ...) must be added there, or via a compose override file, before it reaches the server. The service runs as uid 10001 with a read-only root filesystem; data lives in the named volumes postgres_data, artifacts_data (/var/lib/artifacts), backups_data (/var/lib/openartifact/backups) and scanner_cache (/var/cache/openartifact/scanners: Trivy / Grype binaries installed at runtime and their vulnerability databases).
The image has no shell, wget or nc; its health check is openartifact-server -healthcheck, and the server waits for PostgreSQL itself (DB_CONNECT_TIMEOUT, default 60s). See the 0.5.0 upgrade notes for debugging and custom CAs.
Released images are published to ghcr.io/openartifact/openartifact (linux/amd64, linux/arm64; tags in upgrading). To run one instead of building, replace the build: block of the openartifact service with image: ghcr.io/openartifact/openartifact:<version>. Verify signatures as described in signing.
First login
On first start with an empty user table, OpenArtifact creates the user admin. There are two ways to choose its credentials; without either, it generates a password.
Supplied credentials (recommended for automation). Provide the password and, optionally, an admin API token before the first start. Nothing is printed and no password change is forced (set OPENARTIFACT_BOOTSTRAP_FORCE_PASSWORD_CHANGE=true to force one):
| Environment variable | YAML key | Meaning |
|---|---|---|
OPENARTIFACT_BOOTSTRAP_ADMIN_PASSWORD_FILE | bootstrap.adminPasswordFile | File holding the admin password (a trailing newline is trimmed). Preferred: a secret mounted as a file never shows up in docker inspect, process environments or compose files |
OPENARTIFACT_BOOTSTRAP_ADMIN_PASSWORD | bootstrap.adminPassword | The password itself, a convenience for compose and local trials. The file wins when both are set |
OPENARTIFACT_BOOTSTRAP_ADMIN_TOKEN_FILE | bootstrap.adminTokenFile | File holding an admin API token secret; created as the access token bootstrap of admin, so oa apply works on the first sync |
OPENARTIFACT_BOOTSTRAP_ADMIN_TOKEN_TTL | bootstrap.adminTokenTtl | Lifetime of that token (for example 720h); 0 (default) means no expiry |
OPENARTIFACT_BOOTSTRAP_FORCE_PASSWORD_CHANGE | bootstrap.forcePasswordChange | Force a change at the first login of a supplied password (default false; a generated password always forces it) |
umask 077
openssl rand -base64 24 > admin-password
echo "oa_pat_$(openssl rand -hex 32)" > admin-token- The password must satisfy the normal policy (12 to 1024 characters). An invalid password, an unreadable file or an invalid token makes the server refuse to start with an error naming the setting; the value itself is never logged.
- The token secret must be
oa_pat_followed by 32 to 128 characters fromA-Za-z0-9_-with at least 12 distinct characters (the shape of a token the server generates;oa_pat_$(openssl rand -hex 32)always qualifies). Only its SHA-256 is stored, like every other token. It carries the globalmanagepermission of the admin user and can be revoked in the UI. - The settings apply only to an empty instance. They never reset an existing admin or add a token later; on a non-empty database the server logs an info line and ignores them (they are not even read). With several replicas starting at once, the admin, its password and its token are created in one transaction, so exactly one admin and one token exist. Use
resetpwto change an existing password.
Generated password. Without these settings the server generates a random 32-character password and prints it once to stderr (not through the structured logger, so it is not in a LOG_OUTPUT file):
===== OPENARTIFACT BOOTSTRAP =====
admin password: <random>
valid for 24h0m0s (after that, reset it with cmd/resetpw)
Change immediately at /api/v1/auth/change-password
To choose the password instead, set OPENARTIFACT_BOOTSTRAP_ADMIN_PASSWORD_FILE
(bootstrap.adminPasswordFile) before the first start.
===================================Read it from the container output:
docker compose -f docker/docker-compose.yml logs openartifact | grep -A6 'OPENARTIFACT BOOTSTRAP'A generated password stops working for login after OPENARTIFACT_BOOTSTRAP_GENERATED_PASSWORD_TTL (bootstrap.generatedPasswordTtl, default 24h, 0 = never), so a banner that ended up in a log aggregator does not stay a credential. Changing the password, or running resetpw, removes the expiry. An expired password is rejected exactly like a wrong one (401 invalid_credentials); the server log then says login rejected: the generated bootstrap password has expired; reset it with cmd/resetpw, and only after the correct password was presented. Run resetpw to get back in.
The generated account is flagged must_change_password: until the password is changed, every API call except POST /api/v1/auth/change-password and POST /api/v1/auth/logout is rejected, and the UI redirects to the change-password page. New passwords must be at least 12 characters. With several replicas starting on an empty database, only the replica that creates the user prints a password.
Next: create a repository, then an API token for clients (authentication, RBAC).
Reset the admin password
resetpw is a break-glass tool that writes directly to the database. It is built from ./cmd/resetpw and shipped next to openartifact in the release archives. It never takes the password as an argument.
| Input | Meaning |
|---|---|
OPENARTIFACT_RESET_PASSWORD | New password. If unset, one line is read from stdin; an interactive terminal is refused (input would echo) |
-username NAME / OPENARTIFACT_RESET_USERNAME | User to reset (default admin) |
-sslmode MODE / DB_SSLMODE | disable, allow, prefer, require, verify-ca or verify-full (default require, like the server) |
DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME | Connection settings; defaults localhost, 5432, postgres, empty, artifact_repo. Read from the environment only, not from the YAML file |
OPENARTIFACT_RESET_PASSWORD='<new-password>' go run ./cmd/resetpw
# or: printf '%s\n' '<new-password>' | go run ./cmd/resetpw -username admin
# or, from a release archive: ./resetpw -username admin < password-file- The password must be 12 to 1024 characters; the tool fails if the user does not exist.
- In one transaction it sets the new Argon2id hash, clears
must_change_passwordand any generated-password expiry (you chose the password, so no forced change follows) and deletes all of the user's sessions. - A running server may take up to
DB_AUTH_CACHE_TTL_MS(default 2000 ms) to see the change, becauseresetpwbypasses its auth cache. - With the default compose file PostgreSQL is only on the compose network. To reach it from the host, add
ports: ["127.0.0.1:5432:5432"]to thepostgresservice, then useDB_HOST=127.0.0.1 DB_USER=… DB_PASSWORD=… DB_NAME=… DB_SSLMODE=disable(the bundled database has no TLS).
Helm
The chart lives in its own repository, openartifact/helm-charts, and is published both as an OCI artifact and as a classic Helm repository:
# OCI
helm install openartifact oci://ghcr.io/openartifact/charts/openartifact \
--namespace openartifact --create-namespace -f my-values.yaml
# Classic repository
helm repo add openartifact https://openartifact.github.io/helm-charts
helm repo update
helm install openartifact openartifact/openartifact \
--namespace openartifact --create-namespace -f my-values.yamlKey points from the chart README (the option reference is values.yaml):
- Needs Kubernetes 1.29+ and PostgreSQL: either
database.external.*orcnpg.enabled=true(CloudNativePG operator). - Blob storage:
filesystemon a PVC or an object store (s3,garage,seaweedfs,gcs,azure,oci). More than one replica needs object storage or aReadWriteManyvolume. - The chart-managed Secret holds
AUTH_HMAC_KEYandOPENARTIFACT_SECRET_KEY, generated on first install and reused on upgrade. Back it up; see upgrading. - Set
baseUrlto the public URL andconfig.trustedProxiesto your ingress controller's CIDR. - Any server variable can be set through
envorsecrets.extra. - First-login password: find the
OPENARTIFACT BOOTSTRAPblock in the pod logs (kubectl -n openartifact logs <pod>), as above.
Binary and build from source
Release archives
Each GitHub release (tag vX.Y.Z) carries archives built by GoReleaser, plus SHA256SUMS:
| Archive | Contents | Platforms |
|---|---|---|
openartifact_<version>_<os>_<arch>.tar.gz | openartifact (server), resetpw, LICENSE, README.md, openartifact.example.yaml, .env.example | linux amd64, arm64 |
oa_<version>_<os>_<arch>.tar.gz (.zip on Windows) | oa CLI, LICENSE | linux, darwin, windows × amd64, arm64 |
Archives and checksums carry a build-provenance attestation; verification commands are in signing. The CLI is covered in CLI.
sha256sum -c SHA256SUMS --ignore-missing
tar -xzf openartifact_<version>_linux_amd64.tar.gz
cp openartifact.example.yaml openartifact.yaml # edit database.*, baseUrl, storage.*
./openartifact verify openartifact.yaml
./openartifact -config openartifact.yamlThe example file ships database.password: CHANGE_ME, sslMode: require and a placeholder corsOrigins entry; adjust all three (the server refuses to start while the database password is still CHANGE_ME). The defaults storage.path: /var/lib/artifacts and backup.backupDir: /var/lib/openartifact/backups must exist or be creatable by the process user (the filesystem store creates its directory). Migrations run at startup; the first start creates the admin user (see First login). Options and environment variables: configuration.
Build from source
Requires Go 1.27 (go.mod: go 1.27.0; CI and releases use 1.27.3) and bun 1.4.4 (the web UI is embedded into the binary).
make build # web UI + bin/openartifact
make build-cli # bin/oa
make run # build, then run ./bin/openartifact
make help # list all targetsThe version comes from the VERSION file and is injected with -ldflags "-X main.version=… -X main.commit=… -X main.buildTime=…". Override with make build CI_VERSION=1.2.3 CI_COMMIT=abc1234. A plain go build ./cmd/server reports version dev. The commit is always the 7-character short SHA and the build time RFC 3339 in UTC, in the Makefile, the Dockerfile and GoReleaser alike. openartifact-server -version prints <version> <commit> <build time> <go>/<os>/<arch>. Useful targets: make migrate (runs go run ./cmd/server -migrate), make docker-build (image openartifact:latest), make docker-run (that image against an external PostgreSQL from docker/.env). Development targets are in development.
Behind a reverse proxy and TLS
- Set
BASE_URLto the public URL (defaulthttp://localhost:8080). It is used for download and index URLs generated by the format handlers and for SSO callbacks (the SAML entity ID and ACS URL are derived from it). OPENARTIFACT_TRUSTED_PROXIES(comma-separated CIDRs): only requests whose TCP peer is in this list may setX-Forwarded-For/X-Real-IP. Without it the peer address is always the client IP, so IP allowlist, rate limiting and login throttling see the proxy's address.- Session and CSRF cookies get
Securewhen the request arrived over TLS or carriesX-Forwarded-Proto: https, so have the proxy set that header. - To terminate TLS in OpenArtifact:
SERVER_TLS_ENABLED=truewithSERVER_TLS_CERTandSERVER_TLS_KEY(PEM files; both required, minimum TLS 1.2). Only in this mode does the server sendStrict-Transport-Security; behind a TLS-terminating proxy, set HSTS on the proxy. - Uploads can be large, and read/write timeouts default to
0(unlimited). Raise or disable any body-size and timeout limit on the proxy as well. GET /health(always 200, JSON with version and component status) andGET /ready(503 when the database is down) are the probe endpoints; the compose healthcheck uses/health.
Install the web UI as an app
The web UI is an installable app (PWA): it opens in its own window without browser chrome and gets an icon on the desktop, launcher or home screen. Installing needs a secure origin: serve OpenArtifact over HTTPS (see above) or open it on localhost. Over plain http:// on any other host the browser offers no install and does not run the service worker; the UI itself keeps working in a tab.
| Platform | How |
|---|---|
| Desktop Chrome / Edge | Click the install icon at the right end of the address bar, or Install app in the user menu (shown only when the browser offers an install). |
| Android (Chrome) | Menu → Install app (or Add to Home screen), or the Install app item in the user menu. |
| iOS / iPadOS (Safari) | Share → Add to Home Screen. Safari fires no install event, so there is no in-app button. |
What the installed app does and does not do:
- It is the same UI, served by the same server, and signs in like any browser tab: no separate login mode, and sessions, tokens and permissions are unchanged.
- The service worker (
/sw.js) caches only the build assets and the static app shell (offline page, icons, manifest). API, repository (/repos/), registry (/v2/),/metrics,/health, login and SSO responses are never cached and never touched by the worker, so no data is served from a previous session. - Without a network connection, opening a page shows a small "You are offline" page with a Try again link instead of the browser error page.
- After an upgrade the UI shows a New version available notice with a Reload button. The page is never reloaded on its own, so a form in progress is not lost; the new version is used after you click it (or reopen the app).
- A reverse proxy must pass
/sw.jsand/manifest.webmanifeston with theirCache-Control: no-cacheheader, or an upgrade is noticed late. To remove the app, uninstall it from the browser or OS like any other app.