diff --git a/README.md b/README.md
index 76e9c39..d0125c1 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# Headplane
-> An advanced UI for [juanfont/headscale](https://github.com/juanfont/headscale)
+> A feature-complete web UI for [Headscale](https://headscale.net)
-Headscale is a self-hosted version of the Tailscale control server, however, it currently lacks a first-party web UI.
-Headplane aims to solve this issue by providing a GUI that can deeply integrate with the Headscale server.
-It's able to replicate nearly all of the functions of the official Tailscale SaaS UI, including:
+Headscale is the de-facto self-hosted version of Tailscale, a popular Wireguard
+based VPN service. By default, it does not ship with a web UI, which is where
+Headplane comes in. Headplane is a feature-complete web UI for Headscale, allowing
+you to manage your nodes, networks, and ACLs with ease.
-- Machine/Node expiry, network routing, name, and owner management
-- Access Control List (ACL) and tagging configuration
+Headplane aims to replicate the functionality offered by the official Tailscale
+product and dashboard, being one of the most feature complete Headscale UIs available.
+These are some of the features that Headplane offers:
+
+- Machine management, including expiry, network routing, name, and owner management
+- Access Control List (ACL) and tagging configuration for ACL enforcement
- Support for OpenID Connect (OIDC) as a login provider
-- DNS and *safe* Headscale configuration management
+- The ability to edit DNS settings and automatically provision Headscale
+- Configurability for Headscale's settings
## Deployment
-> For more configuration options, refer to the [Configuration](/docs/Configuration.md) guide.
+Headplane runs as a server-based web-application, meaning you'll need a server to run it.
+It's available as a Docker image (recommended) or through a manual installation.
+There are 2 ways to deploy Headplane:
-For fully-featured deployments, see the [Advanced Deployment](/docs/Advanced-Integration.md) guide.
-This includes automatic management of ACLs, DNS settings, and Headscale configuration.
-*This is the closest experience to the Tailscale UI that can be achieved with Headscale and Headplane.*
-*If you aren't sure which one to pick, we recommend this.*
+- ### [Integrated Mode (Recommended)](/docs/Integrated-Mode.md)
+ Integrated mode unlocks all the features of Headplane and is the most
+ feature-complete deployment method. It communicates with Headscale directly.
-If your environment is not able to support the advanced deployment, you can still use the basic deployment.
-For basic deployments, see the [Basic Deployment](/docs/Basic-Integration.md) guide.
-It does not include automatic management of ACLs, DNS settings, or the Headscale configuration,
-instead requiring manual editing and reloading when making changes.
+- ### [Simple Mode](/docs/Simple-Mode.md)
+ Simple mode does not include the automatic management of DNS and Headplane
+ settings, requiring manual editing and reloading when making changes.
-## Contributing
-If you would like to contribute, please install a relatively modern version of Node.js and PNPM.
-Clone this repository, run `pnpm install`, and then run `pnpm dev` to start the development server.
+
+
+
+
+
-> Copyright (c) 2024 Aarnav Tale
+
+
+
+
+
+
+> Copyright (c) 2025 Aarnav Tale
diff --git a/assets/acls-dark.png b/assets/acls-dark.png
index ce5c742..c778814 100644
Binary files a/assets/acls-dark.png and b/assets/acls-dark.png differ
diff --git a/assets/acls-light.png b/assets/acls-light.png
index 5275231..b8f1a8e 100644
Binary files a/assets/acls-light.png and b/assets/acls-light.png differ
diff --git a/assets/dns-dark.png b/assets/dns-dark.png
new file mode 100644
index 0000000..5ac108b
Binary files /dev/null and b/assets/dns-dark.png differ
diff --git a/assets/dns-light.png b/assets/dns-light.png
new file mode 100644
index 0000000..35ec39b
Binary files /dev/null and b/assets/dns-light.png differ
diff --git a/assets/integration-dark.png b/assets/integration-dark.png
deleted file mode 100644
index c5c388f..0000000
Binary files a/assets/integration-dark.png and /dev/null differ
diff --git a/assets/integration-light.png b/assets/integration-light.png
deleted file mode 100644
index 22a34c3..0000000
Binary files a/assets/integration-light.png and /dev/null differ
diff --git a/assets/machine-dark.png b/assets/machine-dark.png
new file mode 100644
index 0000000..1ea748b
Binary files /dev/null and b/assets/machine-dark.png differ
diff --git a/assets/machine-light.png b/assets/machine-light.png
new file mode 100644
index 0000000..0aae488
Binary files /dev/null and b/assets/machine-light.png differ
diff --git a/assets/preview-dark.png b/assets/preview-dark.png
index 0037a89..10b66fa 100644
Binary files a/assets/preview-dark.png and b/assets/preview-dark.png differ
diff --git a/assets/preview-light.png b/assets/preview-light.png
index eff1411..ffdf41c 100644
Binary files a/assets/preview-light.png and b/assets/preview-light.png differ
diff --git a/config.example.yaml b/config.example.yaml
index cb3df4f..309db63 100644
--- a/config.example.yaml
+++ b/config.example.yaml
@@ -54,6 +54,10 @@ integration:
# are set correctly. Turn this off if you are having issues with
# shareProcessNamespace not being validated correctly.
validate_manifest: true
+ # This should be the name of the Pod running Headscale and Headplane.
+ # If this isn't static you should be using the Kubernetes Downward API
+ # to set this value (refer to docs/Integrated-Mode.md for more info).
+ pod_name: "headscale"
# Proc is the "Native" integration that only works when Headscale and
# Headplane are running outside of a container. There is no configuration,
diff --git a/docs/Advanced-Integration.md b/docs/Advanced-Integration.md
deleted file mode 100644
index b4bcefe..0000000
--- a/docs/Advanced-Integration.md
+++ /dev/null
@@ -1,78 +0,0 @@
-# Advanced Integration
-
-The advanced integration methods unlock the full capabilities of Headplane.
-This is the closest you can get to the SaaS experience if you were paying for
-Tailscale.
-
-### Configuration Management
-
-
-
-
-
-
-
-The advanced integration allows you to manage the Headscale configuration via
-the Headplane UI. When the configuration is available for editing, the `DNS`
-and `Settings` tabs will become available. When using the Docker or Kubernetes
-integration, changes to the configuration file will be automatically applied
-to Headscale.
-
-> By default, the configuration file is read from `/etc/headscale/config.yaml`.
-This can be overridden by setting the `CONFIG_FILE` environment variable. Any
-variables including `HEADSCALE_URL`, `OIDC_CLIENT_ID`, `OIDC_ISSUER`, and
-`OIDC_CLIENT_SECRET` will take priority over the configuration file.
-
-### Access Control Lists (ACLs)
-
-
-
-
-
-
-
-The advanced integration allows you to manage the ACLs via the Headplane UI.
-When the ACL file is available for editing, the `Access Controls` tab will
-become available. All of the integrations support automatic reloading of the
-ACLs when the file is changed.
-
-> By default, the ACL file is read from `/etc/headscale/acl_policy.json`.
-> If `policy.path` is set and `policy.mode` is set to `file`, the ACL file will
-> be read from the path specified in the configuration file instead.
-
-## Deployment
-
-Requirements:
-- Headscale 0.23 or newer
-- Headscale and Headplane need a Reverse Proxy (NGINX, Traefik, Caddy, etc)
-
-Currently there are 3 integration providers that can do this for you:
-- [Docker Integration](/docs/integration/Docker.md)
-- [Kubernetes Integration](/docs/integration/Kubernetes.md)
-- [Native Linux Integration](/docs/integration/Native.md)
-
-Once configured, the Headplane UI will be available at the `/admin` path
-of the server you deployed it on. This is currently not configurable unless
-you build the Docker image yourself or run the Node.js server directly.
-
-Additionally, if you require access to health information for either Docker
-or Kubernetes, the `/admin/healthz` path will be available. This is useful for
-monitoring services like Prometheus or Grafana.
diff --git a/docs/Bare-Metal.md b/docs/Bare-Metal.md
new file mode 100644
index 0000000..3424a4c
--- /dev/null
+++ b/docs/Bare-Metal.md
@@ -0,0 +1,80 @@
+# Bare-Metal Mode
+
+Bare-Metal mode is the most flexible way to deploy Headplane. It allows you to
+run Headplane on any system without the need for Docker or any other container
+runtime. This is not recommended, but I understand that everyone has
+different needs.
+
+> It works with both the **Simple** and **Integrated** deployment modes. Refer
+> to the section below for instructions on configuring Integrated mode.
+
+## Deployment
+
+Requirements:
+- Headscale 0.25 or newer (already deployed)
+- Node.js 22 LTS or newer
+- [PNPM](https://pnpm.io/installation) 10.x
+- A finished configuration file (config.yaml)
+
+Clone the Headplane repository, install dependencies, and build the project:
+```sh
+git clone https://github.com/tale/headplane
+cd headplane
+pnpm install
+pnpm build
+```
+
+## Running Headplane
+You can start headplane through `pnpm start` or `node build/headplane/server.js`.
+Headplane expects the `build` directory to be present when running the server.
+The structure of this folder is very important and should not be tampered with.
+
+### Integrated Mode
+Since you are running Headplane in Bare-Metal, you most likely also are running
+Headscale in Bare-Metal. Refer to the [**Integrated Mode**](/docs/Integrated-Mode.md)
+guide for instructions on setting up the integrated mode in Native Linux (/proc).
+
+### Changing the Admin Path
+Since you are building Headplane yourself, you are able to configure the admin
+path to be anything you want. When running `pnpm build`, you can pass the
+`__INTERNAL_PREFIX` environment variable to change the admin path. For example:
+
+```sh
+__INTERNAL_PREFIX=/admin2 pnpm build
+```
+
+Just keep in mind that the admin path is not configurable at runtime, so you
+will need to rebuild the project if you want to change it. Also, anything aside
+from `/admin` is not officially supported and could break in future versions.
+
+> Refer to the [**Configuration**](/docs/Configuration.md) guide for help with
+> setting up your `config.yaml` file to the appropriate values.
+
+### Systemd Unit
+Here is an example of a systemd unit file that you can use to manage the
+Headplane service:
+
+```ini
+[Unit]
+Description=Headplane
+After=network.target
+
+[Service]
+Type=simple
+User=headplane
+Group=headplane
+WorkingDirectory=/path/to/headplane
+ExecStart=/usr/bin/node /path/to/headplane/build/headplane/server.js
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
+```
+
+You will need to replace `/path/to/headplane` with the actual path to the
+Headplane repository on your system. Save this file as `headplane.service` in
+`/etc/systemd/system/` and run `systemctl enable headplane` to enable the service.
+
+Other fields may also need some configuration, as this unit expects a user and a
+group called `headplane` to exist on the system. You can change these values to
+match your system's configuration.
diff --git a/docs/Basic-Integration.md b/docs/Basic-Integration.md
deleted file mode 100644
index 9f1c4c2..0000000
--- a/docs/Basic-Integration.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# Basic Integration
-
-The basic integration is the simplest way to get started with Headplane.
-It's more of a preview and is heavily limited in the features it can offer
-when compared to the [Advanced Integration](/docs/Advanced-Integration.md).
-
-> Note that the Advanced integration is the recommend way to run
-Headplane in a production environment.
-
-## Limitations
-- No automatic management of Access Control Lists (ACLs)
-- No management of DNS settings for your tailnet
-- No capability to edit the configuration
-- Limited support for OIDC authentication
-
-## Deployment
-
-Requirements:
-- Headscale 0.23 or newer
-- Headscale and Headplane need a Reverse Proxy (NGINX, Traefik, Caddy, etc)
-
-Docker heavily simplifies the deployment process, but this process can be
-adopted to run natively. Follow the first section of the deployment guide
-in the [Native Integration](/docs/integration/Native.md#deployment) for a
-bare-metal or virtual machine deployment.
-
-Here is a simple Docker Compose deployment:
-```yaml
-services:
- headplane:
- container_name: headplane
- image: ghcr.io/tale/headplane:0.3.9
- restart: unless-stopped
- ports:
- - '3000:3000'
- environment:
- HEADSCALE_URL: 'http://headscale:8080'
- COOKIE_SECRET: 'abcdefghijklmnopqrstuvwxyz'
-
- # These are all optional!
- ROOT_API_KEY: 'abcdefghijklmnopqrstuvwxyz'
- OIDC_CLIENT_ID: 'headscale'
- OIDC_ISSUER: 'https://sso.example.com'
- OIDC_CLIENT_SECRET: 'super_secret_client_secret'
- DISABLE_API_KEY_LOGIN: 'true'
- COOKIE_SECURE: 'false'
-
- # These are the default values
- HOST: '0.0.0.0'
- PORT: '3000'
-```
-
-Once configured, the Headplane UI will be available at the `/admin` path
-of the server you deployed it on. This is currently not configurable unless
-you build the Docker image yourself or run the Node.js server directly.
-
-> For a breakdown of each configuration variable, please refer to the
-[Configuration](/docs/Configuration.md) guide.
-> It explains what each variable does, how to configure them, and what the
-default values are.
diff --git a/docs/Configuration.md b/docs/Configuration.md
index a4401e8..abdbf57 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -1,62 +1,86 @@
# Configuration
+> Previous versions of Headplane used environment variables without a configuration file.
+> Since 0.5, you will need to manually migrate your configuration to the new format.
-You can configure Headplane using environment variables.
+Headplane uses a configuration file to manage its settings
+([**config.example.yaml**](./config.example.yaml)). By default, Headplane looks
+for a the file at `/etc/headplane/config.yaml`. This can be changed using the
+**`HEADPLANE_CONFIG_PATH`** environment variable to point to a different location.
-#### Required Variables
+## Environment Variables
+It is also possible to override the configuration file using environment variables.
+These changes get merged *after* the configuration file is loaded, so they will take precedence.
+Environment variables follow this pattern: **`HEADPLANE___`**.
+For example, to override `oidc.client_secret`, you would set `HEADPLANE_OIDC__CLIENT_SECRET`
+to the value that you want.
-- **`COOKIE_SECRET`**: A secret used to sign cookies (use a relatively long and random string).
-- **`HEADSCALE_URL`**: The public URL of your Headscale server (not required if using the configuration file).
+Here are a few more examples:
-#### Optional Variables
+- `HEADPLANE_HEADSCALE__URL`: `headscale.url`
+- `HEADPLANE_SERVER__PORT`: `server.port`
-- **`HEADSCALE_PUBLIC_URL`**: The public URL of your Headscale server (if different from `HEADSCALE_URL`).
-- **`DEBUG`**: Enable debug logging (default: `false`).
-- **`HOST`**: The host to bind the server to (default: `0.0.0.0`).
-- **`PORT`**: The port to bind the server to (default: `3000`).
-- **`CONFIG_FILE`**: The path to the Headscale `config.yaml` (default: `/etc/headscale/config.yaml`).
-- **`HEADSCALE_CONFIG_UNSTRICT`**: This will disable the strict configuration loader (default: `false`).
-- **`COOKIE_SECURE`**: This option enables the `Secure` flag for cookies, ensuring they are sent only over HTTPS, which helps prevent interception and enhances data security. It should be disabled when using HTTP instead of HTTPS (default: `true`).
-- **`LOAD_ENV_FILE`**: Tell Headplane to read the `.env` file and load it into the environment (default: `false`).
+**This functionality is NOT enabled by default!**
+To enable it, set the environment variable **`HEADPLANE_LOAD_ENV_OVERRIDES=true`**.
+Setting this also tells Headplane to load the relative `.env` file into the environment.
-#### Docker Integration
-The Docker integration allows Headplane to manage the Headscale docker container.
-You'll need to provide these variables if you want to use this feature.
-Keep in mind that `DOCKER_SOCK` must start with a protocol (e.g., `unix://`).
-Secure API is currently not supported.
+## Debugging
+To enable debug logging, set the **`HEADPLANE_DEBUG_LOG=true`** environment variable.
+This will enable all debug logs for Headplane, which could fill up log space very quickly.
+This is not recommended in production environments.
-- **`DOCKER_SOCK`**: The protocol and path to the Docker socket (default: `unix:///var/run/docker.sock`).
-- **`HEADSCALE_CONTAINER`**: The name of the Headscale container (required for Docker integration).
+## Reverse Proxying
+Reverse proxying is very common when deploying web applications. Headscale and
+Headplane are very similar in this regard. You can use the same configuration
+of any reverse proxy you are familiar with. Here is an example of how to do it
+using Traefik:
-### SSO/OpenID Connect
-If you want to use OpenID Connect for SSO, you'll need to provide these variables.
-Headplane will utilize the expiry of your tokens to determine the expiry of the session.
-If you use the Headscale configuration integration, these are not required.
-
-- **`OIDC_ISSUER`**: The issuer URL of your OIDC provider.
-- **`OIDC_CLIENT_ID`**: The client ID of your OIDC provider.
-- **`OIDC_CLIENT_SECRET`**: The client secret of your OIDC provider.
-- **`OIDC_CLIENT_SECRET_METHOD`**: The method used to send the client secret (default: `client_secret_basic`).
-- **`OIDC_REDIRECT_URI`**: The redirect URI for the OIDC provider (recommended, otherwise guessed).
-- **`OIDC_SKIP_CONFIG_VALIDATION`**: Skip the OIDC configuration validation (default: `false`).
-- **`ROOT_API_KEY`**: An API key used to issue new ones for sessions (keep expiry fairly long).
-- **`DISABLE_API_KEY_LOGIN`**: If you want to disable API key login, set this to `true`.
-
-Here's what an example with Authelia would look like if you used the same client for both Headscale and Headplane.
-Keep in mind that the recommended deployment would be putting Headplane behind /admin on a reverse proxy.
-If you use a different domain than the Headscale server, you'll need to make sure that Headscale responds with CORS headers.
+> The important part here is the CORS middleware. This is required for the
+> frontend to communicate with the backend. If you are using a different reverse
+> proxy, make sure to add the necessary headers to allow the frontend to communicate
+> with the backend.
```yaml
-- client_id: 'headscale'
- client_name: 'Headscale and Headplane'
- public: false
- authorization_policy: 'two_factor'
- redirect_uris:
- - 'https://headscale.example.com/oidc/callback'
- - 'https://headscale.example.com/admin/oidc/callback'
- scopes:
- - 'openid'
- - 'profile'
- - 'email'
- userinfo_signed_response_alg: 'none'
- client_secret: 'my_super_secret_client_secret'
-```
+http:
+ routers:
+ headscale:
+ rule: 'Host(`headscale.tale.me`)'
+ service: 'headscale'
+ middlewares:
+ - 'cors'
+
+ rewrite:
+ rule: 'Host(`headscale.tale.me`) && Path(`/`)'
+ service: 'headscale'
+ middlewares:
+ - 'rewrite'
+
+ headplane:
+ rule: 'Host(`headscale.tale.me`) && PathPrefix(`/admin`)'
+ service: 'headplane'
+
+ services:
+ headscale:
+ loadBalancer:
+ servers:
+ - url: 'http://headscale:8080'
+
+ headplane:
+ loadBalancer:
+ servers:
+ - url: 'http://headplane:3000'
+
+ middlewares:
+ rewrite:
+ addPrefix:
+ prefix: '/admin'
+ cors:
+ headers:
+ accessControlAllowHeaders: '*'
+ accessControlAllowMethods:
+ - 'GET'
+ - 'POST'
+ - 'PUT'
+ accessControlAllowOriginList:
+ - 'https://headscale.tale.me'
+ accessControlMaxAge: 100
+ addVaryHeader: true
diff --git a/docs/Integrated-Mode.md b/docs/Integrated-Mode.md
new file mode 100644
index 0000000..c2d0341
--- /dev/null
+++ b/docs/Integrated-Mode.md
@@ -0,0 +1,181 @@
+# Integrated Mode
+
+
+
+
+
+
+
+Integrated mode is a deployment method that allows you to deploy Headplane with
+automatic management of DNS and Headplane settings. This is the recommended
+deployment method for most users, as it provides a more feature-complete
+experience.
+
+## Deployment
+> If you are not looking to deploy with Docker, follow the [**Bare-Metal**](/docs/Bare-Metal.md) deployment guide.
+> Refer to the `Integrated Mode` section at the bottom for caveats.
+
+Requirements:
+- Docker and Docker Compose
+- Headscale 0.25 or newer
+- A finished configuration file (config.yaml)
+
+Here is what a sample Docker Compose deployment would look like:
+```yaml
+services:
+ headplane:
+ # I recommend you pin the version to a specific release
+ image: ghcr.io/tale/headplane:0.5.0
+ container_name: headplane
+ restart: unless-stopped
+ ports:
+ - '3000:3000'
+ volumes:
+ - './config.yaml:/etc/headplane/config.yaml'
+ # This should match headscale.config_path in your config.yaml
+ - './headscale-config/config.yaml:/etc/headscale/config.yaml'
+
+ # If you are using the Docker integration, mount the Docker socket
+ - '/var/run/docker.sock:/var/run/docker.sock:ro'
+ headscale:
+ image: headscale/headscale:0.25.0
+ container_name: headscale
+ restart: unless-stopped
+ command: serve
+ ports:
+ - '8080:8080'
+ volumes:
+ - './headscale-data:/var/lib/headscale'
+ - './headscale-config:/etc/headscale'
+
+```
+
+This will result in the Headplane UI being available at the `/admin` path of the
+server you deployed it on. The `/admin` path is currently not configurable unless
+you build the container yourself or run Headplane in Bare-Metal mode.
+
+> Refer to the [**Configuration**](/docs/Configuration.md) guide for help with
+> setting up your `config.yaml` file to the appropriate values.
+
+## Docker Integration
+The Docker integration is the easiest to setup, as it only requires the Docker socket
+to be mounted into the container along with some configuration. As long as Headplane
+has access to the Docker socket and the name of the Headscale container, it will
+automatically propagate config and DNS changes to Headscale without any additional
+configuration.
+
+## Native Linux (/proc) Integration
+The `proc` integration is used when you are running Headscale and Headplane on
+non-Docker environments. Headplane will attempt to locate the Headscale process
+PID through the `/proc` filesystem and communicate with it directly. In order for
+this to work, the Headplane process must have permission to do the following:
+
+- Read the `/proc` filesystem
+- Send signals to the Headscale process (`SIGTERM`)
+
+The best way to ensure this is to run Headplane as the same user as Headscale
+(or optionally just run them both as `root`). Due to the way the integration is
+currently configured, Headplane will not re-check the Headscale process PID if
+it changes. This means that if you restart Headscale manually, you will need to
+restart Headplane as well.
+
+## Kubernetes Integration
+The Kubernetes integration is the most complex to setup, as it requires a
+service account with the appropriate permissions to be created. The service
+account must have the following permissions and looks like this:
+```yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: headplane-agent
+ namespace: default # Adjust namespace as needed
+rules:
+- apiGroups: ['']
+ resources: ['pods']
+ verbs: ['get', 'list']
+- apiGroups: ['apps']
+ resources: ['deployments']
+ verbs: ['get', 'list']
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: headplane-agent
+ namespace: default # Adjust namespace as needed
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: headplane-agent
+subjects:
+- kind: ServiceAccount
+ name: default # If you use a different service account, change this
+ namespace: default # Adjust namespace as needed
+```
+
+To successfully deploy Headplane in Kubernetes, you will need to run both the
+Headplane and Headscale containers in the same pod. This is because Headplane
+needs access to Headscale's PID in order to communicate with it. Here is an
+example, note the **`shareProcessNamespace: true`** field:
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: headplane
+ namespace: default # Adjust namespace as needed
+ labels:
+ app: headplane
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: headplane
+ template:
+ metadata:
+ labels:
+ app: headplane
+ spec:
+ shareProcessNamespace: true
+ serviceAccountName: default
+ containers:
+ - name: headplane
+ image: ghcr.io/tale/headplane:0.5.0
+ env:
+ # Set these if the pod name for Headscale is not static
+ # We will use the downward API to get the pod name instead
+ - name: HEADPLANE_ENV_LOAD_OVERRIDES
+ value: 'true'
+ - name: 'HEADPLANE_INTEGRATION__KUBERNETES__POD_NAME'
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ volumeMounts:
+ - name: headscale-config
+ mountPath: /etc/headscale
+
+ - name: headscale
+ image: headscale/headscale:0.25.0
+ command: ['serve']
+ volumeMounts:
+ - name: headscale-data
+ mountPath: /var/lib/headscale
+ - name: headscale-config
+ mountPath: /etc/headscale
+
+ volumes:
+ - name: headscale-data
+ persistentVolumeClaim:
+ claimName: headscale-data
+ - name: headscale-config
+ persistentVolumeClaim:
+ claimName: headscale-config
+```
diff --git a/docs/Simple-Mode.md b/docs/Simple-Mode.md
new file mode 100644
index 0000000..b99c0ec
--- /dev/null
+++ b/docs/Simple-Mode.md
@@ -0,0 +1,36 @@
+# Simple Mode
+
+Simple mode enables you to quickly deploy Headplane and is recommended for any
+testing or simple environments. It does not include the automatic management of
+DNS and Headplane settings, requiring manual editing and reloading when making
+changes. If you're looking for a more feature-complete deployment method, check
+out [**Integrated Mode**](/docs/Integrated-Mode.md).
+
+## Deployment
+> If you are not looking to deploy with Docker, follow the [**Bare-Metal**](/docs/Bare-Metal.md) deployment guide.
+
+Requirements:
+- Docker and Docker Compose
+- Headscale 0.25 or newer (already deployed)
+- A finished configuration file (config.yaml)
+
+Here is what a sample Docker Compose deployment would look like:
+```yaml
+services:
+ headplane:
+ # I recommend you pin the version to a specific release
+ image: ghcr.io/tale/headplane:0.5.0
+ container_name: headplane
+ restart: unless-stopped
+ ports:
+ - '3000:3000'
+ volumes:
+ - './config.yaml:/etc/headplane/config.yaml'
+```
+
+This will result in the Headplane UI being available at the `/admin` path of the
+server you deployed it on. The `/admin` path is currently not configurable unless
+you build the container yourself or run Headplane in Bare-Metal mode.
+
+> Refer to the [**Configuration**](/docs/Configuration.md) guide for help with
+> setting up your `config.yaml` file to the appropriate values.
diff --git a/docs/integration/Docker.md b/docs/integration/Docker.md
deleted file mode 100644
index 083dc55..0000000
--- a/docs/integration/Docker.md
+++ /dev/null
@@ -1,89 +0,0 @@
-## Docker Integration
-
-The Docker integration allows you to run Headplane and Headscale separately
-in a dockerized environment. It allows you to unlock full functionality such as
-automatic reloading of ACLs, DNS management, and Headscale configuration
-management.
-
-### Deployment
-
-> When running with the Docker integration, it's assumed that both Headscale and
-Headplane will run as containers. If you are running Headscale natively, then
-refer to the [Native Integration](/docs/integration/Native.md) guide.
-
-To enable the Docker integration, set the `HEADSCALE_INTEGRATION` environment
-variable to `docker`. You'll also need to supply `HEADSCALE_CONTAINER` with the
-name or ID of the Headscale container.
-
-By default Headplane uses `unix:///var/run/docker.sock` to connect to Docker.
-This can be overridden by setting the `DOCKER_SOCK` environment variable. For
-example, a remote socket would be `tcp://:2375`. When setting
-the variable, you'll need to specify the protocol (`unix://` or `tcp://`).
-
-> The `DOCKER_SOCK` variable does not support the HTTPS protocol.
-
-To enable the Docker integration, set `HEADSCALE_INTEGRATION=docker` in the environment variables.
-Additionally, you'll need to pass in the `HEADSCALE_CONTAINER` environment variable.
-This should be either the name or ID of the Headscale container (you can retrieve this using `docker ps`).
-If the other integrations aren't setup, then Headplane will automatically disable the Docker integration.
-
-By default the integration will check for `/var/run/docker.sock`, however you can override this by
-setting the `DOCKER_SOCK` environment variable if you use a different configuration than the default.
-When setting `DOCKER_SOCK`, you'll need to include the protocol (e.g., `unix://` or `tcp://`).
-Headplane currently does not support the HTTPS protocol for the Docker socket.
-
-Here's an example deployment using Docker Compose (recommended). Keep in mind
-that you'll NEED to setup a reverse proxy and this is incomplete:
-```yaml
-services:
- headscale:
- image: 'headscale/headscale:0.23.0'
- container_name: 'headscale'
- restart: 'unless-stopped'
- command: 'serve'
- volumes:
- - './data:/var/lib/headscale'
- - './configs:/etc/headscale'
- ports:
- - '8080:8080'
- environment:
- TZ: 'America/New_York'
- headplane:
- container_name: headplane
- image: ghcr.io/tale/headplane:0.3.9
- restart: unless-stopped
- volumes:
- - './data:/var/lib/headscale'
- - './configs:/etc/headscale'
- - '/var/run/docker.sock:/var/run/docker.sock:ro'
- ports:
- - '3000:3000'
- environment:
- # This is always required for Headplane to work
- COOKIE_SECRET: 'abcdefghijklmnopqrstuvwxyz'
-
- HEADSCALE_INTEGRATION: 'docker'
- HEADSCALE_CONTAINER: 'headscale'
- DISABLE_API_KEY_LOGIN: 'true'
- HOST: '0.0.0.0'
- PORT: '3000'
-
- # Only set this to false if you aren't behind a reverse proxy
- COOKIE_SECURE: 'false'
-
- # Overrides the configuration file values if they are set in config.yaml
- # If you want to share the same OIDC configuration you do not need this
- OIDC_CLIENT_ID: 'headscale'
- OIDC_ISSUER: 'https://sso.example.com'
- OIDC_CLIENT_SECRET: 'super_secret_client_secret'
-
- # This NEEDS to be set with OIDC, regardless of what's in the config
- # This needs to be a very long-lived (999 day) API key used to create
- # shorter ones for OIDC and allow the OIDC functionality to work
- ROOT_API_KEY: 'abcdefghijklmnopqrstuvwxyz'
-```
-
-> For a breakdown of each configuration variable, please refer to the
-[Configuration](/docs/Configuration.md) guide.
-> It explains what each variable does, how to configure them, and what the
-default values are.
diff --git a/docs/integration/Kubernetes.md b/docs/integration/Kubernetes.md
deleted file mode 100644
index 0059503..0000000
--- a/docs/integration/Kubernetes.md
+++ /dev/null
@@ -1,133 +0,0 @@
-## Kubernetes Integration
-
-The Kubernetes integration allows you to run Headplane and Headscale together
-in a cluster. It allows you to unlock full functionality such as automatic
-reloading of ACLs, DNS management, and Headscale configuration management.
-
-Currently there are a few limitations to the Kubernetes integration:
-- Headplane and Headscale need to run in the same Pod and share the same
- process space for the integration to work correctly due to a limitation in
- the Kubernetes API.
-
-- The only supported methods of deploying the integration are through a
- `Deployment` or `Pod` (more coming soon). You can still get around this with
- the `HEADSCALE_INTEGRATION_UNSTRICT` variable, but it's not recommended.
-
-- The integration will assume that the Headscale container will always restart
- because the integration relies on a system call that will exit the container.
-
-### Deployment
-
-In order to ensure Headplane can read Kubernetes resources, you'll need to
-grant additional RBAC permissions to the default `ServiceAccount` in the
-namespace. This can be done with the following:
-```yaml
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
- name: headplane-agent
- namespace: default # Adjust namespace as needed
-rules:
-- apiGroups: ['']
- resources: ['pods']
- verbs: ['get', 'list']
-- apiGroups: ['apps']
- resources: ['deployments']
- verbs: ['get', 'list']
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
- name: headplane-agent
- namespace: default # Adjust namespace as needed
-roleRef:
- apiGroup: rbac.authorization.k8s.io
- kind: Role
- name: headplane-agent
-subjects:
-- kind: ServiceAccount
- name: default # If you use a different service account, change this
- namespace: default # Adjust namespace as needed
-```
-
-Keep in mind you'll need to make `PersistentVolumeClaim`s for the data and that
-they need to be either `ReadWriteOnce` or `ReadWriteMany` depending on your
-topology. Additionally, you can abstract environment variables and configuration
-away into a `ConfigMap` or `Secret` for easier management.
-
-The important parts of this deployment are the `HEADSCALE_INTEGRATION` and
-`DEPLOYMENT_NAME` environment variables. The `HEADSCALE_INTEGRATION` variable
-should be set to `kubernetes` and the `POST_NAME` variable should be set
-to the name of the pod (done using the Downward API below).
-
-> If you are having issues with validating `shareProcessNamespace`, you can
-set `HEADSCALE_INTEGRATION_UNSTRICT` to `true` to disable the strict checks.
-
-A basic deployment of the integration would look like this. Keep in mind that
-you are responsible for setting up a reverse-proxy via an `Ingress` or `Service`
-otherwise Headplane will not work:
-```yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: headplane
- namespace: default # Adjust namespace as needed
- labels:
- app: headplane
-spec:
- replicas: 1
- selector:
- matchLabels:
- app: headplane
- template:
- metadata:
- labels:
- app: headplane
- spec:
- shareProcessNamespace: true
- serviceAccountName: default
- containers:
- - name: headplane
- image: ghcr.io/tale/headplane:0.3.9
- env:
- - name: COOKIE_SECRET
- value: 'abcdefghijklmnopqrstuvwxyz'
- - name: HEADSCALE_INTEGRATION
- value: 'kubernetes'
- - name: POD_NAME
- valueFrom:
- fieldRef:
- fieldPath: metadata.name
-
- # Only set this to false if you aren't behind a reverse proxy
- - name: COOKIE_SECURE
- value: 'false'
- volumeMounts:
- - name: headscale-config
- mountPath: /etc/headscale
-
- - name: headscale
- image: headscale/headscale:0.23.0
- command: ['serve']
- env:
- - name: TZ
- value: 'America/New_York'
- volumeMounts:
- - name: headscale-data
- mountPath: /var/lib/headscale
- - name: headscale-config
- mountPath: /etc/headscale
-
- volumes:
- - name: headscale-data
- persistentVolumeClaim:
- claimName: headscale-data
- - name: headscale-config
- persistentVolumeClaim:
- claimName: headscale-config
-```
-
-> For a breakdown of each configuration variable, please refer to the
-[Configuration](/docs/Configuration.md) guide.
-> It explains what each variable does, how to configure them, and what the
-default values are.
diff --git a/docs/integration/Native.md b/docs/integration/Native.md
deleted file mode 100644
index 84e020d..0000000
--- a/docs/integration/Native.md
+++ /dev/null
@@ -1,69 +0,0 @@
-## Native Integration
-
-The Native integration allows you to run both Headplane and Headscale on
-bare-metal servers or virtual machines. This integration is best suited for
-environments where Docker or Kubernetes are not available or not desired.
-
-Currently the Native integration only supports automatic reloading of ACLs. It
-cannot handle configuration changes as killing the `headscale` process can lead
-to undefined behavior or the service not restarting.
-
-### Deployment
-
-1. Follow the instructions to install Headscale from the
-[Linux Installation Guide](https://headscale.net/stable/setup/install/official/).
-
-2. Install [Node.js](https://nodejs.org/en/download/package-manager)
-version 20 or higher (your package manager most likely already has this).
-
-3. Install [PNPM](https://pnpm.io/installation). This is required
-as Headplane has issues running correctly when installed and built via NPM or Yarn.
-
-4. Clone the Headplane repository, install dependencies, and build the project:
-```sh
-git clone https://github.com/tale/headplane # (or clone via SSH)
-cd headplane
-pnpm install
-pnpm build
-```
-
-### Running Headplane
-Start Headplane with `node build/headplane/server.js`.
-
-Headplane does need various environment variables to run correctly. The required
-variables can be found in the [Configuration](/docs/Configuration.md) guide.
-If you choose to do this with a `.env` file, you can use the `LOAD_ENV_FILE`
-variable to tell Headplane to load the file.
-
-Finally, make sure to set `HEADSCALE_INTEGRATION=proc` to take advantage
-of controlling Headscale natively on Linux.
-
-```sh
-LOAD_ENV_FILE=true node ./build/headplane/server.js
-```
-
-> If you'd like, you can turn this into a `systemd` unit to manage the service.
-> I plan to provide packages and unit files to make this easier in the future.
-
-### Cannot find ./build directory?
-Headplane expects the `build` directory to be present when running the server.
-The structure of this folder is very important and should not be tampered with.
-If you would like to keep the build directory in a different location, you can
-set the `BUILD_PATH` environment variable to the path of the build directory
-at runtime.
-
-```sh
-LOAD_ENV_FILE=true BUILD_PATH=/path/to/build node ./build/headplane/server.js
-```
-
-### Changing Headplane's Path from `/admin`
-Additionally, because you are building Headplane from source, you're able to
-change the default path that Headplane is served from. This can be done by
-specifying the `__INTERNAL_PREFIX` environment variable, when building.
-
-```sh
-__INTERNAL_PREFIX=/my-admin-path pnpm build
-```
-
-> Keep in mind that this is very much an experimental feature. Things can easily
-> break and until it's more stable, it's not recommended to use in production.
diff --git a/server/context/loader.ts b/server/context/loader.ts
index bd1b681..f453c7e 100644
--- a/server/context/loader.ts
+++ b/server/context/loader.ts
@@ -1,5 +1,6 @@
import { constants, access, readFile } from 'node:fs/promises';
import { type } from 'arktype';
+import dotenv from 'dotenv';
import { parseDocument } from 'yaml';
import { testOidc } from '~/utils/oidc';
import log, { hpServer_loadLogger } from '~server/utils/log';
@@ -27,7 +28,6 @@ const envBool = type('string | undefined').pipe((v) => {
const rootEnvs = type({
HEADPLANE_DEBUG_LOG: envBool,
- HEADPLANE_LOAD_ENV_FILE: envBool,
HEADPLANE_LOAD_ENV_OVERRIDES: envBool,
HEADPLANE_CONFIG_PATH: 'string | undefined',
}).onDeepUndeclaredKey('reject');
@@ -65,7 +65,6 @@ export async function hp_loadConfig() {
const envs = rootEnvs({
HEADPLANE_DEBUG_LOG: process.env.HEADPLANE_DEBUG_LOG,
HEADPLANE_CONFIG_PATH: process.env.HEADPLANE_CONFIG_PATH,
- HEADPLANE_LOAD_ENV_FILE: process.env.HEADPLANE_LOAD_ENV_FILE,
HEADPLANE_LOAD_ENV_OVERRIDES: process.env.HEADPLANE_LOAD_ENV_OVERRIDES,
});
@@ -97,12 +96,10 @@ export async function hp_loadConfig() {
debug: envs.HEADPLANE_DEBUG_LOG,
});
- if (envs.HEADPLANE_LOAD_ENV_FILE) {
- log.info('CFGX', 'Loading a .env file if one exists');
- await import('dotenv/config');
- }
-
if (config && envs.HEADPLANE_LOAD_ENV_OVERRIDES) {
+ log.info('CFGX', 'Loading a .env file if one exists');
+ dotenv.config();
+
log.info(
'CFGX',
'Loading environment variables to override the configuration',