Guide
Build a station from scratch
What you build
| piece | choice |
|---|---|
| Sensors | Ecowitt WS90 7-in-1 array |
| Gateway | Ecowitt GW2000A, polled over the LAN API |
| weewx | 5.4.0, pip-installed into a venv |
| Driver | gjr80 Ecowitt Gateway driver (GW1000) |
| Database | SQLite, the weewx default |
| Archive interval | 300 s (5 minutes) |
| Units | metricwx |
| Runtime | Docker Compose, two containers |
| Web server | nginx, serving a shared volume |
| Dashboard | Nordlys, at the site root |
Nothing here is required by Nordlys. It is a skin for weewx, so it works with any station weewx supports - this is simply a setup that is known to work end to end, written down so it can be copied. If weewx is already running, skip to the last section.
The hardware
The WS90 is a single mast-mounted unit carrying temperature, humidity, wind, light, UV and a haptic piezo rain sensor. No moving parts, so nothing to seize up over a winter, and one thing to mount rather than five. It talks over 868 MHz to the GW2000A gateway indoors, which puts the readings on your LAN.
The gateway can push to a collector, but this setup polls it instead, over the local API on port 45000. That means weewx makes the connection outbound and nothing needs to reach the machine, so there are no ports to publish and nothing to expose.
The weewx image
weewx is installed with pip into a virtual environment on a slim Python base. Pinning the version matters: this is a thing that runs unattended for years, and a surprise upgrade at rebuild time is not what you want on the morning a storm rolls through.
Dockerfile
FROM python:3.12-slim-bookworm
ARG WEEWX_VERSION="5.4.0"
ENV WEEWX_HOME="/home/weewx-data"
RUN apt-get update && \
apt-get install -q -y --no-install-recommends wget ca-certificates tzdata && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# weewx timestamps everything in local time; set the zone explicitly.
RUN ln -sf /usr/share/zoneinfo/Europe/Oslo /etc/localtime
WORKDIR ${WEEWX_HOME}
RUN python3 -m venv ${WEEWX_HOME}/weewx-venv && \
. ${WEEWX_HOME}/weewx-venv/bin/activate && \
python3 -m pip install --no-cache-dir weewx==${WEEWX_VERSION} six==1.17.0Station identity
weectl station create writes the initial weewx.conf. It is created with the simulator driver and switched to the real one afterwards, because the gateway driver runs an interactive discovery step that has no place in a build.
Dockerfile
RUN . ${WEEWX_HOME}/weewx-venv/bin/activate && \
weectl station create "${WEEWX_HOME}" --no-prompt \
--driver=weewx.drivers.simulator \
--altitude="20,meter" \
--latitude=66.365413 \
--longitude=13.034191 \
--location="Aldersundet, Lurøy, Norway" \
--register="y" \
--station-url="https://weather.example.com/" \
--units="metricwx"- Altitude is the sensor height above sea level, not the mast height. weewx uses it to reduce station pressure to sea level, so a wrong figure makes barometer readings wrong.
- Units.
metricwxgives °C, mm, m/s and hPa - the sensible metric mix. Remember that every threshold you later set in the skin is read in these units. - Registration is optional.
--register=ylists the station on weewx.com and needs a reachable--station-url.
The archive interval is left at the weewx default of 300 seconds. Nordlys reads it and times its own auto-refresh to match, so pages update shortly after each new record.
weewx extensions
weewx is deliberately small; extensions add the rest. These are installed from source archives, with the unmaintained ones pinned to a specific commit rather than a branch.
| extension | what it adds |
|---|---|
weewx-gw1000 | The Ecowitt gateway driver. Required. |
weewx-forecast | Forecast data. Nordlys computes Zambretti itself, so this is optional. |
weewx-cmon | Host metrics: CPU, memory, disk, network. |
weewx-xaggs | Extra aggregation types for templates. |
weewx-xcumulative | Cumulative aggregates. |
weewx-GTS | Growing degree days and related agricultural sums. |
Dockerfile
# gjr80's account is gone; the community fork is pinned at a known commit.
ARG GW1000_REF="a01a4b47ce3b39fab39d4e364f1f9ac1324cb89c"
RUN wget -nv -O weewx-gw1000.zip \
"https://github.com/weewx-contrib/weewx-gw1000/archive/${GW1000_REF}.zip"
RUN . ${WEEWX_HOME}/weewx-venv/bin/activate && \
weectl extension install -y --config "${WEEWX_HOME}/weewx.conf" /tmp/weewx-gw1000.zipPoint weewx at the gateway
Three changes to weewx.conf, applied with configobj so they are order-independent and do not trigger the driver’s interactive discovery.
Dockerfile
RUN . ${WEEWX_HOME}/weewx-venv/bin/activate && python3 - <<'PY'
import configobj
c = configobj.ConfigObj("/home/weewx-data/weewx.conf", encoding="utf-8")
# 1. Use the gateway driver, polling the LAN API.
c["Station"]["station_type"] = "GW1000"
gw = c.setdefault("GW1000", {})
gw["driver"] = "user.gw1000"
gw["ip_address"] = "192.168.1.50" # your gateway
gw["poll_interval"] = "20"
# 2. Take rain from the WS90 piezo sensor rather than the absent tipping bucket.
fme = gw.setdefault("field_map_extensions", {})
fme["rain"] = "p_rain"
fme["rainRate"] = "p_rainrate"
# 3. Log to stdout so 'docker logs' shows something.
log = c.setdefault("Logging", {})
log.setdefault("handlers", {})["console"] = {
"level": "INFO", "formatter": "standard",
"class": "logging.StreamHandler", "stream": "ext://sys.stdout",
}
log.setdefault("root", {})["handlers"] = ["console"]
c.write()
PYThe piezo rain mapping
This is the step that costs people an afternoon. The WS90 senses rain acoustically rather than with a tipping bucket, and the gateway reports it in separate fields - p_rain and p_rainrate. Without the field map extension, weewx reads the traditional rain field, finds nothing, and records a permanent zero. Every rain tile and every rain-day count then quietly reads zero while everything else looks fine.
Logging to stdout
weewx logs to syslog by default, which in a container means the logs go nowhere you will look. docker logs weewx showing nothing is not a great place to start debugging a driver that will not connect.
Install Nordlys
The release ships pre-built assets, so there is no Node build step. Pin the version and bump it deliberately.
Dockerfile
ARG NORDLYS_VERSION="0.4.2"
RUN . ${WEEWX_HOME}/weewx-venv/bin/activate && \
wget -nv -O /tmp/weewx-nordlys.zip \
"https://github.com/TheEskil/weewx-nordlys/releases/download/v${NORDLYS_VERSION}/weewx-nordlys-${NORDLYS_VERSION}.zip" && \
weectl extension install -y --config "${WEEWX_HOME}/weewx.conf" /tmp/weewx-nordlys.zipThe installer registers a NordlysReport and writes into a nordlys/ subdirectory. To make it the site root instead, point HTML_ROOT at public_html and set the SEO base URL so og:url, og:image and the sitemap resolve against the right origin.
weewx.conf
[StdReport]
[[NordlysReport]]
skin = Nordlys
HTML_ROOT = public_html
[[[Nordlys]]]
[[[[seo]]]]
base_url = https://weather.example.comThen disable the skins you are replacing, so weewx is not rendering a second dashboard nobody looks at on every cycle:
weewx.conf
[StdReport]
[[SeasonsReport]]
enable = falseRun it
Two containers and two named volumes. weewx writes the archive database to one and the generated HTML to the other; nginx mounts the HTML volume read-only.
docker-compose.yml
services:
weewx:
build: .
image: weewx:local
# No published ports: the driver polls outbound, nothing pushes in.
volumes:
- weewx-db:/home/weewx-data/archive
- weewx-html:/home/weewx-data/public_html
restart: unless-stopped
weewx-web:
image: nginx:1.27.5
ports:
- 8181:80
volumes:
- weewx-html:/usr/share/nginx/html:ro
restart: unless-stopped
volumes:
weewx-db:
weewx-html:The entrypoint activates the venv and runs weewx in the foreground, so the container lives and dies with the process and Docker’s restart policy does what you expect.
start.sh
#!/bin/bash
set -e
. "${WEEWX_HOME}/weewx-venv/bin/activate"
exec weewxd --config "${WEEWX_HOME}/weewx.conf"Serve the HTML
Nordlys generates a complete static site - real HTML files, a stylesheet, a script bundle, a service worker and a manifest. It needs nothing but a static file server, which is why the stock nginx image with no configuration is enough.
Putting a reverse proxy in front for a hostname and a certificate is the usual next step, and entirely up to you. Whatever you use, set the resulting public URL as base_url under [[seo]] so social cards and the sitemap point at the right place.
Verify
weewx writes a record every archive interval, but you do not have to wait for one to see the dashboard:
docker compose logs -f weewx # driver connected, records archived?
docker compose exec weewx \
/home/weewx-data/weewx-venv/bin/weectl report run \
--config /home/weewx-data/weewx.conf- A page with data but no rain, ever, on a WS90 means the piezo field mapping did not take.
- Empty tiles for sensors you do not own are correct: absent observations hide themselves.
- A barometer reading that looks off is usually the altitude.
- “Today” boundaries in the wrong place means the container timezone is still UTC.
Already running weewx?
Then none of the above applies and the whole install is two commands. Download the release, install it as an extension, and render once:
weectl extension install weewx-nordlys-0.4.2.zip
weectl report runThe report lands in HTML_ROOT/nordlys/ and regenerates on each archive interval. Everything after that is configuration.