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:

FlagValue hereWhy
--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:

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

Sources

Where the claims on this page that are not measurements of this machine come from: