Skip to content

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).

MethodUse it for
Docker ComposeEvaluation, small single-host installs; builds the image from the checkout and bundles PostgreSQL
HelmKubernetes; optional CloudNativePG database, HA, Ingress/Gateway API
Release binary or source buildBare metal / VMs / systemd, with your own PostgreSQL

Docker Compose ​

sh
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 time

To 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:

FileUsed by
docker/.env.example → docker/.envCompose (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):

VariableDefaultEffect
AUTH_HMAC_KEYempty (random key generated once, persisted in the database)Signs OIDC/SAML login state; optional, a configured value wins over the stored one
OPENARTIFACT_BIND127.0.0.1Host address the port mapping binds to
OPENARTIFACT_PORT8080Host 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_VERSIONdevBuild argument; the version reported by /health, the log, -version and the System page. make docker-up sets it from the VERSION file
OPENARTIFACT_COMMITunknownBuild argument; commit reported by the System page. make docker-up sets the 7-character short SHA
OPENARTIFACT_BUILD_TIMEunknownBuild argument; build time, RFC 3339 in UTC. make docker-up sets it
OPENARTIFACT_RUNTIMEdhiBuild 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_SCANNERSnoneBuild 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 variableYAML keyMeaning
OPENARTIFACT_BOOTSTRAP_ADMIN_PASSWORD_FILEbootstrap.adminPasswordFileFile 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_PASSWORDbootstrap.adminPasswordThe password itself, a convenience for compose and local trials. The file wins when both are set
OPENARTIFACT_BOOTSTRAP_ADMIN_TOKEN_FILEbootstrap.adminTokenFileFile 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_TTLbootstrap.adminTokenTtlLifetime of that token (for example 720h); 0 (default) means no expiry
OPENARTIFACT_BOOTSTRAP_FORCE_PASSWORD_CHANGEbootstrap.forcePasswordChangeForce a change at the first login of a supplied password (default false; a generated password always forces it)
sh
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 from A-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 global manage permission 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 resetpw to 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):

text
===== 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:

sh
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.

InputMeaning
OPENARTIFACT_RESET_PASSWORDNew password. If unset, one line is read from stdin; an interactive terminal is refused (input would echo)
-username NAME / OPENARTIFACT_RESET_USERNAMEUser to reset (default admin)
-sslmode MODE / DB_SSLMODEdisable, allow, prefer, require, verify-ca or verify-full (default require, like the server)
DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAMEConnection settings; defaults localhost, 5432, postgres, empty, artifact_repo. Read from the environment only, not from the YAML file
sh
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_password and 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, because resetpw bypasses 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 the postgres service, then use DB_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:

sh
# 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.yaml

Key points from the chart README (the option reference is values.yaml):

  • Needs Kubernetes 1.29+ and PostgreSQL: either database.external.* or cnpg.enabled=true (CloudNativePG operator).
  • Blob storage: filesystem on a PVC or an object store (s3, garage, seaweedfs, gcs, azure, oci). More than one replica needs object storage or a ReadWriteMany volume.
  • The chart-managed Secret holds AUTH_HMAC_KEY and OPENARTIFACT_SECRET_KEY, generated on first install and reused on upgrade. Back it up; see upgrading.
  • Set baseUrl to the public URL and config.trustedProxies to your ingress controller's CIDR.
  • Any server variable can be set through env or secrets.extra.
  • First-login password: find the OPENARTIFACT BOOTSTRAP block 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:

ArchiveContentsPlatforms
openartifact_<version>_<os>_<arch>.tar.gzopenartifact (server), resetpw, LICENSE, README.md, openartifact.example.yaml, .env.examplelinux amd64, arm64
oa_<version>_<os>_<arch>.tar.gz (.zip on Windows)oa CLI, LICENSElinux, darwin, windows × amd64, arm64

Archives and checksums carry a build-provenance attestation; verification commands are in signing. The CLI is covered in CLI.

sh
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.yaml

The 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).

sh
make build        # web UI + bin/openartifact
make build-cli    # bin/oa
make run          # build, then run ./bin/openartifact
make help         # list all targets

The 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_URL to the public URL (default http://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 set X-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 Secure when the request arrived over TLS or carries X-Forwarded-Proto: https, so have the proxy set that header.
  • To terminate TLS in OpenArtifact: SERVER_TLS_ENABLED=true with SERVER_TLS_CERT and SERVER_TLS_KEY (PEM files; both required, minimum TLS 1.2). Only in this mode does the server send Strict-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) and GET /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.

PlatformHow
Desktop Chrome / EdgeClick 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.js and /manifest.webmanifest on with their Cache-Control: no-cache header, or an upgrade is noticed late. To remove the app, uninstall it from the browser or OS like any other app.

Free software under the AGPL-3.0-or-later.