Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ jobs:

docker:
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -190,6 +192,7 @@ jobs:
type=raw,value=latest

- uses: docker/build-push-action@v6
id: build
with:
context: .
platforms: linux/amd64,linux/arm64
Expand All @@ -202,3 +205,77 @@ jobs:
BUILD_TIME=${{ steps.tag.outputs.built }}
cache-from: type=gha
cache-to: type=gha,mode=max

# Arca hosts its own image on an arca instance. The index is copied rather than
# rebuilt, so both registries serve the same digests and pulling by digest from
# either gets identical bytes.
#
# Skipped until MIRROR_IMAGE is set as a repository variable, which is what keeps a
# fork from failing on a registry it has no credentials for.
mirror:
needs: docker
runs-on: ubuntu-latest
if: ${{ vars.MIRROR_IMAGE != '' }}
steps:
- uses: docker/setup-buildx-action@v3

- name: Resolve the tag and the mirror registry
id: mirror
env:
MIRROR_IMAGE: ${{ vars.MIRROR_IMAGE }}
run: |
set -euo pipefail
echo "tag=${{ inputs.tag || github.ref_name }}" >> "$GITHUB_OUTPUT"
# The registry to authenticate against is the host part of the image, so the
# two can never drift apart.
echo "registry=${MIRROR_IMAGE%%/*}" >> "$GITHUB_OUTPUT"

- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/login-action@v3
with:
registry: ${{ steps.mirror.outputs.registry }}
username: ${{ secrets.MIRROR_USERNAME }}
password: ${{ secrets.MIRROR_TOKEN }}

- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ vars.MIRROR_IMAGE }}
tags: |
type=semver,pattern={{version}},value=${{ steps.mirror.outputs.tag }}
type=semver,pattern={{major}}.{{minor}},value=${{ steps.mirror.outputs.tag }}
type=semver,pattern={{major}},value=${{ steps.mirror.outputs.tag }}
type=raw,value=latest

