Deployment guide

Wakapi

Overview

Wakapi is an open source, self-hosted coding statistics server that speaks the WakaTime API. You keep using the standard WakaTime plugins for Visual Studio Code, JetBrains IDEs, Vim, Neovim, Sublime Text and dozens of other editors. You only point them at your own server.

Wakapi turns the "heartbeats" those plugins send into dashboards of the time you spend per project, language, editor, operating system and machine. It also provides daily and ranged summaries, project pages, badges and a WakaTime-compatible REST API. Your coding history stays inside your own Azure subscription.

This guide covers deploying and using Wakapi 2.18.2 on Ubuntu 24.04 LTS from SecureAnyCloud on Microsoft Azure.

What you get:

  • HTTPS from the first boot. nginx serves Wakapi on port 443 with a certificate generated on your VM's first boot. Port 80 redirects to HTTPS, and Wakapi's session cookies are marked Secure.
  • An administrator made for your VM, nobody else. The first boot creates an admin administrator with a password and an API key generated on your VM, before nginx or Wakapi accept a single connection. It writes them to /var/lib/sac/data/wakapi-credentials.txt (root only). It also generates the VM's password salt and the key that signs its session cookies. No account, password or key is baked into the image.
  • Closed to strangers. Public sign-up and invite codes are off, so nobody can register. Visitors who are not signed in land on the login page, and the public leaderboard is off.
  • Your plugins keep working. WakaTime plugins authenticate with your API key on /api, so there is no extra password prompt in front of the API. A heartbeat with a wrong key is refused.
  • Outbound requests limited to WakaTime. Wakapi can forward your heartbeats to a WakaTime account. On this image Wakapi can reach api.wakatime.com (and wakapi.dev) and nothing else, so a user cannot make the server fetch an address of their choosing. Data imports are off until you enable them (Step 8).
  • No route to the Azure metadata service. Wakapi, nginx and the first-boot service are blocked at the kernel level from the Azure Instance Metadata Service and the WireServer, even if you later allow more outbound hosts. The Azure VM agent keeps its access.
  • Cross-site requests refused. nginx refuses a form post from another website. It also refuses a heartbeat from another site that relies on your browser's session instead of an API key, and another site's attempt to trigger the links that change a setting. Compressed request bodies are refused.
  • Features you do not use are closed. The built-in API explorer (/swagger-ui, which bundles an old copy of the DOMPurify library), OpenID Connect sign-in, the anonymous plugin error report, metrics and subscriptions answer 404.
  • The host name comes from configuration, never from the request. Wakapi's public URL is set on first boot from the VM's public IP address (read from the Azure Instance Metadata Service). nginx refuses requests for host names the VM does not serve. Wakapi always sees the real client address, whatever forwarding headers a request carries, so its login rate limit applies to the real client.
  • A current, checked build. Wakapi 2.18.2 includes the fix for GHSA-x48w-3rq3-w2pq (a pre-authentication account takeover fixed in 2.17.6). The upstream release binary was built with an older Go and network library that govulncheck flags. SecureAnyCloud therefore builds the 2.18.2 source tag with Go 1.27.3 and the fixed golang.org/x/net, reproducibly, and govulncheck finds no vulnerable code in it.
  • Hardened defaults. Wakapi runs as its own unprivileged user with no capabilities and a read-only view of the system except its data directory. nginx and Wakapi refuse to start until first boot has finished.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Wakapi 2.18.2 (/usr/local/bin/wakapi, with its web UI built in, listening on 127.0.0.1:3000 only), nginx, SQLite, Ubuntu 24.04 LTS, cloud-init and the Azure Linux agent.

  • Default user: the admin user you choose (azureuser in the examples)
  • Architecture: x64, Gen2
  • Recommended size: Standard_B2s

Prerequisites

  • An Azure subscription, an SSH public key, and a virtual network and subnet in your region.
  • A network security group that allows TCP 22 (SSH), TCP 443 (Wakapi) and TCP 80 (redirects to HTTPS) from your own networks only. Include the networks your developers' editors send heartbeats from.
1

Deploy from the Azure portal

Search the Azure Marketplace for Wakapi, choose the SecureAnyCloud offer and select Create.

Pick your subscription, resource group and region. Choose size Standard_B2s, because Wakapi is light and a few developers need very little. Add an SSH public key for the admin user, and allow inbound ports 22, 80 and 443. Then select Review + create.

2

Deploy from the Azure CLI

