SelfHost HubSelfHost Hub
中文
← Back to all services

GitHub repository information

Fetched · August 14, 2026
★ 168Latest: v4.0.0Updated: August 13, 2026
README
<p align="center"><img src="docs/images/logo.png" alt="portkey logo"></p>

<h1 align="center">portkey</h1>
<div align="center">
  ⚡🚀🔗
</div>
<div align="center">
  <strong>Repository of the portkey application</strong>
</div>
<div align="center">
  A simple web portal that can act as startup page and shows a collection of links/urls. It also supports adding small custom pages.
</div>

<div align="center">
  <!-- Sonar quality gate -->
  <a href="https://sonarcloud.io/project/overview?id=kodehat_portkey">
    <img src="https://sonarcloud.io/api/project_badges/measure?project=kodehat_portkey&metric=alert_status"
      alt="Sonar quality gate" />
  </a>
  <!-- GitHub stars -->
  <a href="https://github.com/kodehat/portkey">
    <img src="https://img.shields.io/github/stars/kodehat/portkey"
      alt="GitHub stars" />
  </a>
  <!-- Go version -->
  <img src="https://img.shields.io/github/go-mod/go-version/kodehat/portkey"
      alt="Go version" />
  <!-- Docker image size -->
  <a href="https://hub.docker.com/r/codehat/portkey">
    <img src="https://img.shields.io/docker/image-size/codehat/portkey"
      alt="Docker image size" />
  </a>
</div>

<div align="center">
  <h3>
    <a href="https://www.portkey.page">
      Website (GitHub)
    </a>
    <span> | </span>
    <a href="https://github.com/kodehat/portkey/blob/main/.github/CONTRIBUTING.md">
      Contributing
    </a>
  </h3>
</div>

<div align="center">
  <sub>As magical as in Harry Potter. Built with ☕️ by
  <a href="https://www.codehat.de">CodeHat</a> and
  <a href="https://github.com/kodehat/portkey/graphs/contributors">
    contributors
  </a></sub>
</div>

## Table of Contents

