Skip to content

Home server + Cloudflare

The receiver gets a public hostname (https://looku.example.com) through a Cloudflare Tunnel, with no open ports at home. A Cloudflare rule lets through only POST /ingest and POST /orders. The dashboard listens only on the home network. First time? 2. Your first installation on a home server goes through the same steps with explanations.

Needs: Linux with Docker Compose v2 or newer, a domain on Cloudflare, the server’s LAN address (here 192.168.1.10). Commands run on the server in ~/looku/deploy.

Terminal window
cd ~ && git clone https://github.com/niemenjoki/Looku.git looku && cd looku/deploy
cp .env.example .env

In .env:

LOOKU_TIMEZONE=Europe/Helsinki
LOOKU_DASHBOARD_BIND=192.168.1.10
LOOKU_API_HOSTS=192.168.1.10:8788
Setting Why
LOOKU_TIMEZONE time zone of the nightly job and the monthly email (names)
LOOKU_DASHBOARD_BIND the dashboard port is published only on this address; 127.0.0.1 = the server only
LOOKU_API_HOSTS accepted Host headers, against DNS rebinding. Add other names after a comma (server.local:8788); keep the port in step with LOOKU_DASHBOARD_PORT

After any later change to .env: docker compose up -d. All settings: Settings.

Terminal window
mkdir -p data && sudo chown 1000:1000 data
docker compose up -d
docker compose ps

Both containers show Up, after a minute (healthy). Then add the site and the password:

Terminal window
docker compose run --rm ingest site:add myblog --domain example.com --timezone Europe/Helsinki
docker compose run --rm -it ingest admin:password
  • site:add prints Signing key (shown once): and the key. Store it; site:rotate-key myblog replaces it. --timezone decides when a day changes in the site’s reports; site:timezone changes it later (recorded days keep their dates).
  • admin:password asks once, without echo, at least 10 characters. docker compose restart dashboard ends existing logins.
  • Use run --rm ingest for one-off commands. run dashboard may restart the receiver first, because the dashboard depends on it.

The dashboard is at http://192.168.1.10:8788.

In the Cloudflare dashboard (paths checked in November 2026; search for the word in brackets if a menu has moved):

  1. Networking → Tunnels (“Tunnels”) → Create a tunnel, name looku.

  2. Choose Docker and copy the token after --token (starts with eyJ). Do not run the shown command.

  3. On the server: LOOKU_TUNNEL_TOKEN=<token> in .env (the last line), then:

    Terminal window
    docker compose --profile tunnel up -d

    The tunnel shows as connected in Cloudflare within a minute (Cloudflare calls cloudflared a “connector”).

  4. In the tunnel: Routes → Add route → Published application (“Published application”): subdomain looku, domain example.com, path empty, service URL http://ingest:8787.

Your domain → Security → Security rules (“Security rules”) → Create rule → Custom rules. Name Looku: only POST /ingest and /orders, Edit expression:

(http.host eq "looku.example.com" and not (http.request.method eq "POST" and http.request.uri.path in {"/ingest" "/orders"}))

Action Block → Deploy.

Terminal window
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://looku.example.com/ingest
curl -s -o /dev/null -w "%{http_code}\n" https://looku.example.com/
First Second Meaning
401 403 correct: the receiver refused an unsigned request, Cloudflare blocked the rest
403 the rule blocks the receiver too; check the expression
502, 530 the tunnel does not reach the receiver; see Troubleshooting

docker compose --profile tunnel ps shows looku-ingest-1, looku-dashboard-1 and looku-tunnel-1 as Up.

The site’s settings are LOOKU_ENDPOINT=https://looku.example.com, LOOKU_SITE_ID=myblog and the signing key: Connect a Next.js site or Node.js, Express, Cloudflare or your own. Backups: Backups and updates. Dashboard away from home: Tailscale.

About the author