RG="wakapi-rg"; LOCATION="eastus"; VM_NAME="wakapi1" IMAGE_URN="sac1647283583153:wakapi-ubuntu-24-04:default:latest" az group create --name "$RG" --location "$LOCATION" az vm image terms accept --urn "$IMAGE_URN" az vm create --resource-group "$RG" --name "$VM_NAME" --image "$IMAGE_URN" \
  --size Standard_B2s --admin-username azureuser --ssh-key-values ~/.ssh/id_rsa.pub \
  --public-ip-sku Standard az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 443 --priority 1001 az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 80 --priority 1002

az vm create opens port 22 for you. If you can, narrow the rules to your own networks with az network nsg rule update --source-address-prefixes <your-cidr>.

3

Connect and check the services

ssh azureuser@<public-ip>

First boot reads the VM's public IP from the Azure Instance Metadata Service. It generates the certificate, the password salt, the cookie key and the administrator, and then Wakapi and nginx start:

systemctl is-active wakapi nginx wakapi-firstboot wakapi-address
sudo test -f /var/lib/sac/data/wakapi-firstboot.done && echo FIRSTBOOT_DONE

All four units are active and the first-boot marker is present:

active active active active FIRSTBOOT_DONE

Only SSH and nginx listen on the network. Wakapi itself listens on 127.0.0.1 only. Check the listeners:

sudo ss -lntp | awk 'NR>1 {print $4, $6}' | grep -v '127.0.0.5' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort
0.0.0.0:22 users:(("sshd"),("systemd")) 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx")) 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx")) 127.0.0.1:3000 users:(("wakapi")) [::]:22 users:(("sshd"),("systemd")) [::]:443 users:(("nginx"),("nginx"),("nginx")) [::]:80 users:(("nginx"),("nginx"),("nginx"))
4

Get your URL, password and API key

wakapi-address.service reads the VM's addresses from the Azure Instance Metadata Service on every boot. For a Standard SKU public IP, it uses the load balancer view, which is the only view that shows it. It is the one service of the image that may reach the metadata service, and it runs no Wakapi code:

cat /run/sac-wakapi/address.env
WAKAPI_PUBLIC_IP=<public-ip> WAKAPI_PRIVATE_IPS="<private-ip>"
sudo cat /var/lib/sac/data/wakapi-credentials.txt

Only root can read the file. It holds the URL, the administrator's user name, this VM's password and the administrator's API key:

# Wakapi 2.18.2 - generated on first boot by wakapi-firstboot.service
# This administrator was created on THIS VM; no account was baked into the image. Keep this file secret and
# change the password after your first sign-in (Settings > Account). WAKAPI_URL=https://<public-ip>/ WAKAPI_ADMIN_USER=admin WAKAPI_ADMIN_PASSWORD=<WAKAPI_ADMIN_PASSWORD> WAKAPI_API_KEY=<WAKAPI_API_KEY>
#
# Open WAKAPI_URL in a browser (self-signed certificate minted on this VM's first boot; port 80 redirects to # https) and sign in. Point your editor's WakaTime plugin at this server in ~/.wakatime.cfg:
# [settings]
# api_url = https://<public-ip>/api
# api_key = <WAKAPI_API_KEY>
# (with this image's self-signed certificate also add: no_ssl_verify = true, or install a CA-issued certificate) # Public sign-up and invite codes are OFF. Forwarding to WakaTime reaches api.wakatime.com only; imports are off. # Self-test:
sudo wakapi-selftest
# Change the name it is served as:
sudo wakapi-set-hostname <your-dns-name>
# User guide: https://secureanycloud.com/stacks/wakapi/

The URL uses the VM's public IP address. The certificate also names the private address, and requests for the private address are answered too. To serve Wakapi under your own DNS name, see Step 10.

5

Sign in

Open the WAKAPI_URL in your browser. The certificate is self-signed. It is generated on this VM and is a server certificate that cannot sign other certificates, so your browser warns about it the first time. Before you accept it, compare the fingerprint your browser shows with the one the VM prints:

sudo openssl x509 -in /etc/ssl/wakapi/wakapi.crt -noout -fingerprint -sha256
sha256 Fingerprint=<64 hex digits in pairs, separated by colons>

Accept the certificate only if the two match. Do not add it to your operating system's or browser's trust store. For a certificate your users' machines trust without a warning, use a CA-issued certificate for your own DNS name instead (Step 10).

Sign in as admin with the password from Step 4. The Sign up button is still drawn on the page, but registration is switched off. Wakapi answers "registration is disabled on this server".

Then open Settings, and change the password under Account to one only you know.

6

Connect your editors

Install the WakaTime plugin for your editor. The WakaTime plugin list covers VS Code, JetBrains IDEs, Vim, Neovim, Sublime Text, Xcode and many more. Then point the plugin at your Wakapi server instead of WakaTime. On each workstation, edit ~/.wakatime.cfg (on Windows %USERPROFILE%\.wakatime.cfg):

[settings] api_url = https://<your-vm-address>/api api_key = <WAKAPI_API_KEY> no_ssl_verify = true

You need no_ssl_verify only while the VM uses its self-signed certificate. Remove it once you install a CA-issued certificate (Step 10). Wakapi's Setup page shows the same snippet for the account you are signed in as.

You can check the API from the VM itself. This sends one test heartbeat with the administrator's API key and reads it back. The key is read from the credentials file into a temporary file and is never typed on a command line:

H=$(sudo sed -n 's#^WAKAPI_URL=https://\([^/]*\)/#\1#p' /var/lib/sac/data/wakapi-credentials.txt)
CFG=$(mktemp); chmod 600 "$CFG" printf 'header = "Authorization: Basic %s"\n' "$(sudo sed -n 's/^WAKAPI_API_KEY=//p' /var/lib/sac/data/wakapi-credentials.txt | tr -d '\n' | base64 -w0)" > "$CFG" curl -sk -K "$CFG" --resolve "$H:443:127.0.0.1" -o /dev/null -w 'heartbeat: %{http_code}\n' \
  -H 'Content-Type: application/json' -A 'wakatime/v1.130.1 (linux) curl-test' \
  --data "[{\"entity\":\"/home/dev/hello/main.go\",\"type\":\"file\",\"time\":$(date +%s),\"project\":\"hello-wakapi\",\"language\":\"Go\",\"category\":\"coding\"}]" \
  "https://$H/api/heartbeat"
TODAY=$(curl -sk -K "$CFG" --resolve "$H:443:127.0.0.1" "https://$H/api/compat/wakatime/v1/users/current/heartbeats?date=$(date -u +%F)") rm -f "$CFG" grep -o '"project":"hello-wakapi"' <<<"$TODAY" | sort -u
heartbeat: 201 "project":"hello-wakapi"

Each person who uses the server should have their own account and API key (Step 8). You can list, add and revoke API keys under Settings > API Keys.

7

Your dashboard

Once your editors send heartbeats, Dashboard shows the totals and breakdowns for the interval you pick (today, this week, the past 7 days, a custom range and more). It breaks them down by project, language, editor, operating system, machine, label and category. The filters at the top narrow everything to one project, language or machine.

Projects lists every project you worked on, most recent first. Choose one to see its own statistics.

8

Users, forwarding to WakaTime and imports

The image ships as single-user: sign-up, invite codes and imports are off. The settings live in /etc/wakapi/config.yml:

sudo grep -E '^ (public_url|allow_signup|invite_codes|import_enabled|trust_reverse_proxy_ips|insecure_cookies|disable_frontpage):' /etc/wakapi/config.yml
public_url: https://<public-ip> import_enabled: false insecure_cookies: false allow_signup: false invite_codes: false disable_frontpage: true trust_reverse_proxy_ips: 127.0.0.1
  • Add teammates with invite codes. Set invite_codes: true and restart Wakapi (sudo systemctl restart wakapi). Settings then shows an Invite Friends button that generates a single-use invite link, valid for 24 hours. Send the link to a colleague, who registers with it while open sign-up stays off. Avoid allow_signup: true unless the VM is reachable only from a private network.
  • Forward your heartbeats to WakaTime. Under Settings > Integrations, enter your WakaTime API key and choose Connect. Wakapi then relays each heartbeat to api.wakatime.com as well, so you can use both.
  • Forward to another server, or import data. Wakapi's outbound requests are limited to the hosts in NO_PROXY in /etc/wakapi/egress.env. To forward to your own Wakapi or WakaTime-compatible server, or to import history from WakaTime (whose data export downloads from a separate storage host), add the host names, comma-separated, to NO_PROXY. For imports, also set import_enabled: true and add the host to import_hosts_whitelist in config.yml. Then run sudo systemctl restart wakapi. Even then, Wakapi cannot reach the Azure metadata service or the WireServer. That block is in the kernel, not in the allowlist.
9

Run the self test

The image has a self test. It checks HTTPS, that anonymous visitors and sign-up are refused, and that guessed passwords are refused. It then signs in, sends a heartbeat with the API key, reads the heartbeat back and checks that a wrong key is refused. Finally, it removes its test heartbeat and checks the listening ports:

