{"id":17590978,"url":"https://github.com/niklasrosenstein/headscale-fly-io","last_synced_at":"2026-01-16T10:41:02.305Z","repository":{"id":258293132,"uuid":"870855276","full_name":"NiklasRosenstein/headscale-fly-io","owner":"NiklasRosenstein","description":"Run Headscale on Fly.io with Litestream replication to the integrated Tigris object storage","archived":false,"fork":false,"pushed_at":"2026-01-14T09:50:15.000Z","size":271,"stargazers_count":32,"open_issues_count":5,"forks_count":5,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-14T13:45:54.870Z","etag":null,"topics":["flyio","headscale","tailscale"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/NiklasRosenstein.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-10-10T19:39:19.000Z","updated_at":"2026-01-14T09:48:04.000Z","dependencies_parsed_at":"2024-10-18T10:20:02.941Z","dependency_job_id":"76fafa86-d071-4504-b9e5-9e2be4ce5a29","html_url":"https://github.com/NiklasRosenstein/headscale-fly-io","commit_stats":null,"previous_names":["niklasrosenstein/headscale-fly-io"],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/NiklasRosenstein/headscale-fly-io","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NiklasRosenstein%2Fheadscale-fly-io","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NiklasRosenstein%2Fheadscale-fly-io/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NiklasRosenstein%2Fheadscale-fly-io/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NiklasRosenstein%2Fheadscale-fly-io/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/NiklasRosenstein","download_url":"https://codeload.github.com/NiklasRosenstein/headscale-fly-io/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NiklasRosenstein%2Fheadscale-fly-io/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28478050,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-16T06:30:42.265Z","status":"ssl_error","status_checked_at":"2026-01-16T06:30:16.248Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["flyio","headscale","tailscale"],"created_at":"2024-10-22T04:43:50.064Z","updated_at":"2026-01-16T10:41:02.294Z","avatar_url":"https://github.com/NiklasRosenstein.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\".github/assets/headscale-on-fly.jpg\"\u003e\n\u003c/p\u003e\n\n# Headscale on Fly.io\n\nThis repository builds a Docker image that can be run as an app on [Fly.io] to create an easy, robust and affordable\ndeployment of [Headscale] (an open source implementation of the [Tailscale] control plane, allowing you to create your\nself-hosted virtual private network using Tailscale clients). It uses [Litestream] to replicate and restore the SQlite\ndatabase from an S3 bucket (such as [Tigris] bucket integrated with your Fly.io app).\n\nThe default configuration is to use the cheapested VM size available, `shared-cpu-1x`. This sizing should be sufficient\nto support tens if not up to 100 nodes in your VPN while costing you approx. 2 USD/mo (depending on the region). Tigris\nobject storage has a free allowance of 5GB/mo, which you will likely not exceed. (By default we run Litestream with a\nlonger sync interval to not exceed the free Tigris API request limit all too easily).\n\nNote that, because Tailscale connected devices report back to the control plane on a regular, short interval, you won't\nbe able to benefit from Fly.io technically being able to automatically scale your application down to 0, unless you have\nno nodes connected.\n\n  [Fly.io]: https://fly.io\n  [Headscale]: https://github.com/juanfont/headscale\n  [Litestream]: https://litestream.io/\n  [Tailscale]: https://tailscale.com/\n  [Tigris]: https://fly.io/docs/tigris/\n\n__Contents__\n\n\u003c!-- toc --\u003e\n- [Headscale on Fly.io](#headscale-on-flyio)\n  - [Prerequisites](#prerequisites)\n  - [Installation](#installation)\n  - [Usage](#usage)\n  - [Updates](#updates)\n  - [Advanced configuration and usage](#advanced-configuration-and-usage)\n    - [ACLs](#acls)\n    - [Configuring OIDC](#configuring-oidc)\n    - [Headplane Web UI](#headplane-web-ui)\n      - [Accessing Headplane](#accessing-headplane)\n      - [Headplane Configuration](#headplane-configuration)\n    - [Using a custom domain](#using-a-custom-domain)\n    - [Metrics](#metrics)\n    - [Environment variables](#environment-variables)\n    - [Migrating to Headscale on Fly.io](#migrating-to-headscale-on-flyio)\n    - [Migrating from Postgres](#migrating-from-postgres)\n    - [litestream-entrypoint.sh](#litestream-entrypointsh)\n  - [Development](#development)\n  - [Integration testing](#integration-testing)\n\u003c!-- end toc --\u003e\n\n## Prerequisites\n\n* An account on [Fly.io]\n* The [fly](https://github.com/superfly/flyctl) CLI\n* The [age](https://github.com/FiloSottile/age) CLI\n\n## Installation\n\nCopy [`fly.example.toml`](./fly.example.toml) to a `fly.toml` file and modify it. The minimum change you need to make\nis to update the `app` field. Unless you configure a [custom domain](#using-a-custom-domain), this will define the name\nof your Headscale server (i.e. `https://\u003capp\u003e.fly.dev`).\n\nYou then need to create the app, create object storage and initialize secret values that Headscale requires to run.\nThese steps can be performed with the following commands. Note that the storage name can be anything, but if you don't\nhave a better name, just give it the same name as the app.\n\n    $ fly apps create \u003capp\u003e\n    $ fly storage create -a \u003capp\u003e -n \u003cname\u003e\n    $ age-keygen -o age.privkey\n    $ fly secrets set NOISE_PRIVATE_KEY=\"privkey:$(openssl rand -hex 32)\" AGE_SECRET_KEY=\"$(tail -n1 age.privkey)\"\n\nAll that's left now is to deploy the application. After initial deployment, you should scale the application down to\none, (or pass `--ha=false` to the deploy command), as the initial deploy will default to set the machine count to two.\nDespite the SQlite database being replicated, it does not support multiple users that independently write data to the\nsame database.\n\n    $ fly deploy\n    $ fly scale count 1\n\n\u003e You could run the SQlite database with something like [LiteFS] to achieve a highly available installation of\n\u003e Headscale, but that is not currently supported in this project.\n\n  [LiteFS]: https://github.com/superfly/litefs\n\n## Usage\n\nOn a device, run\n\n    $ tailscale up --login-server https://\u003capp\u003e.fly.dev\n\nFollowing the link that will be displayed in the console will give you the `headscale` command to run to register\nthe device. You may need to create a user first with the `headscale user create` command. If you have not\n[configured OIDC](#configuring-oidc), you need to use the Headscale CLI to register the node in the control plane.\n\nFor this you can either shell into your Headscale deployment via `fly ssh console` and use the `headscale` command\nthere, or use the Headscale CLI locally to remotely control it. For this, you must have first generated an API key\nby connecting via SSH and running `headscale apikeys create`.\n\nThen, locally, make sure you have the same version of the Headscale CLI installed that is running on your Fly.io app\nand follow [as documented](https://headscale.net/ref/remote-cli/?h=api#download-and-configure-headscale).\n\nThe gRPC endpoint is exposed at the `/headscale.` path. Connect to port 443 (Fly.io's TLS termination):\n\n    $ export HEADSCALE_CLI_ADDRESS=${FLY_APP_NAME}.fly.dev:443\n    $ export HEADSCALE_CLI_API_KEY=...\n    $ headscale node list\n\n**Note:** Fly.io handles TLS termination on port 443 and forwards traffic to nginx on port 8080 with HTTP/2 enabled. Nginx then proxies the gRPC traffic to the local headscale instance on port 50443.\n\n## Updates\n\nYou should use an immutable tag in your `fly.toml` configuration file's `[build.image]` parameter. Using a mutable tag,\nsuch as `:main` (pointing to the latest version of the `main` branch of this repository), does not guarantee that your\ndeployment comes up with the latest image version as a prior version may be cached.\n\nSimply run `fly deploy` after updating the `[build.image]`. Note that there will be a brief downtime unless you\nconfigured a highly available deployment. Be sure to check the release notes to see if there are any breaking changes\nthat require an update to your apps configuration!\n\n## Advanced configuration and usage\n\n### ACLs\n\nWe configure Headscale to store the ACL in the database instead of from file, this allows updating the ACLs without\na `fly deploy` on every update. Follow the above steps to remote-control the Headscale server and then use the\n`headscale policy get` and `headscale policy set` commands.\n\n### Configuring OIDC\n\nTo enable OIDC, you must at the minimum provide the following environment variables:\n\n* `HEADSCALE_OIDC_ISSUER`\n* `HEADSCALE_OIDC_CLIENT_ID`\n* `HEADSCALE_OIDC_CLIENT_SECRET`\n\nPlease make sure that you pass the client secret using `fly secrets set` instead of via the `[[env]]` section of\nyour `fly.toml` configuration file.\n\n### Headplane Web UI\n\nThis deployment includes [Headplane](https://headplane.net/), a full-featured web-based admin interface for Headscale.\nHeadplane provides:\n\n- 👥 Single-sign-on integration\n- 🔎 Detailed Tailnet overview and host information\n- 📝 Configure Headscale settings including networking and auth controls\n\n#### Accessing Headplane\n\nOnce deployed, access Headplane at `https://\u003cyour-domain\u003e/admin` (or `https://\u003cyour-app-name\u003e.fly.dev/admin`).\n\nOn first access, you'll need a Headscale API key. Generate one with:\n\n```bash\n$ fly ssh console -C \"headscale apikeys create --expiration 90d\"\n```\n\nCopy the API key and use it to log in to Headplane.\n\n#### Headplane Configuration\n\nHeadplane is disabled by default. To enable it, set:\n\n```toml\n[env]\n  HEADPLANE_ENABLED = \"true\"\n```\n\n**Enabling OIDC for Headplane:**\n\nTo enable single sign-on for Headplane, first register the callback URL in your OIDC provider:\n- **Callback URL:** `https://\u003cyour-domain\u003e/admin/oidc/callback`\n- Example: `https://tailscale.microcloud.dev/admin/oidc/callback`\n\nThen set the following environment variables:\n\n```bash\n$ fly secrets set HEADPLANE_OIDC_CLIENT_SECRET=\u003cyour-client-secret\u003e\n$ fly secrets set HEADPLANE_OIDC_HEADSCALE_API_KEY=$(fly ssh console -C \"headscale apikeys create --expiration 999d\")\n```\n\nAdd to your `fly.toml`:\n\n```toml\n[env]\n  HEADPLANE_OIDC_ISSUER = \"https://accounts.google.com\"\n  HEADPLANE_OIDC_CLIENT_ID = \"your-client-id\"\n```\n\n**Note:** For the best experience, use the **same** OIDC client ID for both Headscale and Headplane.\n\n**Logs:**\n\nBoth Headscale and Headplane log to stdout/stderr, so all logs are visible in `fly logs`.\n\n**Architecture:**\n\nHeadplane runs alongside Headscale in the same container, with nginx routing:\n- `/` → Headscale (API and control plane)\n- `/admin` → Headplane (web UI)\n- `/headscale.*` → Headscale gRPC endpoint (for remote CLI access)\n\nFly.io handles TLS termination on port 443 and forwards traffic to nginx on port 8080 with HTTP/2. Nginx then acts as a reverse proxy to the internal services.\n\nFor more information, see the [Headplane documentation](https://headplane.net/).\n\n### Using a custom domain\n\n1. Create a CNAME entry for your Fly.io application\n2. Run `fly certs add \u003ccustom_domain\u003e`\n3. Set the `HEADSCALE_DOMAIN_NAME=\u003ccustom_domain\u003e` in the `fly.toml`'s `[env]` section and re-deploy\n\nSee also the related documentation on [Fly.io: Custom domains](https://fly.io/docs/networking/custom-domain/).\n\n### Metrics\n\nMetrics are automatically available through Fly.io's built-in managed Prometheus metrics collection and Grafana\ndashboard. Simply click on \"Metrics\" in your Fly.io account and explore `headscale_*` metrics.\n\n### Environment variables\n\nMany Headscale configuration options can be set vie the `[env]` section in your `fly.toml` configuration file. The\nfollowing is a complete list of the environment variables the Headscale-on-Fly.io recognizes, including those that\nare expected to be set automatically.\n\n__System variables__\n\n| Variable                | Default     | Description                                                                                                                                    |\n| ----------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |\n| `AWS_ACCESS_KEY_ID`     | (automatic) | Access key for the object storage for Litestream SQlite replication. Usually set automatically by Fly.io when enabling the Tigris integration. |\n| `AWS_SECRET_ACCESS_KEY` | (automatic) | Secret key for the object storage.                                                                                                             |\n| `AWS_REGION`            | (automatic) |                                                                                                                                                |\n| `AWS_ENDPOINT_URL_S3`   | (automatic) |                                                                                                                                                |\n| `BUCKET_NAME`           | (automatic) |                                                                                                                                                |\n| `FLY_APP_NAME`          | (automatic) | Used to determine the Headscale server URL, if `HEADSCALE_DOMAIN_NAME` is not set.                                                             |\n\n__Security variables__\n\n| Variable            | Default           | Description                                                                                                                                            |\n| ------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `AGE_SECRET_KEY`    | n/a, but required | [age] Secret key for encryption your Litestream SQLite replication.                                                                                    |\n| `NOISE_PRIVATE_KEY` | n/a, but required | Noise private key for Headscale. Generate with `echo privkey:$(openssl rand -hex 32)`. **Important:** Pass this value securely with `fly secrets set`. |\n\n__Headscale configuration variables__\n\n| Variable                                         | Default                                                        | Description                                                                                                                                                                                                                                                                                        |\n| ------------------------------------------------ | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `HEADSCALE_DOMAIN_NAME`                          | `${FLY_APP_NAME}.fly.dev`                                      | URL of the Headscale server.                                                                                                                                                                                                                                                                       |\n| `HEADSCALE_DNS_BASE_DOMAIN`                      | `tailnet`                                                      | Base domain for members in the Tailnet. This **must not** be a part of the `HEADSCALE_DOMAIN_NAME`.                                                                                                                                                                                                |\n| `HEADSCALE_DNS_MAGIC_DNS`                        | `true`                                                         | Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).                                                                                                                                                                                                                                |\n| `HEADSCALE_DNS_NAMESERVERS_GLOBAL`               | `1.1.1.1, 1.0.0.1, 2606:4700:4700::1111, 2606:4700:4700::1001` | A comma-separated list of global DNS servers to use. Defaults to Cloudflare DNS servers. To use NextDNS, supply the URL like `https://dns.nextdns.io/abc123`.                                                                                                                                      |\n| `HEADSCALE_DNS_SEARCH_DOMAINS`                   | (empty)                                                        | A comma-separated list of search domains. Note that with MagicDNS enabled, tour tailnet base domain is always the first search domain.                                                                                                                                                             |\n| `HEADSCALE_LOG_LEVEL`                            | `info`                                                         | Log level for the Headscale server.                                                                                                                                                                                                                                                                |\n| `HEADSCALE_PREFIXES_V4`                          | `100.64.0.0/10`                                                | Prefix for IP-v4 addresses of nodes in the Tailnet.                                                                                                                                                                                                                                                |\n| `HEADSCALE_PREFIXES_V6`                          | `fd7a:115c:a1e0::/48`                                          | Prefix for IP-v6 addresses of nodes in the Tailnet.                                                                                                                                                                                                                                                |\n| `HEADSCALE_PREFIXES_ALLOCATION`                  | `random`                                                       | How IPs are allocated to nodes joining the Tailnet. Can be `random` or `sequential`.                                                                                                                                                                                                               |\n| `HEADSCALE_EPHEMERAL_NODE_INACTIVITY_TIMEOUT`    | `30m`                                                          | The time after which an inactive ephemeral node is deleted from the control plane.                                                                                                                                                                                                                 |\n| `HEADSCALE_OIDC_ISSUER`                          | n/a                                                            | If set, enables OIDC configuration. Must be set to the URL of the OIDC issuer. For example, if you use Keycloak, it might look something like `https://mykeycloak.com/realms/main`                                                                                                                 |\n| `HEADSCALE_OIDC_CLIENT_ID`                       | n/a, but required if oidc is enabled                           | The OIDC client ID.                                                                                                                                                                                                                                                                                |\n| `HEADSCALE_OIDC_CLIENT_SECRET`                   | n/a, but required if oidc is enabled                           | The OIDC client secret. **Important:** Configure this through `fly secrets set`.                                                                                                                                                                                                                   |\n| `HEADSCALE_OIDC_SCOPES`                          | `openid, profile, email`                                       | A comma-separated list of OpenID scopes. (The comma-separated list must be valid YAML if placed inside `[ ... ]`.)                                                                                                                                                                                 |\n| `HEADSCALE_OIDC_ALLOWED_GROUPS`                  | n/a                                                            | A comma-separated list of groups to permit. Note that this requires your OIDC client to be configured with a groups claim mapping. In some cases you may need to prefix the group name with a slash (e.g. `/headscale`). (The comma-separated list must be valid YAML if placed inside `[ ... ]`.) |\n| `HEADSCALE_OIDC_ALLOWED_DOMAINS`                 | n/a                                                            | A comma-separated list of email domains to permit. (The comma-separated list must be valid YAML if placed inside `[ ... ]`.)                                                                                                                                                                       |\n| `HEADSCALE_OIDC_ALLOWED_USERS`                   | n/a                                                            | A comma-separated list of users to permit. (The comma-separated list must be valid YAML if placed inside `[ ... ]`.)                                                                                                                                                                               |\n| `HEADSCALE_OIDC_ALLOWED_USERS_FLY`               | n/a                                                            | A comma-separated list of users to permit. Takes precedence over `HEADSCALE_OIDC_ALLOWED_USERS` (The comma-separated list must be valid YAML if placed inside `[ ... ]`.)                                                                                                                          |\n| `HEADSCALE_OIDC_EXPIRY`                          | `180d`                                                         | The amount of time from a node is authenticated with OpenID until it expires and needs to reauthenticate. Setting the value to \"0\" will mean no expiry.                                                                                                                                            |\n| `HEADSCALE_OIDC_USE_EXPIRY_FROM_TOKEN`           | `false`                                                        | Use the expiry from the token received from OpenID when the user logged in, this will typically lead to frequent need to reauthenticate and should only been enabled if you know what you are doing. If enabled, `HEADSCALE_OIDC_EXPIRY` is ignored.                                               |\n| `HEADSCALE_OIDC_ONLY_START_IF_OIDC_IS_AVAILABLE` | `true`                                                         | Fail startup if the OIDC server cannot be reached.                                                                                                                                                                                                                                                 |\n\n__Headplane configuration variables__\n\n| Variable                                    | Default                            | Description                                                                              |\n| ------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------- |\n| `HEADPLANE_ENABLED`                         | `false`                            | Enable or disable the Headplane web UI.                                                  |\n| `HEADPLANE_BASE_URL`                        | `https://${HEADSCALE_DOMAIN_NAME}` | Public URL where Headplane is accessible (required for OIDC callback URLs)               |\n| `HEADPLANE_COOKIE_SECRET`                   | (auto)                             | Cookie encryption secret (automatically generated if not provided)                       |\n| `HEADPLANE_PROC_ENABLED`                    | `true`                             | Enable process inspection for network management features                                |\n| `HEADPLANE_OIDC_ISSUER`                     | n/a                                | OIDC issuer URL for Headplane SSO (e.g., `https://accounts.google.com`)                  |\n| `HEADPLANE_OIDC_CLIENT_ID`                  | n/a                                | OIDC client ID for Headplane. **Important:** Should match Headscale's OIDC client ID     |\n| `HEADPLANE_OIDC_CLIENT_SECRET`              | n/a                                | OIDC client secret for Headplane. **Important:** Use `fly secrets set`                   |\n| `HEADPLANE_OIDC_HEADSCALE_API_KEY`          | n/a                                | Headscale API key for OIDC flow. **Important:** Use `fly secrets set`                    |\n| `HEADPLANE_OIDC_SCOPE`                      | `openid email profile`             | OIDC scopes to request                                                                   |\n| `HEADPLANE_OIDC_USE_PKCE`                   | `true`                             | Use PKCE for additional security (requires OIDC provider support)                        |\n| `HEADPLANE_OIDC_DISABLE_API_KEY_LOGIN`      | `false`                            | Disable traditional API key login when OIDC is enabled                                   |\n| `HEADPLANE_OIDC_TOKEN_ENDPOINT_AUTH_METHOD` | `client_secret_basic`              | Token endpoint authentication method (e.g., `client_secret_post`, `client_secret_basic`) |\n\n__Litestream configuration variables__\n\n| Variable                              | Default | Description                                                                                                                                                                                     |\n| ------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `LITESTREAM_ENABLED`                  | `true`  | Whether to restore and replicate the SQlite database with Litestream. You likely never want to turn this option off, as you will loose your SQlite database on restarts.                        |\n| `LITESTREAM_RETENTION`                | `24h`   | Configure the Litestream retention period. Retention is enforced periodically and can be changed with `LITESTREAM_RETENTION_CHECK_INTERVAL`.                                                    |\n| `LITESTREAM_RETENTION_CHECK_INTERVAL` | `1h`    | The interval at which retention should be applied.                                                                                                                                              |\n| `LITESTREAM_VALIDATION_INTERVAL`      | `12h`   | The interval at which Litestream does a separate restore of the database and validates the result vs. the current database.                                                                     |\n| `LITESTREAM_SYNC_INTERVAL`            | `10s`   | Frequency in which frames are pushed to the replica. Note that Litestream's typical default is `1s`, and increasing this frequency can increase storage costs due to higher API request counts. |\n\n__Maintenance variables__\n\n| Variable           | Default | Description                                                                                                                                                                                                                                                                                                   |\n| ------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `ENTRYPOINT_DEBUG` | n/a     | If set to `true`, enables logging of executed commands in the container entrypoint and prints out the Headscale configuration before startup. Use with caution, as it might reveal secret values to stdout (and thus into Fly.io's logging infrastructure).                                                   |\n| `ENTRYPOINT_IDLE`  | `false` | If set to `true`, go idle instead of starting the Headscale server. Will also go idle if an intermediate error occurs. Useful for recovering secrets when the deployment critically fails. Note that after a short time, Fly will turn off the machine since its health check won't be coming online.         |\n| `IMPORT_DATABASE`  | `false` | If set to `true`, the entrypoint will check for an `import-db.sqlite` file in the S3 bucket to restore, and use that instead of `litestream restore` if it exists. Note that the file will not be removed, so you should disable this option and remove the file from the bucket once the import is complete. |\n\n### Migrating to Headscale on Fly.io\n\nTo migrate your existing Headscale instance that uses SQlite to Fly.io, you must upload the database to the S3 bucket\nunder a file named `import-db.sqlite` and temporarily set the `IMPORT_DATABASE=true` environment variable. This will\ninstruct the application to load this database file instead of attempting a Litestream restore on startup. Once done\nand Litestream has finished replicating this database state to S3, you must remove the `IMPORT_DATABASE` environment\nvariable and re-deploy your application, and you should also consider removing the `import-db.sqlite` file from the\nS3 bucket again.\n\nYou should also make sure that you set the `NOISE_PRIVATE_KEY` secret variable to the contents of your original\nHeadscale instance's noise private key.\n\n### Migrating from Postgres\n\n\u003e __Warning__: These steps have been tested on Headscale 0.23.0 only.\n\n  [bigbozza/headscalebacktosqlite]: https://github.com/bigbozza/headscalebacktosqlite/tree/main\n\nIf your current Headscale deployment is using a Postgres database, you must convert it to an SQlite database before\nyou can migrate your instance to Headscale on Fly.io. You can leverage script provided by\n[bigbozza/headscalebacktosqlite] for this, and it is more conveniently made available in this repository in\n[./headscale-back-to-sqlite](./headscale-back-to-sqlite/).\n\nFirst, you need to grab an empty SQlite database that was initialized by Headscale (so all the tables exist with the\nright schemas). You can do this by grabbing it from an initial Fly.io deployment. If your deployment already has some\ndata in it because you did some prior testing, you can set the `LITESTREAM_ENABLED=false` environment variable to not\nuse Litestream and have Headscale start from an empty database (remember to unset this variable again once you have\nretrieved the empty SQlite database).\n\nBecause Headscale is configured to use SQlite in WAL mode, we must first create a WAL checkpoint to ensure that the\ndatabase initialization is committed to the database file.\n\n    $ fly deploy\n    $ fly console ssh\n    app\u003e $ apk add sqlite\n    app\u003e $ sqlite3 /var/lib/headscale/db.sqlite\n    app\u003e sqlite3\u003e PRAGMA wal_checkpoint(TRUNCATE);\n    app\u003e sqlite3\u003e [Ctrl+D]\n    app\u003e $ exit\n    $ fly ssh sftp get /var/lib/headscale/db.sqlite\n\n  [UV]: https://github.com/astral-sh/uv\n\nChange into the [./headscale-back-to-sqlite](./headscale-back-to-sqlite/) directory and use [UV] to run the script.\n\n    $ uv run main.py \\\n        --pg-host db-host.example \\\n        --pg-port 5432 \\\n        --pg-db headscale \\\n        --pg-user headscale \\\n        --pg-password DBPASSWORD \\\n        --sqlite-out path/to/db.sqlite\n\n\u003e This will perform read-only operations on the Postgres database so you do not need to worry about creating a separate\n\u003e backup of your Postgres database.\n\n  [mc]: https://min.io/docs/minio/linux/reference/minio-mc.html\n\nIf all succeeded, upload the database to the S3 bucket that Headscale on Fly.io also uses to replicate the database\nto with Litestream. If you're using the Tigris object storage extension in Fly.io, you will likely need to log into\nthe Tigris console via the Fly.io dashboard and generate some temporary access credentials. The following example uses\nthe [mc] CLI to upload the file.\n\n    $ mc alias set tigris https://fly.storage.tigris.dev \u003cACCESS_KEY_ID\u003e \u003cSECRET_ACCESS_KEY\u003e\n    $ mc cp path/to/db.sqlite tigris/\u003cYOUR_BUCKET_NAME\u003e/import-db.sqlite\n\nSet the `IMPORT_DATABASE=true` environment variable and re-deploy your application.\n\n    $ fly deploy --env IMPORT_DATABASE=true\n    $ fly logs\n\nWait for the application to start, the database to be imported from S3 and Litestream to have replicated it to the\nS3 bucket. Then re-deploy to remove the `IMPORT_DATABASE` variable.\n\n    $ fly deploy\n\nYou should be good to go!\n\n### litestream-entrypoint.sh\n\nAs part of this repository, the [`litestream-entrypoint.sh`](./headscale-fly-io/litestream-entrypoint.sh) can be\nconsidered public API can consumed by other projects that want to use Litestream in the same fashion as this project.\nIt can be retrieved with curl or copied from the container published by the project under the\n`/var/lib/headscale/litestream-entrypoint.sh` path, however you must pin a tagged version to ensure reproducability and\ncompatibility (newer versions might change in a backwards incompatible way).\n\nOther projects that use this script include:\n\n* [NiklasRosenstein/vaultwarden-on-fly](https://github.com/NiklasRosenstein/vaultwarden-fly-io)\n\n## Development\n\nSimply iterating via `fly deploy` works quite well!\n\nTo update the ToC in this file, run\n\n    $ uvx mksync -i README.md\n\n  [GitHub CLI]: https://cli.github.com/\n\nReleases a tagged in the form of `\u003cversion\u003e-headscale-\u003cheadscale_version\u003e`. Requires that the [GitHub CLI].\n\n    $ ./scripts/release 0.1.0-headscale-0.23.0\n\n## Integration testing\n\nWe perform a lightweight integration test by deploying the application to a Fly.io app after successful build on\nthe `main` branch, which will fail if the application doesn't come up healthy.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fniklasrosenstein%2Fheadscale-fly-io","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fniklasrosenstein%2Fheadscale-fly-io","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fniklasrosenstein%2Fheadscale-fly-io/lists"}