- [Features](#features)
- [Screenshots](#screenshots)
- [Installation](#installation)
- [Usage](#usage)
- [Configuration](#configuration)
- [Metrics](#metrics)
- [Docker](#docker)
- [Development](#development)
- [See Also](#see-also)
- [License](#license)

## Features

- 🔗 Shows a collection of links acting as startup page or similar.
- 🔎 Includes a search box with configurable keyword support and fuzzy matching.
- 📂 Portals can be organised into named groups/sections on the home page.
- 📐 Optional multi-column grid layout for portals and groups (configurable column count, mobile-responsive).
- 🌎 Automatically fetches and caches favicons for external portals (direct or proxied).
- 🎨 Portal icons support emojis, image files, or URLs.
- 📄 Can be configured easily by modifying only one file.
- 🗒️ Also supports adding smaller custom pages.
- 🌓 Dark and light mode available.
- 🪰 Very lightweight application with Docker images available.

## Screenshots

<p>You can also find a demo <a href="https://demo.portkey.page">here</a></p>

<details>

  <summary>Click to view screenshots</summary>

  #### Link collection (no groups)

  ![LinkCollectionNoGroups](docs/images/screenshot_no_groups_full.png "Link collection (no groups)")
  
#### Link collection (with groups)

  ![LinkCollectionWithGroups](docs/images/screenshot_with_groups_full.png "Link collection (with groups)")

  #### Search for a link

  ![SearchForLink](docs/images/screenshot_search.png "Search for a link")

  #### Custom page
  
  ![CustomPage](docs/images/screenshot_custom_page.png "Custom page")

</details>

## Installation

Download the `portkey` file for your OS. Probably to a location that is in your `PATH`, so you can use it right away. 

## Usage

1. Create a `config.yml` or use the [example configuration](https://github.com/kodehat/portkey/blob/main/config.yml) from this repository and configure it as you want.
> You can find a detailed explanation of all configuration options [here](#configuration).
2. Start the application with `portkey --config-path=<dir_to_config_yml>`. Providing the path to the configuration file is optional if it's in the working directory.
3. Open your browser at the defined host and port. Default is <http://localhost:3000>

## Configuration

`portkey` is configured with a single configuration file called `config.yml`. You can pass its location using the `--config-path` argument.

You can also overwrite configuration values from file by using environment variables in uppercase and prefixed with `PORTKEY_`. Nested keys use underscores: the configuration key `server.host` can be passed as environment variable `PORTKEY_SERVER_HOST`, `favicon.mode` as `PORTKEY_FAVICON_MODE`.

The `config.yml` contains the following configuration options:

### Server

```yaml
server:
  # Can be changed to reduce or increase logs. Values could be "ERROR", "WARN", "INFO" or "DEBUG". Default: "INFO".
  logLevel: INFO
  # If enabled logs are in JSON format. Default: false.
  logJson: false
  # Set the host where the application should bind to. Default: "localhost".
  host: localhost
  # Set the port where the application should bind to. Default: "3000".
  port: 3000
  # Set the context path (aka base-url) portkey is hosted under. Must not be specified unless you're using a reverse proxy and are hosting portkey under a directory. If that's the case then you can set this value to e.g. /portkey or whatever the directory is called. Note that the forward slash (/) in the beginning is required! Default: "" (no context path).
  contextPath: ""
  # Enables development-mode additions (browser-reload WebSocket endpoint at /reload and dev-mode markup). Default: false.
  devMode: false
```

### Metrics

```yaml
metrics:
  # Enables the HTTP server that serves metrics that can be scraped by e.g. Prometheus. Default: false.
  enabled: false
  # Set the host where the metrics server should bind to. Default: "localhost".
  host: localhost
  # Set the port where the metrics server should bind to. Default: "3030".
  port: 3030
```

### UI

```yaml
ui:
  # Title of the application shown in the browser tab and in the top bar.
  # If empty, the top-bar title is hidden and the browser tab only shows the page name. Default: "portkey".
  title: "portkey"
  # Whether the search bar is shown. Can be useful with a low amount of portals making the search unnecessary. Default: true.
  showSearchBar: true
  # Defines whether portkey's application icon should be shown at the top left of the front page. Default: true.
  showTopIcon: true
  # If true keywords of portals are shown as tooltip on hover. Default: false.
  showKeywordsAsTooltips: false
  # If true all links are sorted alphabetically when shown on the front page. Otherwise they are shown in the order they are defined. Default: false.
  sortAlphabetically: false
  # Number of columns for the grid layout (0 = disabled/vertical).
  # On mobile (<768px) the layout always falls back to vertical stacking.
  # When groups exist, each group occupies one grid cell.
  # When no groups, portals are distributed across N columns. Default: 0 (vertical).
  layoutColumns: 0
  # Allows adding additional scripts/stylesheets etc. to the HTML header. Can be useful for analytics or smaller style modifications. Default: "" (nothing added).
  headerAddition: |-
    <script async src="https://analytics.example.com"></script>
  # Footer (HTML support) that is shown on every page.
  # Remember that Tailwind CSS classes used here do only work if already used somewhere else in the application because the bundler couldn't look here! Default: "Works like a portal.".
  footer: |-
    <p>This is a footer!</p>
```

### Search

```yaml
search:
  # If true search query is also compared to portals and keywords using Levenshtein string metric. Default: false.
  stringSimilarity: false
  # Minimum required similarity for results when 'stringSimilarity' is 'true'. Must be between '0.0' (0%) and '1.0' (100%). Default: 0.75.
  minimumSimilarity: 0.5
```

### Favicons

```yaml
favicon:
  # How favicons are loaded. "direct": portkey discovers icons itself via the
  # favifetch library (parses the target site's HTML <link> tags, web app manifests and
  # common fallback paths, with the Vemetric favicon API as last-resort fallback).
  # "proxied": every request is relayed to a Vemetric-compatible favicon service
  # (serviceUrl), which performs all discovery; the result is cached like a
  # direct fetch. Empty or unrecognized values fall back to "direct". Default: "direct".
  mode: direct
  # Favicon service host or full https URL (e.g. https://favicon.vemetric.com) used as
  # last-resort fallback in "direct" mode and as relay target in "proxied" mode.
  # Empty uses the built-in default (favicon.vemetric.com). A full URL is reduced to
  # its host (both favifetch and proxied mode always use HTTPS). Default: "" (built-in default).
  serviceUrl: ""
  # On-disk favicon cache directory. Icons are stored by normalized hostname, TTL is
  # 7 days; stale entries are refreshed in the background, failed fetches are backed
  # off for 1 hour. Created at startup, so it must be writable by the process.
  # Mountable as a Docker volume for persistence across restarts. Default: "" —
  # required (non-empty) when cacheEnabled is true.
  cacheDir: ./favicon-cache
  # Whether fetched favicons are stored on disk under cacheDir. Disabled by default;
  # if enabled, cacheDir must be set (startup fails otherwise). Default: false.
  cacheEnabled: true
  # Directory for custom icon files (SVG, PNG). Files are served at /_/icons/<filename>.
  # The path is resolved on the machine the process runs on. In Docker it must be a path
  # valid INSIDE the container: use the container-side destination of the volume mount,
  # e.g. mount with `-v $(PWD)/icons:/opt/icons` and set `/opt/icons` here. Relative paths
  # resolve against the working directory (/opt in the official image). Requires creating
  # the directory and placing icon files. Default: "" (custom icons disabled).
  customIconsDir: ./icons
```

> **Note:** For external portals without an explicit `icon`, Portkey makes outbound
> HTTP requests to fetch their favicons. Favicon caching is **disabled by default**;
> to enable it, set `favicon.cacheEnabled: true` and a writable `favicon.cacheDir`
> (created at startup — in Docker, mount a writable volume there, see the Docker
> section). With the cache disabled, icons are still fetched, just never stored.

### Portals (Links)

```yaml
# Defines a list of portals (links) that have additional attributes defining their appearance.
portals:
  # Name of the link
- title: example
  # (Optional) Icon shown in front of the title. Supports multiple formats:
  #   - Emoji:           icon: "🔗"
  #   - Custom SVG/PNG:  icon: /_/icons/github.svg  (place file in customIconsDir)
  #   - Absolute URL:    icon: https://example.com/icon.png
  #   - Data URI:        icon: data:image/svg+xml,%3Csvg...
  #   If empty, the global favicon is used for external links (cached automatically)
  #   and a file icon is shown for internal pages.
  # Link where the portal will lead to (can be relative for custom pages or absolute otherwise)
  link: https://example.com/
  # Additional keywords used by the search feature.
  keywords:
  - url
  - example
  # (Optional) Group name for organising portals into sections on the home page.
  # Portals sharing the same group value are rendered together under a labelled heading.
  # Portals without a group are shown ungrouped at the bottom. Groups appear in the
  # order the first portal of each group is defined.
  group: My Group
```

> **Tip:** When a search query is active, portals are still grouped by their group field. Groups with no matching portals are hidden.

### Custom pages

```yaml
# Defines a list of custom pages that are made available at the defined paths.
# Important: These are not automatically added to the list of portals and have to be added manually!
pages:
  # Heading for the custom page. Shown in browser tab and as heading on the page.
- heading: Custom
  # Optional subtitle for the page (accepted but not displayed by the 4.0.0 layout).
  subtitle: An optional subtitle
  # Path where the custom page will be available.
  path: /custom
  # Content of the custom page and it supports using HTML.
  # The same CSS rules apply as for the footer!
  content: |-
    This is a <em>custom page</em><br>
    It also supports using <strong>HTML</strong>!
```

## Metrics

Metrics can be enabled with the `metrics.enabled` configuration key and are served on a dedicated HTTP server. By default they are served on `http://localhost:3030/metrics`. Use this address to configure your tool of choice (e.g. [Prometheus](https://prometheus.io/)) to scrape the exported metrics.

Besides the default metrics provided by the [Prometheus instrumentation library for Go applications ](https://github.com/prometheus/client_golang), the following additional metrics are provided:

```plain
# HELP portkey_portal_handler_requests_total Total number of HTTP requests by portal.
# TYPE portkey_portal_handler_requests_total counter
portkey_portal_handler_requests_total{portal="<portal_title>"} 0

# HELP portkey_page_handler_requests_total Total number of HTTP requests by page.
# TYPE portkey_page_handler_requests_total counter
portkey_page_handler_requests_total{path="<page_path>"} 0

# HELP portkey_search_requests_with_results_total Total number of HTTP requests for search with at least one result.
# TYPE portkey_search_requests_with_results_total counter
portkey_search_requests_with_results_total 0

# HELP portkey_search_requests_no_results_total Total number of HTTP requests for search with no results.
# TYPE portkey_search_requests_no_results_total counter
portkey_search_requests_no_results_total 0

# HELP portkey_search_duration_seconds Search query duration in seconds.
# TYPE portkey_search_duration_seconds histogram
portkey_search_duration_seconds_bucket{le="0.001"} 0
portkey_search_duration_seconds_bucket{le="0.005"} 0
portkey_search_duration_seconds_bucket{le="0.01"} 0
portkey_search_duration_seconds_bucket{le="0.025"} 0
portkey_search_duration_seconds_bucket{le="0.05"} 0
portkey_search_duration_seconds_bucket{le="0.1"} 0
portkey_search_duration_seconds_bucket{le="+Inf"} 0
portkey_search_duration_seconds_sum 0
portkey_search_duration_seconds_count 0

# HELP portkey_http_request_duration_seconds HTTP request duration by handler pattern.
# TYPE portkey_http_request_duration_seconds histogram
portkey_http_request_duration_seconds_bucket{handler="/",le="0.005"} 0
portkey_http_request_duration_seconds_bucket{handler="/",le="0.01"} 0
portkey_http_request_duration_seconds_bucket{handler="/",le="0.025"} 0
portkey_http_request_duration_seconds_bucket{handler="/",le="0.05"} 0
portkey_http_request_duration_seconds_bucket{handler="/",le="0.1"} 0
portkey_http_request_duration_seconds_bucket{handler="/",le="0.25"} 0
portkey_http_request_duration_seconds_bucket{handler="/",le="0.5"} 0
portkey_http_request_duration_seconds_bucket{handler="/",le="1"} 0
portkey_http_request_duration_seconds_bucket{handler="/",le="2.5"} 0
portkey_http_request_duration_seconds_bucket{handler="/",le="5"} 0
portkey_http_request_duration_seconds_bucket{handler="/",le="10"} 0
portkey_http_request_duration_seconds_bucket{handler="/",le="+Inf"} 0
portkey_http_request_duration_seconds_sum{handler="/"} 0
portkey_http_request_duration_seconds_count{handler="/"} 0

# HELP portkey_favicon_cache_hits_total Total number of favicon cache hits.
# TYPE portkey_favicon_cache_hits_total counter
portkey_favicon_cache_hits_total 0

# HELP portkey_favicon_cache_misses_total Total number of favicon cache misses.
# TYPE portkey_favicon_cache_misses_total counter
portkey_favicon_cache_misses_total 0

# HELP portkey_favicon_fetch_failures_total Total number of failed favicon fetches.
# TYPE portkey_favicon_fetch_failures_total counter
portkey_favicon_fetch_failures_total 0

# HELP portkey_favicon_cache_size Current number of favicons in the on-disk cache.
# TYPE portkey_favicon_cache_size gauge
portkey_favicon_cache_size 0

# HELP portkey_portals_total Total number of configured portals.
# TYPE portkey_portals_total gauge
portkey_portals_total 0

# HELP portkey_groups_total Total number of portal groups.
# TYPE portkey_groups_total gauge
portkey_groups_total 0

# HELP portkey_version_info Version information about portkey.
# TYPE portkey_version_info gauge
portkey_version_info{buildTime="2024.10.09_17:29:19",commitHash="4fd1a0f",goVersion="1.23.1",version="dev"} 1
```

## Docker

There are also Docker images available at Docker hub that you can use. You can start a container with the following command:

```sh
# Assumes that there is a config.yml in the current directory.
# It is probably better to use a specific version than 'latest'.
docker run --rm -it \
  -v $(PWD)/config.yml:/opt/config.yml \
  -v $(PWD)/favicon-cache:/opt/favicon-cache \
  -v $(PWD)/icons:/opt/icons \
  -e PORTKEY_FAVICON_CACHEENABLED=true \
  -e PORTKEY_FAVICON_CACHE_DIR=/opt/favicon-cache \
  -e PORTKEY_FAVICON_CUSTOMICONSDIR=/opt/icons \
  -p 3000:3000 \
  codehat/portkey:latest
```

> **Paths in Docker:** the container runs with the working directory `/opt`, so relative
> paths in `config.yml` (e.g. `favicon.cacheDir: ./favicon-cache`) resolve to `/opt/...`.
> For volume-mounted directories such as `favicon.customIconsDir`, always configure the
> **container-side destination** of the mount, not a host path: `-v $(PWD)/icons:/opt/icons`
> requires `favicon.customIconsDir: /opt/icons` (or `PORTKEY_FAVICON_CUSTOMICONSDIR=/opt/icons`). A volume
> mount does not rewrite config paths — the path in the config must equal the mount target
> inside the container.

## Development

### Application Code

**portkey** is a *Go* application. You can install its dependencies with `go mod download`.

### Frontend

The frontend dependencies (e.g. TailwindCSS, AlpineJS) can be installed with `npm install --include dev`.

They can be watched with `npm run watch` and built with `npm run build`.

### Templates

A library called [templ](https://templ.guide) is used for the templates. To generate the `.go` files from the templates, it has to be installed. `templ` is installed using go tools and can be invoked with:

```sh
go tool templ
```

Afterwards you can generate the compiled templates with `templ generate`.

### Live Reload

Live reloading is possible by installing [air](https://github.com/cosmtrek/air) and calling `air`.
`air` is installed using go tools and can be invoked with:

```sh
go tool air
```

Set `server.devMode: true` in `config.yml` (or the environment variable `PORTKEY_SERVER_DEVMODE=true`) to enable development-mode additions: a browser-reload WebSocket endpoint at `/reload` and dev-mode markup in the page layout. Dev mode is disabled by default.

## License

[AGPL-3.0](https://www.tldrlegal.com/license/gnu-affero-general-public-license-v3-agpl-3-0)

Discover more