- name: Copy the index across
env:
SOURCE: ${{ env.IMAGE }}@${{ needs.docker.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -euo pipefail

# imagetools takes one source and any number of destination tags, so the
# whole multi-platform index is copied in a single call.
tags=()
while IFS= read -r tag; do
[ -n "$tag" ] && tags+=(--tag "$tag")
done <<< "$TAGS"

echo "mirroring $SOURCE to ${#tags[@]} tags"
docker buildx imagetools create "${tags[@]}" "$SOURCE"

- name: Confirm it is pullable
env:
MIRROR_IMAGE: ${{ vars.MIRROR_IMAGE }}
DIGEST: ${{ needs.docker.outputs.digest }}
run: |
set -euo pipefail

# Inspecting by digest proves the copy landed as the same bytes, which a tag
# lookup on its own would not.
docker buildx imagetools inspect "${MIRROR_IMAGE}@${DIGEST}"
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
</p>

# Arca [![Release](https://github.com/pixelib/arca/actions/workflows/release.yml/badge.svg)](https://github.com/pixelib/arca/actions/workflows/release.yml) [![CI](https://github.com/pixelib/arca/actions/workflows/ci.yml/badge.svg)](https://github.com/pixelib/arca/actions/workflows/ci.yml)
A Maven, npm and Eclipse p2 repository server that runs as a single Go binary.
A Maven, npm, Docker and Eclipse p2 repository server that runs as a single Go binary.

![Arca](docs/assets/img/repositories.jpg)

Artifacts live on disk, metadata lives in SQLite, and the React UI is embedded in the binary. There
is no separate database to run and no config file to write before the first start.

- Maven 2 and npm repositories, hosted or proxying a remote
- Maven 2, npm and Docker repositories, hosted or proxying a remote
- Eclipse p2 update sites, hosted or proxied and cached, with composite groups for PDE and Tycho
- Container images pushed and pulled with any Docker client, with layers, platforms and build history
in the UI
- Release, prerelease and mixed version policies
- Public repositories readable without an account, private ones granted per user
- Generated `maven-metadata.xml` and npm packuments, built from the database
Expand All @@ -38,8 +40,8 @@ Open <http://localhost:8080> and complete the setup wizard. There is a

## Docs

**[pixelib.github.io/arca](https://pixelib.github.io/arca)** covers using it from Maven
and npm, deploying it, and running an installation.
**[pixelib.github.io/arca](https://pixelib.github.io/arca)** covers using it from Maven,
npm and Docker, deploying it, and running an installation.

A live instance is at [repo.pixelib.dev](https://repo.pixelib.dev).

Expand All @@ -65,6 +67,7 @@ make test # go tests plus a frontend typecheck
| `internal/maven` | Maven coordinates, version ordering, metadata rendering |
| `internal/npm` | npm routes, semver, packuments, publish parsing |
| `internal/p2` | p2 paths, OSGi versions, artifact document rewriting |
| `internal/docker` | image names, tags, digests, the storage layout and manifest parsing |
| `internal/proxy` | upstream HTTP client and fetch deduplication |
| `internal/blob` | artifact storage on disk |
| `internal/frontend` | React app plus the `go:embed` handler |
2 changes: 1 addition & 1 deletion docs/_config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
title: Arca
description: A Maven and npm repository server that runs as a single Go binary.
description: A Maven, npm, Docker and Eclipse p2 repository server that runs as a single Go binary.
url: https://pixelib.github.io
baseurl: /arca

Expand Down
2 changes: 1 addition & 1 deletion docs/deploying.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Deploying
layout: default
nav_order: 7
nav_order: 8
---

# Deploying
Expand Down
138 changes: 138 additions & 0 deletions docs/docker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
title: Using it from Docker
layout: default
nav_order: 6
---

# Using it from Docker

Create a repository with a format of `docker`. The format is fixed once the repository exists,
because nothing migrates between formats.

Unlike the other formats there is no `/repository/` URL to point a client at. A Docker client builds
its own URLs from the image reference, so the registry is the bare host and the repository name is
the first segment of the image:

```
docker pull repo.example.com/docker-hosted/team/api:1.4.0
```

That reaches the `team/api` image of the `docker-hosted` repository. One host serves every
repository this way, with no extra port and no hostname of its own.

## Signing in

```
docker login repo.example.com --username you@example.com
```

Use an API token from your account page as the password. Your account password works too, but a
token can be revoked on its own.

A public repository needs no credentials to pull. Pushing always does.

## Pushing

```
docker tag your-image:1.4.0 repo.example.com/docker-hosted/team/api:1.4.0
docker push repo.example.com/docker-hosted/team/api:1.4.0
```

Multi-platform images work as they are: `docker buildx build --push` sends an index and one manifest
per platform, and the repository page shows the platform matrix.

## Pulling

```
docker pull repo.example.com/docker-hosted/team/api:1.4.0
docker pull repo.example.com/docker-hosted/team/api@sha256:6c6e1260a377...
```

A tag can be moved to another image. Pulling by digest is the only reference that always resolves to
the same bytes, which is what to pin in a deployment.

In a Dockerfile or a compose file:

```
FROM repo.example.com/docker-hosted/team/api:1.4.0
```

```yaml
services:
app:
image: repo.example.com/docker-hosted/team/api:1.4.0
```

## Dropping the repository prefix

Set a default docker repository under **Manage, Settings** and a bare reference resolves against it:

```
docker pull repo.example.com/team/api:1.4.0
```

The leading segment still wins when it names a docker repository, so both forms keep working. This
is what makes a single-repository install read the way Docker Hub does.

## Tags and policy

A tag is a version, so a repository's release or prerelease policy applies to it. Docker tags carry
arbitrary suffixes, though: `1.25-alpine` names a variant and `0.1.9-swaggerui-staging` names
whatever its author meant. Only a known marker (`rc`, `alpha`, `beta`, `dev`, `pre`, `snapshot`,
`nightly`, `edge`) counts as a prerelease, and everything else is a release. A **mixed** policy is
the honest default for that reason.

Moving a tag is ordinary Docker practice, so a docker repository allows it by default. Turn off
**Allow tags to be moved** to make every tag here permanent, and a push over an existing one answers
409.

## Proxying another registry

Create a repository with a type of **proxy** and a remote URL:

| Registry | Remote URL |
| --- | --- |
| Docker Hub | `https://registry-1.docker.io` |
| GitHub Container Registry | `https://ghcr.io` |
| Quay | `https://quay.io` |
| Google Artifact Registry | `https://<region>-docker.pkg.dev` |

Pull through it and each image is fetched once, then served from here. Docker Hub's official images
live under an implicit `library/` scope, which is applied for you: `docker pull
repo.example.com/hub/nginx` resolves `library/nginx` upstream.

Registries that require credentials use a token service rather than accepting them directly. Set the
remote username and password and the exchange is handled for you. Credentials are only ever sent to
an HTTPS token service.

A proxy caches a tag for its metadata TTL, because upstream can move it, and caches everything
content-addressed for ever, because a digest names one document for all time. Set a cache retention
in days to evict what nothing has pulled recently.

## Reclaiming storage

Deleting a tag does not free its layers. That is deliberate: layers are shared, so any other tag may
still need them. The artifact page says how much of a tag is unique to it for exactly this reason.

**Manage, Maintenance** has a **Reclaim docker storage** action that removes layers and untagged
manifests no tag can reach any more. Check first and it reports what would go without touching
anything. It also runs on its own every few hours.

Proxy repositories are left alone by it: their content is refetchable and ages out by last access
instead, through the cache retention setting.

## Large layers

The artifact upload limit does not apply to container layers, which are routinely larger than any
sane ceiling for a jar. Set `--max-blob` or `MAX_BLOB_BYTES` to cap them; the default is no limit.

## What is not supported

- **Schema 1 manifests.** Deprecated for years and rejected on push. Anything built this decade
sends schema 2 or OCI.
- **Manifest conversion.** A manifest is served as the type it was pushed with, because re-encoding
it would change the digest it is addressed by. Every current client accepts both encodings.
- **The referrers API.** Signatures and SBOMs attached with `cosign attach` or `docker buildx
--attest` are stored and served, since they are ordinary manifests, but they are not discoverable
through `/v2/<name>/referrers/`. Clients fall back to the tag scheme, which works.
- **Docker groups.** One URL over several repositories is not implemented yet.
22 changes: 14 additions & 8 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,28 @@ nav_order: 1

# Arca

An artifact repository server that runs as a single Go binary. It speaks Maven 2, npm and Eclipse p2,
stores artifacts on disk, keeps metadata in SQLite, and serves an embedded React UI from the same
process. There is no separate database to run, no application server, and no config file to write
An artifact repository server that runs as a single Go binary. It speaks Maven 2, npm, Docker and
Eclipse p2, stores artifacts on disk, keeps metadata in SQLite, and serves an embedded React UI from
the same process. There is no separate database to run, no application server, and no config file to write
before the first start.

![The repository index]({{ site.baseurl }}/assets/img/repositories.jpg)

## What it does

- **Three formats.** Maven 2, npm and Eclipse p2, chosen per repository and fixed once it exists.
- **Four formats.** Maven 2, npm, Docker and Eclipse p2, chosen per repository and fixed once it
exists.
- **Hosted, proxy and group repositories.** Publish to a hosted repository, mirror Maven Central,
registry.npmjs.org and download.eclipse.org through a proxy that caches what it serves, or put a
p2 group over several update sites so a target platform needs one location instead of a dozen.
registry.npmjs.org, Docker Hub and download.eclipse.org through a proxy that caches what it serves,
or put a p2 group over several update sites so a target platform needs one location instead of a
dozen.
- **Version policies.** A repository takes releases only, prereleases only, or both.
- **Per user permissions.** Public repositories are readable without an account. Private ones are
granted per user, per repository.
- **Generated metadata.** `maven-metadata.xml` and npm packuments are built from the database, so
they cannot drift from what is actually stored.
- **Container images as first-class artifacts.** Push and pull with any Docker client, then read the
layers, platforms, build history and how much of a tag is unique to it rather than shared.
- **Insights.** Traffic, storage and per version artifact management, with the numbers coming from
real requests rather than estimates.
- **Your own front page.** A markdown welcome text, an accent colour and a logo of your own, so the
Expand All @@ -37,6 +41,7 @@ before the first start.
| Resolve and publish jars | [Using it from Maven]({{ site.baseurl }}/maven) |
| Resolve and publish packages | [Using it from npm]({{ site.baseurl }}/npm) |
| Resolve Eclipse bundles and features | [Using it from Eclipse]({{ site.baseurl }}/p2) |
| Push and pull container images | [Using it from Docker]({{ site.baseurl }}/docker) |
| Move off Sonatype Nexus | [Migrating from Nexus]({{ site.baseurl }}/migrating) |
| Put it on a server | [Deploying]({{ site.baseurl }}/deploying) |
| Run it day to day | [Managing an installation]({{ site.baseurl }}/managing) |
Expand All @@ -52,11 +57,12 @@ Search spans every repository you can read, in any format, matching names and na

## How it fits together

One process serves three things on the same port:
One process serves four things on the same port:

| Path | Serves |
| --- | --- |
| `/repository/<name>/**` | the Maven or npm endpoint, depending on the repository format |
| `/repository/<name>/**` | the Maven, npm or p2 endpoint, depending on the repository format |
| `/v2/**` | the Docker registry API, which clients build their own URLs for and so cannot be given a prefix |
| `/api/**` | the JSON API the UI runs on |
| everything else | the embedded React bundle |

Expand Down
2 changes: 1 addition & 1 deletion docs/managing.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Managing an installation
layout: default
nav_order: 8
nav_order: 9
---

# Managing an installation
Expand Down
Loading
Loading