Running Freenet on a Server
One served instance, written down exactly as it runs here, so you can copy it.
This is not a survey of the options — it is the deployment running on this machine, with the commands and file contents verbatim. It is a Void Linux box, so the service is runit. The parts that are about Freenet rather than about runit are transferable; the init system is not.
Why a VPS is a good home for a peer: it has a public IP and no NAT to punch through, it stays up while your laptop is closed, and your keys and the contract state sit on a machine you control.
First, which Freenet this is about. The name moved in March 2023. The original Freenet — started in 1999, rewritten in 2005 — was spun off as Hyphanet and is still maintained by its own team. The internal redesign begun in 2019 under the name Locutus took the name Freenet. Both projects say so openly (Freenet's history, Hyphanet's announcement), and both say the original maintainers disagreed with the rebrand.
They are not the same software and not compatible, and one difference matters
most: the Freenet at freenet.org does not provide anonymity.
Its own FAQ puts it plainly — "Freenet
does not claim to be an anonymity system and should not be relied on as one." If the
Freenet you have in mind is the one that meant anonymous publishing and censorship
resistance, that is
Hyphanet. This page is about the new
Freenet (freenet-core).
A peer on a rented server is therefore not anonymous either: it has a public IP and a stable identity, and it caches and routes other people's data.
Everything below was run here. Two things on this page were not: the last hop of the tunnel (it needs a browser on your machine) and the exit-42 update path, which has not fired in situ yet — both are marked where they come up. Where a claim comes from the project's own documentation instead, it links to it; the sources are listed at the end.
1. The user and the directories
The node runs as its own unprivileged user and keeps everything under
/var/lib/freenet. Nothing below needs its own partition; the state is
bounded by the settings in step 5.
useradd -r -U -d /var/lib/freenet -s /sbin/nologin \
-c "Freenet node (unprivileged)" _freenet
install -d -o _freenet -g _freenet -m 0755 /var/lib/freenet
install -d -o _freenet -g _freenet -m 0755 /var/lib/freenet/bin
install -d -o _freenet -g _freenet -m 0700 /var/lib/freenet/state
install -d -o _freenet -g _freenet -m 0700 \
/var/lib/freenet/state/config \
/var/lib/freenet/state/data \
/var/lib/freenet/state/logs
install -d -o _freenet -g _freenet -m 0755 /var/log/freenet
bin and /var/log/freenet are the two the node itself
must write to. state holds the identity, so it stays 0700.
2. The binary, in a directory the node can write
Take the statically linked musl build from the
releases page:
it runs on any Linux regardless of the C library version. It lands under
/var/lib/freenet/bin, not /usr/local/bin,
and that is deliberate — see the note after the commands.
base=https://github.com/freenet/freenet-core/releases
tag=$(curl -fsSI -o /dev/null -w '%{url_effective}' "$base/latest" | sed 's#.*/tag/##')
asset=freenet-x86_64-unknown-linux-musl.tar.gz # or ...-aarch64-... on ARM
cd /tmp
curl -fsSLO "$base/download/$tag/$asset"
curl -fsSLO "$base/download/$tag/SHA256SUMS.txt"
grep -F " $asset" SHA256SUMS.txt | sha256sum -c -
tar -xzf "$asset" freenet
install -m 0755 -o _freenet -g _freenet freenet /var/lib/freenet/bin/.freenet.new
mv -f /var/lib/freenet/bin/.freenet.new /var/lib/freenet/bin/freenet
rm -f "$asset" SHA256SUMS.txt freenet
Why the odd path. The auto-updater replaces the binary by writing
a temporary file into the same directory and renaming it over the target — so the
directory has to be writable by the service user. A root-owned
/usr/local/bin/freenet that the node can merely execute is not enough.
After three failed attempts the node locks itself out of updating entirely and
permanently, and says so once, loudly, in a warning that is easy to miss. Renaming
through .freenet.new does the same thing by hand: the running node
keeps its inode, and an interrupted install never leaves a half-written binary.
3. The service: three files
runit starts run, and when that exits it runs finish
with the exit code as its first argument. That is the entire supervisor contract,
and finish is where the auto-update lives.
/etc/sv/freenet/run
#!/bin/sh
# Freenet node under runit.
#
# FREENET_SUPERVISED=1 tells the node that a supervisor will catch its exit
# code 42 ("update needed") and run the updater -- ./finish below does that.
# Without the marker the node warns loudly and the update would be lost.
#
# FREENET_SYSTEMD_FAST_CRASH is deliberately NOT set: that marker claims the
# supervisor understands exit code 45, and ./finish only handles 42. Left
# unset, the node keeps emitting the self-healing 42 for a fast crash.
#
# The settings live here rather than in config.toml, because a hand-written
# partial config.toml is rejected by the parser and because a value that is
# only in the file (ws-api-address in particular) is re-derived on the next
# boot. Flags in the invocation always win.
exec 2>&1
export HOME=/var/lib/freenet
export CONFIG_DIR=/var/lib/freenet/state/config
export DATA_DIR=/var/lib/freenet/state/data
export LOG_DIR=/var/lib/freenet/state/logs
export FREENET_SUPERVISED=1
export FREENET_TELEMETRY_ENABLED=false
PATH=/usr/bin:/usr/sbin:/bin:/sbin
export PATH
cd "$HOME" || exit 1
exec chpst -u _freenet /var/lib/freenet/bin/freenet network \
--ws-api-address 127.0.0.1 \
--ws-api-port 7509 \
--max-hosting-storage 134217728 \
--hosting-disk-pct 0.2 \
--module-cache-budget-bytes 67108864 \
--hosting-mem-share 0.05 \
--min-number-of-connections 10 \
--max-number-of-connections 30 \
--log-level warn
/etc/sv/freenet/finish
#!/bin/sh
# runit runs this after ./run exits, with the exit code as $1.
#
# Exit 42 means: the node verified that a newer release exists and wants to be
# updated. Apply it here; runsv then starts ./run again, with the new binary.
# A zero exit (e.g. `sv down`) is a clean stop and needs nothing.
#
# Any other exit is a crash; runsv restarts the node on its own. If the updater
# cannot reach GitHub, the node itself gives up after a few failed attempts
# instead of demanding an update forever.
echo "freenet: run exited with code ${1:--1}${2:+ (signal $2)}"
if [ "$1" = "42" ]; then
echo "freenet: update requested, applying it"
HOME=/var/lib/freenet
CONFIG_DIR=/var/lib/freenet/state/config
DATA_DIR=/var/lib/freenet/state/data
LOG_DIR=/var/lib/freenet/state/logs
export HOME CONFIG_DIR DATA_DIR LOG_DIR
chpst -u _freenet /var/lib/freenet/bin/freenet update --quiet
echo "freenet: updater exited with code $?"
fi
/etc/sv/freenet/log/run
#!/bin/sh
# The node's stdout/stderr (rate-limit notices, crash lines) into a rotated
# directory; its own tracing logs go to /var/lib/freenet/state/logs.
exec chpst -u _freenet svlogd -tt /var/log/freenet
Then make them executable, cap the log, and enable the service:
chmod 755 /etc/sv/freenet/run /etc/sv/freenet/finish /etc/sv/freenet/log/run
# 1 MiB per file, 10 files kept -- svlogd enforces both on rotation
printf 's1048576\nn10\n' > /var/log/freenet/config
chown _freenet:_freenet /var/log/freenet/config
ln -s /etc/sv/freenet /var/service/
sv up freenet
sv status freenet
Two details in there that are easy to miss and both matter. --quiet
on the update, because without it freenet update tries to restart the
service itself through systemd. And FREENET_SYSTEMD_FAST_CRASH
unset: that marker says "my supervisor understands exit code 45", and
finish above only handles 42 — so leaving it unset keeps the node
emitting the 42 this service knows what to do with.
If you are on systemd instead, you do not need any of this: install the binary,
run freenet service install --system, and check that the unit's user
can write the install directory. The generated unit carries the same contract. And
if you already run containers, the project publishes an
official image that is the
easier route — this page is the native one. Both paths are described on
freenet.org/quickstart.
4. Reach it, and only you
The local API on port 7509 is fully privileged: anything that reaches it can read and write contract state, call delegates, and export your keys. Treat it like SSH. It binds to loopback here by default, and the tunnel is how you use it — this is also what the project itself recommends, as Remote Access to a Node:
ssh -N -L 7509:127.0.0.1:7509 you@your-server
Run that on your own computer, not on the server, then open
http://127.0.0.1:7509/ in your local browser. The
127.0.0.1 in the link is your local one; the tunnel forwards it to
loopback on the server. Because the browser sees a localhost origin, the page counts
as a secure context and features like desktop notifications work with no
certificate and no warning. (This last hop is the one step on this page that a shell
on the server cannot verify for you — on the server side,
curl http://127.0.0.1:7509/ returning 200 is as far as it goes.)
The trap to know about: --allowed-source-cidrs does
not narrow access — it widens it. Because a source filter is meaningless on a
loopback socket, passing that flag (or setting it in the config file) makes the node
bind the API to every interface on its own, and then accept the
ranges you named on top of loopback and all of RFC1918. One line meant for a
Tailscale address can publish your node's full client API to the network your VPS
is plugged into. If you ever want remote access without a tunnel, bind the API to
the private interface explicitly — that is the control, not the CIDR list. The
project documents both the trap and the overlay alternative on the same
remote access page.
Belt and braces, close the port in the firewall as well:
ufw deny 7509 # or the equivalent nftables/iptables rule
On a loopback-only node it changes nothing. It is there for the day a
configuration change makes the node listen on 0.0.0.0.
One honest limitation: all of this restricts control of your node, not the peer-to-peer traffic. Other peers reach it over UDP — that is the point of running one, and there is no switch that turns it off short of local mode, where you are not on the network at all.
5. Budgets, set on purpose
The defaults assume a machine that can spare a gigabyte of state, and this one is
a 2 GB VPS also running mail and a database. So every limit is explicit, in the
run script above. The defaults quoted below are read off
freenet --help on the installed binary, which is the authority for
your own version:
| Flag | Value here | Why |
|---|---|---|
--max-hosting-storage |
134217728 (128 MiB) |
how much contract state the node keeps. Defaults to an eighth of RAM, clamped to 128 MiB–1 GiB — up to 1 GiB you did not plan for. 128 MiB is the floor. |
--hosting-disk-pct |
0.2 |
the disk budget as a fraction of free space, default 0.5. It
counts WASM blobs and the compile cache too, so it has to stay well above
--max-hosting-storage. |
--module-cache-budget-bytes |
67108864 (64 MiB) |
the compiled-WASM cache; the default scales with RAM. |
--hosting-mem-share |
0.05 |
how far an idle node grows its contract count; default 0.125. |
--min/max-number-of-connections |
10 / 30 |
fewer peers, less traffic and memory. |
FREENET_TELEMETRY_ENABLED=false |
off | telemetry is on by default during the alpha: peer activity and general system info. See below — it takes an env var, not a flag. |
--log-level |
warn |
at info the node writes its own log files in the hundreds of
megabytes per day. |
Telemetry is the one setting with a counter-intuitive form: it is
FREENET_TELEMETRY_ENABLED=false in the environment, because
--telemetry-enabled is a bare switch — a value is rejected as an error,
and the bare flag would turn reporting on. The env var sets
telemetry-enabled = false in the node's config file.
6. Two things about the config file
Both cost time here, and both are about where a setting lives:
- A hand-written
config.tomlis rejected. A file with just the keys you care about fails to parse withmissing field 'is_gateway', because most fields have no default. The node writes its own complete file on first start — so pass your settings as flags in the service definition and let it do that. ws-api-addressin the file is thrown away. The merge treats0.0.0.0,::,127.0.0.1and::1as values the node itself could have derived, and re-derives exactly those on the next boot. A loopback bind has to be a flag in the invocation, where it sticks.
Where it writes: config and state under CONFIG_DIR /
DATA_DIR, its own logs under LOG_DIR, and the node
identity — transport keypair, KEK, delegate secrets — in
$DATA_DIR/secrets with mode 0700. Back that directory up if
the identity matters; freenet secrets export is the supported way.
7. Did it work?
# the API is on loopback and nowhere else
ss -tulpn | grep 7509
# tcp LISTEN 0 128 127.0.0.1:7509 0.0.0.0:*
# tcp LISTEN 0 128 [::1]:7509 [::]:*
# it serves the dashboard
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:7509/ # 200
# version, and whether it has been restarting
/var/lib/freenet/bin/freenet --version
sv status freenet # needs root: supervise/ is root-owned
# the log -- no "run exited with code" lines is the healthy state
tail -f /var/log/freenet/current
What you will see plenty of is RATE LIMIT per-callsite. That is the
node throttling its own log output, not an error.
Then the tunnel from your own machine, and the dashboard in the browser.
8. Before you rely on it
- It is alpha software, and it updates itself. The project says
plainly not to use alpha builds for anything sensitive yet. A peer that falls
behind is refused by the network rather than merely missing features, which is
why the update wiring in step 3 is not optional. It has been exercised here by
hand (
freenet update --force, exit 0, binary byte-identical to the official release); what has not happened yet is the real thing — a node running when a new release appears, exiting 42, and being updated byfinish. Until that has happened once, treat it as designed but unobserved. - Root on the box is the node's identity. Anyone with root can read or export the keys in the secrets directory. That is the real cost of running a peer, not the bandwidth.
- Watch the disk. The budgets above bound contract state and the WASM cache — not the node's own log files, and not the release download during an update, which is a full binary of some tens of megabytes.
Sources
Where the claims on this page that are not measurements of this machine come from:
- freenet.org — the project this page is
about, and the home of the
freenet-corepeer. - freenet.org/about/faq — the anonymity statement quoted at the top, the note that the original maintainers disagreed with the rebrand, and the legal position on running a peer.
- freenet.org/about/history — the timeline: 1999, the 2005 rewrite, 2019 Locutus, the March 2023 split.
- hyphanet.org and its about page — the same split from the other side, where the name comes from, and how that project sees it.
- freenet.org/build/manual/remote-access
— the SSH tunnel, the private-overlay alternative, and the
allowed-source-cidrstrap. This page is why the API is loopback-only here. - freenet.org/quickstart and
…/quickstart/docker — the
supported install paths, the
install.shscript, the systemd service, and the official container image. - github.com/freenet/freenet-core — the source, the release assets used in step 2, and where bugs go.
- freenet.org/build/manual — the user manual, for everything this page deliberately leaves out.
freenet --help— not a web page, and deliberately last: the defaults in step 5 come from the binary itself, which is the only source that matches the version you actually installed.