Interactive 3D visualization of Earth's day/night cycle and seasons. Built as a Progressive Web App for classroom use with kids ages 8+. This app is suitably accurate for classroom study of the natural effect of Earth's axial tilt (obliquity). It will become inaccurate in simulations exceeding +-200 years.
- 3D globe and synchronized equirectangular projection showing the day/night terminator, computed from the sun's position using Jean Meeus algorithms
- Includes time controls (smooth sweep and day-snap modes)
- Geographic overlays (coastlines, rivers, lakes)
- Latitude reference lines (equator, tropics, arctic circles) & longitude reference lines
- Per-location sun data (sunrise, sunset, elevation, azimuth)
- Allows changing the Earth's axial tilt from zero to ninety degrees
Use these instructions if this is a new install of the application.
git clone https://github.com/fatcat/ephemeris.git
cd ephemeris
docker compose --buildgit clone https://github.com/fatcat/ephemeris.git
docker build -t ephemeris .After the initial "clone" is performed, the app must be rebuilt if an update from the repo is made, or if any local changes to the code or container configuration are made.
git pull
docker compose build --no-cacheBefore starting the app, SSL certificates must be installed in ./ephemeris/certs. If this is a new clone of the repo, see the section on certificates below. If this is an existing install you should already have certificates in place. Pulling repo updates or performing a docker build does not alter the certificates.
# With Docker Compose
docker compose up -d
# With Docker
docker run -p 8181:8080 -p 8443:8443 -v ./certs:/certs:ro ephemeris# With Docker Compose
docker compose down
# With Docker
docker <container id> downAfter bringing up the container the app will be running at https://x.x.x.x:8443 (HTTP on port 8181 redirects to HTTPS). These ports can be changed by editing docker-compose.yml and running docker compose up -d.
Ephemeris requires HTTPS because the service worker (needed for offline/PWA support) only registers over secure connections. You have two options: self-signed certificates (simpler, good for local/classroom use) or publicly signed certificates (needed if the app is exposed to the internet with a domain name).
The included generate-certs.sh script creates a local Certificate Authority (CA) and a server certificate signed by it:
./generate-certs.sh [hostname] # hostname defaults to "ephemeris"This creates three files in ./certs/:
ca.crt— the CA certificate (install this on client devices)server.crt— the server certificateserver.key— the server private key
The container also serves the CA certificate at https://<host>/ca.crt for easy download.
After generating certs, you must install ca.crt on each device that will access the app so browsers trust the HTTPS connection.
- Download or copy
ca.crtto the device (or navigate tohttps://<host>/ca.crt) - Double-click the
.crtfile - Click Install Certificate...
- Select Local Machine, click Next
- Select Place all certificates in the following store, click Browse
- Choose Trusted Root Certification Authorities, click OK
- Click Next, then Finish
- Restart your browser
Alternatively, an administrator can deploy the certificate via Group Policy. See Microsoft's documentation on distributing certificates using Group Policy.
- Download or copy
ca.crtto the device (or navigate tohttps://<host>/ca.crt) - Double-click the
.crtfile — this opens Keychain Access - When prompted, add it to the System keychain (or login for single-user)
- Find the certificate in Keychain Access (search for "Ephemeris Local CA")
- Double-click it, expand Trust, and set When using this certificate to Always Trust
- Close the window and enter your password to confirm
- Restart your browser
See Apple's documentation on installing a CA certificate on Mac.
- Open Settings > Security and Privacy > More > Manage certificates
- Go to the Authorities tab and click Import
- Select the
ca.crtfile and check Trust this certificate for identifying websites - Click OK and restart Chrome
- Navigate to
https://<host>/ca.crtin Safari and tap Allow to download the profile - Open Settings > General > VPN & Device Management and install the downloaded profile
- Go to Settings > General > About > Certificate Trust Settings and enable full trust for "Ephemeris Local CA"
- Navigate to
https://<host>/ca.crtin Chrome - When prompted, name the certificate and select Wi-Fi as the credential use
- Confirm installation (you may need to set a screen lock if one isn't configured)
If you have a domain name and want real certificates (no CA installation needed on clients), replace the self-signed certs with ones from a public CA like Let's Encrypt.
- Obtain a certificate and key for your domain. With certbot:
sudo certbot certonly --standalone -d yourdomain.example.com
- Copy the certificate and key into the
./certs/directory:cp /etc/letsencrypt/live/yourdomain.example.com/fullchain.pem ./certs/server.crt cp /etc/letsencrypt/live/yourdomain.example.com/privkey.pem ./certs/server.key
- Start or restart the container:
docker compose up -d
No ca.crt is needed — browsers already trust Let's Encrypt. You will need to renew certificates before they expire (every 90 days). See the Certbot documentation for automated renewal options.
If you are behind a reverse proxy (e.g. Traefik, Caddy, or an institutional load balancer) that already terminates TLS, you can skip the certificate setup entirely and proxy directly to the container's HTTP port (8080).
- Svelte 5 (runes mode) + TypeScript
- Three.js (WebGL)
- Vite + vite-plugin-pwa
npm install
npm run dev # dev server with HMR
npm run build # production build
npm run check # type checking (svelte-check)
npm run lint # eslintThe Dockerfile uses a two-stage build:
- Build stage (
node:22-alpine) — installs npm dependencies frompackage-lock.jsonand runsnpm run buildto produce static files indist/ - Serve stage (
nginx:alpine) — serves the static output over HTTPS with TLS 1.2+, security headers, gzip compression, SPA fallback routing, and aggressive caching for Vite-hashed assets. Runs as the unprivilegednginxuser (no root)
Final image size is ~63 MB.
Some bundled data ages over time, albeit slowly.
- Timezone boundaries (
@photostructure/tz-lookup) — IANA timezone data updates a few times per year as countries adjust timezone rules - npm packages — periodic security patches
Updating containers once or twice a year is plenty to keep things current. To update:
npm update # update packages within semver ranges
npm run build # verify the build still passes
npm run check # verify types
docker build -t ephemeris . # rebuild the imageNo external data is fetched at runtime. All textures, timezone data, and solar algorithms are bundled at build time. Rebuilding the image a couple of times per year is sufficient to stay current.
Code is MIT. All textures and geographic data are public domain (Natural Earth, NASA).
