Self-host with Docker¶
This page installs the recommended shape: the Docker stack in deploy/ on a box you own. It is for a sysop
with a Linux box; at the end the instance answers on its address and you continue with
Your first hour.
The stack (deploy/docker-compose.yml) runs the gateway, the RF ingest and Caddy on anything that runs Docker:
a Pi at home, a mini-PC, or a cloud VM. Compose builds one image on the box (amd64 or arm64) with the gateway,
the ingest and the built web app inside, so the host needs no Node.js or pnpm.
Before you start¶
- A Linux box with Docker Engine and the Compose plugin 2.24 or newer:
docker compose versionanswers. The compose files use its!resettag and optionalenv_fileentries. - Your callsign and APRS-IS passcode. The wizard asks for both; a blank passcode runs the feed receive-only.
- How people reach the box, one of:
- Caddy with TLS: a DNS name pointing at the box, with ports 80 and 443 reachable from the internet. Caddy fetches the certificate from Let's Encrypt.
- Cloudflare Tunnel: no inbound port and no static IP, for home connections and CGNAT. Create the tunnel first: Set up the tunnel.
- LAN only: plain http on port 80, no internet needed. Members sign in with a one-time link (Off-grid and LAN).
Open only what the stack needs
Caddy publishes 80 and 443 (and 443/UDP for HTTP/3); the gateway's port 8080 stays inside Docker. On a public box, let the firewall admit only those and SSH from addresses you trust. Never open the gateway port or an ingest port.
With the helper¶
From the root of a clone:
It runs the same wizard as the manual steps below, keeps deploy/.env readable by its owner only, and records
the shape for the other helper commands. --net44-config <file> also brings a
44Net Connect tunnel up afterwards. Then start the stack (step 4 below) and check it with
deploy/aprscaching doctor.
Install by hand¶
-
Clone the repository. Start a new installation from a checked release where one exists (Check a download); otherwise:
-
Change into the stack's directory. Every command below runs from
deploy/. -
Write the configuration:
It asks for your callsign, the APRS-IS passcode and filter, how people reach the box, and the callsign-SSID of an RF receiver you operate. It writes
.envwithADMIN_CALLSIGNS,APP_URL,DOMAIN, theAPRSIS_*feed,RF_SITE_CALLandFIRST_PARTY_SITES, and freshINGEST_SECRET,OPERATOR_SECRETandFED_PRIVATE_KEY. The gateway generatesSESSION_SECRETon first start. At the end it prints the start, health and sign-in commands for your choice.A value already in
.envstays unless you confirm the change, and a secret already set is never regenerated, so running it again is safe. For scripts:./setup.sh --non-interactive --call OE8APR --domain aprs.example.net(--helplists every flag). Without Node.js 22 on the host, the wizard cannot generateFED_PRIVATE_KEY; it prints the command that generates it inside the image instead.A public instance gets the safe federation settings written out, and a LAN instance starts with federation off (Running federation safely).
-
Start the stack.
SOURCE_COMMITnames the commit the instance's source link shows, since the image cannot see.git:With a Cloudflare Tunnel, add the tunnel overlay:
SOURCE_COMMIT=$(git rev-parse HEAD) docker compose -f docker-compose.yml -f compose.home.yml up -d --buildThe gateway applies the database migrations when it starts. The first build takes a few minutes.
Check that it worked¶
docker compose psshows the gateway ashealthy.-
The health endpoint answers, through Caddy:
-
deploy/aprscaching doctor(from the root of the clone) checks the rest: DNS, the TLS certificate, the services and the settings.
What runs¶
| Service | Role | Notes |
|---|---|---|
gateway |
Node and SQLite gateway on port 8080 inside the stack | Not published; Caddy proxies to it. The database and cache media (/data/media) live in the data volume. Health check on /health. It refuses to start without INGEST_SECRET |
ingest |
The APRS-IS feed and any RF transports | Starts once the gateway is healthy. It gets .env with the operator secret, the session secret and the federation key blanked: it holds only INGEST_SECRET |
webdist |
One-shot | Copies the web app built inside the image into the volume Caddy serves |
caddy |
TLS, the web app and the reverse proxy | DOMAIN=:80 serves plain http (LAN, or behind the tunnel); DOMAIN=<your host> gets a Let's Encrypt certificate |
cloudflared |
The tunnel connector | Only with compose.home.yml; Caddy then publishes no port |
What each secret guards is in Secrets and credentials.
Every setting in deploy/.env reaches both the gateway and the ingest. The settings from
Your first hour and every radio transport go there too. Apply a
change with docker compose up -d.
Each long-running service restarts unless stopped, and its logs are capped at about 30 MB.
The services run as an unprivileged user¶
The gateway, the ingest and webdist run as the user aprscaching, UID and GID 10001, never as root. The code
in the image is read-only to them. The data and webdist volumes belong to that user: a new named volume takes
the owner from the image, and deploy/aprscaching update hands any file in them that another owner holds to
UID 10001.
-
A host directory instead of the
datavolume (a bind mount such as- /srv/aprscaching:/data) is not changed by Docker. Give it to the user once, before the first start:Without that, the gateway cannot open its database and restarts in a loop;
deploy/aprscaching doctornames the file it cannot write (service.data_owner). -
A serial TNC or a PTT line reaches the ingest as a device: uncomment
devices:underingestindocker-compose.yml. The user is in the groupdialout, GID 20 on Debian, Ubuntu and Raspberry Pi OS. Where the host gives the device another group (stat -c %g /dev/ttyUSB0), add that GID withgroup_add:.
On Oracle Cloud¶
The one-click Oracle Cloud stack starts this same stack on a free Always Free VM: Self-host on Oracle Cloud.
Logs¶
Next¶
- Your first hour: sign in, confirm your call and make the instance public-ready.
- Backups and moving: what to back up, and how.
- Updates: take a new release.