sudo wakapi-selftest
SELFTEST-OK https://<public-ip>/ : anonymous refused, sign-up refused, guessed passwords refused, admin signed in, heartbeat stored + read back with the API key (wrong key 401), listeners 22/80/443
10

Your own DNS name and certificate

Point a DNS name at the VM's public IP address (an Azure public IP can carry a DNS label such as <label>.<region>.cloudapp.azure.com). Then set it as the name Wakapi is served as. This command writes the name into the nginx configuration and Wakapi's public URL, generates a new self-signed certificate for it and restarts Wakapi:

sudo wakapi-set-hostname <your-domain>

To use a certificate from your own certificate authority (or one you obtained for the name), replace /etc/ssl/wakapi/wakapi.crt and /etc/ssl/wakapi/wakapi.key. Keep the key at mode 0600, owner root. Then reload nginx with sudo systemctl reload nginx.

Running wakapi-set-hostname again generates a new self-signed certificate, so install your own after it. Once a CA-issued certificate is in place, remove no_ssl_verify from the workstations' ~/.wakatime.cfg. Never add the VM's self-signed certificate to a trust store.

11

Check the security posture from the command line

H=$(sudo sed -n 's#^WAKAPI_URL=https://\([^/]*\)/#\1#p' /var/lib/sac/data/wakapi-credentials.txt) c() { curl -sk --resolve "$H:443:127.0.0.1" --resolve "$H:80:127.0.0.1" -o /dev/null "$@"; } c -w 'port 80: %{http_code} -> %{redirect_url}\n' "http://$H/" c -w 'API without a key: %{http_code}\n' "https://$H/api/summary?interval=today" c -w 'heartbeat with a wrong key: %{http_code}\n' -u 'not-a-key:' -H 'Content-Type: application/json' --data '[]' "https://$H/api/heartbeat" c -w '%{http_code}\n' -X POST --data 'username=someone&password=secret123&password_repeat=secret123' "https://$H/signup" | sed -E 's/^(403|429)$/sign-up: refused/' c -w 'cross-origin form post: %{http_code}\n' -X POST -H 'Origin: https://evil.example' "https://$H/login" c -w 'API explorer (/swagger-ui): %{http_code}\n' "https://$H/swagger-ui/" c -w 'compressed request body: %{http_code}\n' -X POST -H 'Content-Encoding: gzip' --data 'x' "https://$H/api/heartbeat" curl -s -o /dev/null -w 'unknown host name: %{http_code} (connection closed)\n' -H 'Host: other.example' http://127.0.0.1/ || true
port 80: 301 -> https://<public-ip>/ API without a key: 401 heartbeat with a wrong key: 401 sign-up: refused cross-origin form post: 403 API explorer (/swagger-ui): 404 compressed request body: 415 unknown host name: 000 (connection closed)

The sign-up attempt is refused with 403 ("registration is disabled"), or with 429 once the sign-up rate limit for your address is reached.

Wakapi, nginx and the first-boot service cannot reach the Azure metadata service or the WireServer. Wakapi's outbound requests go only to the allowed hosts:

for u in wakapi nginx wakapi-firstboot; do echo "$u denies: $(systemctl show -p IPAddressDeny --value $u | tr ' ' '\n' | sort | xargs)"; done
sudo grep -E '^(HTTPS_PROXY|NO_PROXY)=' /etc/wakapi/egress.env
wakapi denies: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128 nginx denies: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128 wakapi-firstboot denies: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128 HTTPS_PROXY=http://127.0.0.1:9 NO_PROXY=api.wakatime.com,wakapi.dev

HTTPS_PROXY points at a port where nothing listens, so any request to a host that is not in NO_PROXY fails before a connection is made. The metadata block applies even to a host you add to NO_PROXY. You can see the kernel block yourself. The same request to the metadata service, run in a throwaway unit with the same rule, gets no answer:

sudo systemd-run --quiet --wait --pipe -p IPAddressDeny="169.254.169.254/32 168.63.129.16/32" curl -s -m 5 -o /dev/null -w 'metadata from a denied unit: %{http_code}\n' -H 'Metadata: true' 'http://169.254.169.254/metadata/instance?api-version=2021-02-01' || true
metadata from a denied unit: 000
12

