What you will set up
Helium is a privacy-focused browser built on Chromium. Some of its features go through Helium services, a set of small web services:
- extension downloads from the Chrome Web Store, through a proxy, so Google does not see your address;
- the filter lists for Helium's built-in uBlock Origin;
- spellcheck dictionaries;
- the list of search shortcuts, such as
!wfor Wikipedia; - updates for some of the browser's built-in components.
By default they come from Helium's own server. Here they run on yours, so those requests go to a server you control, and the services fetch what they need from the internet with your server's address.
The code is imput's own, from github.com/imputnet/helium-services, built unchanged. It is written to run on a server of its own, with Docker. Here it runs in rootless Podman under a user of its own called helium, behind Caddy from the Podman guide, so it can share a server with your other apps. Four containers take part:
- nginx routes each request to the right service, and serves the dictionaries and search shortcuts itself;
- ubo_proxy serves the filter lists;
- ext_proxy and a spare, ext_proxy_backup, handle extensions.
Every step below was run on a Melonslab server with Debian 13:
- Helium 0.18 on Linux, set to use the server, fetched its filter lists and search shortcuts from it.
- An extension downloaded from the Chrome Web Store through the proxy.
- Every service answered in the same way as Helium's own server, also after a reboot and after an update.
Together, the four containers used about 145 MB of memory.
Before you start
You need:
- a server set up as in the Podman guide, with Caddy running;
- an A record and an AAAA record for
services.example.compointing at your server; - Helium on your computers, from helium.computer.
The examples use services.example.com. Replace it with your own name throughout.
1. Create the user
As root, install Git, which fetches the code, and create the user:
apt install -y git
useradd -m -s /bin/bash helium
loginctl enable-linger helium
machinectl shell helium@
Everything from here to step 5 runs as helium.
2. Download the code
git clone https://github.com/imputnet/helium-services ~/helium-services
mkdir -p ~/.config/containers
The code names some images without saying which registry they come from, such as nginx:1.30.1, which Debian's Podman refuses. Create ~/.config/containers/registries.conf, which tells this user's Podman to look for them on Docker Hub:
# The helium-services Dockerfiles name images like nginx:... without a registry.
unqualified-search-registries = ["docker.io"]
3. Create the certificate and the secret
imput's nginx only accepts HTTPS. Caddy talks to it on the server's loopback address, and needs a certificate for that, which you make yourself. Visitors never see this certificate: Caddy shows them its own, from Let's Encrypt.
mkdir -p ~/helium-certs
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -days 3650 \
-subj "/CN=services.example.com" \
-keyout ~/helium-certs/private.key -out ~/helium-certs/fullchain.pem
The extension proxy signs the download addresses it gives out, so that nobody can use your server to fetch anything else. Create its secret:
openssl rand -hex 32 | tr -d '\n' | podman secret create helium-hmac-secret -
4. Describe the containers
mkdir -p ~/.config/containers/systemd
cd ~/.config/containers/systemd
nginx finds each service by its container name, and looks the names up once, when it starts. The services therefore get fixed addresses on a network of their own, so that nginx can still reach one after it restarts.
Create helium.network:
[Network]
NetworkName=helium
# Fixed addresses: nginx looks the services up once, when it starts.
Subnet=10.90.0.0/24
Create helium-ubo_proxy.container:
[Container]
ContainerName=ubo_proxy
Image=localhost/helium-ubo
Network=helium.network
IP=10.90.0.11
ReadOnly=true
Environment=UBO_PROXY_BASE_URL=https://services.example.com/ubo/
Environment=UBO_USE_ORIGINAL_UBLOCK_ASSETS=0
[Service]
Restart=always
[Install]
WantedBy=default.target
Keep the / at the end of UBO_PROXY_BASE_URL, as imput's example file has it. Without it, the filter list addresses the service gives out lack /ubo/, and every list fails to download.
Create helium-ext_proxy.container:
[Container]
ContainerName=ext_proxy
Image=localhost/helium-ext
Network=helium.network
IP=10.90.0.12
ReadOnly=true
Secret=helium-hmac-secret,type=env,target=HMAC_SECRET
Environment=PROXY_BASE_URL=https://services.example.com/ext
[Service]
Restart=always
[Install]
WantedBy=default.target
Copy it to helium-ext_proxy_backup.container, and in the copy change ContainerName to ext_proxy_backup and IP to 10.90.0.13:
sed -e 's/^ContainerName=ext_proxy$/ContainerName=ext_proxy_backup/' -e 's/^IP=10.90.0.12$/IP=10.90.0.13/' \
helium-ext_proxy.container > helium-ext_proxy_backup.container
nginx sends extension requests to the spare when the first one does not answer. It will not start without both.
Create helium-nginx.container:
[Unit]
Description=Helium services, behind Caddy
After=helium-ext_proxy.service helium-ext_proxy_backup.service helium-ubo_proxy.service
Wants=helium-ext_proxy.service helium-ext_proxy_backup.service helium-ubo_proxy.service
[Container]
ContainerName=helium_nginx
Image=localhost/helium-nginx
Network=helium.network
RunInit=true
ReadOnly=true
Tmpfs=/tmp:size=512m
ShmSize=512m
Volume=%h/helium-certs:/certs:ro
# Only Caddy, on this server, can reach it: the port is not open to the internet.
PublishPort=127.0.0.1:8090:443
[Service]
Restart=always
[Install]
WantedBy=default.target
These are the settings from imput's own compose.yml, apart from the port.
5. Build it and start it
The images are built on your server from the code. One script does it the first time, and later updates the code and rebuilds when it has changed. Create ~/bin/update-helium-services:
mkdir -p ~/bin ~/.config/systemd/user
#!/bin/sh
# Fetch helium-services, and rebuild and restart it when the code has changed.
set -e
cd ~/helium-services
old=$(git rev-parse HEAD)
git pull -q
if [ "$(git rev-parse HEAD)" = "$old" ] && podman image exists localhost/helium-nginx; then
exit 0
fi
podman build -q -t localhost/helium-nginx --build-arg SERVICES_HOSTNAME=services.example.com -f nginx/Dockerfile svc
podman build -q -t localhost/helium-ubo svc/ubo
podman build -q -t localhost/helium-ext svc/extension-proxy
systemctl --user restart helium-ubo_proxy helium-ext_proxy helium-ext_proxy_backup helium-nginx
podman image prune -f
The name goes into the nginx image when it is built, so it is in the script. Make it executable, and run it:
chmod +x ~/bin/update-helium-services
systemctl --user daemon-reload
~/bin/update-helium-services
It builds three images, which took under half a minute on our test server, and starts the four containers. Warnings that HEALTHCHECK is not supported are harmless.
To run it once a week, create ~/.config/systemd/user/update-helium-services.service:
[Unit]
Description=Update helium-services
[Service]
Type=oneshot
ExecStart=%h/bin/update-helium-services
And ~/.config/systemd/user/update-helium-services.timer:
[Unit]
Description=Update helium-services once a week
[Timer]
OnCalendar=weekly
RandomizedDelaySec=6h
Persistent=true
[Install]
WantedBy=timers.target
systemctl --user daemon-reload
systemctl --user enable --now update-helium-services.timer
When nothing has changed, a run takes about a second and restarts nothing.
6. Put Caddy in front
Go back to root with exit, switch to machinectl shell caddy@, and add this block to the end of ~/Caddyfile:
services.example.com {
reverse_proxy https://127.0.0.1:8090 {
header_up Host {host}
transport http {
tls_server_name services.example.com
tls_insecure_skip_verify
}
}
}
nginx only answers requests for its own name. tls_server_name and header_up Host pass that name on: without them, Caddy would use 127.0.0.1:8090, and nginx would answer 404 to everything. tls_insecure_skip_verify accepts the certificate from step 3, which no authority has signed. The connection never leaves the server, as with the other apps behind Caddy.
Restart Caddy with systemctl --user restart caddy, and check from any computer:
curl -s -o /dev/null -w '%{http_code}\n' https://services.example.com/bangs.json
curl -s --compressed https://services.example.com/ubo/assets.json | head -c 300
The first prints 200. The second shows addresses that start with https://services.example.com/ubo/. The dictionaries, at https://services.example.com/dict/, appear a minute or two after each start, as nginx downloads them.
7. Use it in Helium
Finish Helium's setup first, the page it shows when it starts for the first time, and allow Helium services there. Until then, Helium contacts no services at all, and its settings say so.
Then open Settings, Privacy and security and Helium services. Under Use your own instance of Helium services, enter https://services.example.com and press Enter. Helium fetches its filter lists from your server from then on. Install an extension from the Chrome Web Store to check the proxy: the download now goes through your server.
Anyone who knows the address can use your server in the same way, as with Helium's own.
Troubleshooting
Every address answers 404. Caddy is not passing the name on. Check header_up Host {host} and tls_server_name in step 6.
short-name ... did not resolve to an alias when building. The file from step 2, ~/.config/containers/registries.conf, is missing.
Filter lists fail, and their addresses lack /ubo/. UBO_PROXY_BASE_URL is missing its / at the end. Add it, then run systemctl --user daemon-reload and systemctl --user restart helium-ubo_proxy as helium.
502 Bad Gateway. A service is not running. As helium, systemctl --user status helium-nginx helium-ubo_proxy helium-ext_proxy helium-ext_proxy_backup shows which, and journalctl --user -u update-helium-services shows whether the last update failed.