Server components

  • Wakapi binary: /usr/local/bin/wakapi
  • Configuration: /etc/wakapi/config.yml (root:wakapi, mode 0640)
  • Per-VM salt and cookie key: /etc/wakapi/wakapi.env (root, mode 0600)
  • Outbound allowlist: /etc/wakapi/egress.env
  • Database (SQLite): /var/lib/wakapi/wakapi.db
  • Service units: wakapi.service, nginx.service, wakapi-firstboot.service, wakapi-address.service
  • nginx site: /etc/nginx/sites-available/sac-wakapi (closed routes: /etc/nginx/wakapi-closed.conf)
  • Certificate and key: /etc/ssl/wakapi/
  • First-boot script: /usr/local/sbin/wakapi-firstboot.sh
  • Address reader: /usr/local/sbin/wakapi-address (writes /run/sac-wakapi/address.env)
  • Host name helper: /usr/local/sbin/wakapi-set-hostname
  • Self test: /usr/local/sbin/wakapi-selftest
  • Credentials: /var/lib/sac/data/wakapi-credentials.txt (mode 0600)
/usr/local/bin/wakapi -version command -v go || echo "no Go toolchain" nginx -v 2>&1 sed -n '1,5p' /usr/share/doc/sac-wakapi/BUILD-INFO.txt
2.18.2 no Go toolchain nginx version: nginx/1.24.0 (Ubuntu) wakapi 2.18.2 (tag 2.18.2, commit 1157f203c8b646466b4e72563764738b10f0cd0d), MIT - built on the build instance from the tag https://github.com/muety/wakapi/archive/refs/tags/2.18.2.tar.gz (sha256 1c77089b75b6e88dbb984708d0a03476867913e29c61b09bf783063336e73e1e) go1.27.3 (official tarball, sha256 65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40; not shipped), CGO_ENABLED=0, GOEXPERIMENT=greenteagc,jsonv2, -trimpath, -ldflags '-w -s'; binary sha256 7a121ee6531fa3c2259f7e3300714db4deab9880b9e189fc30f00978584f9156 (reproducible: identical to the binary audited off-instance)

/usr/share/doc/sac-wakapi/BUILD-INFO.txt records the source and toolchain checksums, the dependency update, the bundled web libraries and the govulncheck result. The files go.mod, go.sum and govulncheck-wakapi-source.txt sit beside it.

Backups: Wakapi keeps everything in the SQLite database /var/lib/wakapi/wakapi.db on the OS disk. Use Azure Backup for the VM, or take a snapshot of the OS disk. For a consistent copy, run sudo systemctl stop wakapi, take the snapshot, then run sudo systemctl start wakapi.

SecureAnyCloud made these changes to the operating system:

  • SSH does not allow root logins, empty passwords or X11 forwarding. Password sign-in follows the choice you made when you created the VM.
  • The root account has no password.
  • The firmware metadata refresh timer (fwupd-refresh) is masked, because an Azure VM has no updatable firmware.
  • Core dumps are off for the Wakapi services.
  • The golden image's boot-time updater is removed.
  • Unattended security upgrades are on.
13

Security notes and recommendations

  • Allow ports 22, 80 and 443 in the network security group from your own networks only.
  • Change the administrator password after your first sign-in (Step 5), keep the credentials file secret, and give each person their own account and API key (Step 8).
  • Use your own DNS name and a CA-issued certificate (Step 10), then drop no_ssl_verify from the plugins' configuration. Until then, check the self-signed certificate's fingerprint (Step 5) instead of trusting it.
  • Send API keys in the Authorization header, as the WakaTime plugins do, instead of as an api_key= URL parameter. Request URLs are written to the nginx and Wakapi logs.
  • govulncheck lists one advisory for a module in Wakapi's build whose vulnerable package Wakapi never uses, so no vulnerable code is reachable: GO-2026-5932 (the golang.org/x/crypto/openpgp package is unmaintained, and there is no fixed version).
  • The built-in API explorer bundles DOMPurify 2.3.10. nginx closes its /swagger-ui route on this image, so use the API reference at https://wakapi.dev/swagger-ui/ instead. SecureAnyCloud rechecks both items at each Wakapi release.
  • Keep the operating system patched. Unattended security upgrades are on, and sudo apt-get update && sudo apt-get upgrade applies everything else.

Support and licensing

Wakapi is open source under the MIT licence (Ferdinand Mütsch and contributors). The licence text is in /usr/share/doc/sac-wakapi/, and the source of this exact release is at https://github.com/muety/wakapi/tree/2.18.2. Its built-in web UI includes open source libraries under the MIT licence and Iconify under the Apache 2.0 licence.

WakaTime is a trademark of its owner. Wakapi and this image are not affiliated with or endorsed by WakaTime. There is no per-user or per-CPU licence fee. The image price covers SecureAnyCloud's build, hardening, first-boot setup and 24/7 support.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com
  • Support: 24/7, with a 24 hour response SLA

Deploy on Azure

Launch Wakapi 2.18.2 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure