Skip to content

What runs where

One server

Everything runs on a single virtual server at Hetzner, managed through Coolify, a self-hosted deployment platform. There is no second environment, no staging server, and no managed cloud services behind any of it.

That is a reasonable choice for a community project of this size, but it means the failure modes are concentrated. If the server is down, everything is down. If the disk fills, everything stops. Both have happened.

Hetzner VPS
  |
  +-- Traefik                 reverse proxy, TLS certificates, routing by hostname
  |
  +-- Coolify                 deployment platform, plus its own database and Redis
  |
  +-- Schoolers app           the Node container built from this repository
  |
  +-- Supabase stack          roughly fifteen containers, see below
  |
  +-- Stalwart                mail server, currently unable to send

Traefik terminates TLS and routes by hostname: the application on one domain, the Supabase gateway on another, the mail dashboard on a third. Certificates renew automatically.

The Supabase stack

Self-hosted Supabase is not one service. The containers that matter for understanding a problem:

ContainerRole
supabase-dbPostgreSQL. All application data
supabase-kongAPI gateway. Everything from the browser enters here
supabase-restPostgREST. Turns tables into the REST API the app calls
supabase-authGoTrue. Sign-up, sign-in, tokens
supabase-storageStorage API for photos and post images
supabase-minioS3-compatible object store backing storage
imgproxyImage transformation, used by storage
supabase-studioThe administrative web interface
supabase-metaDatabase introspection, used by Studio
supabase-analyticsLogflare, log aggregation
supabase-vectorShips container logs into Logflare
supabase-supavisorConnection pooler
realtime-devRealtime subscriptions

Two of these cause confusion often enough to note. supabase-analytics is a hard dependency of Kong and Studio, so when it fails to start, the API gateway and the administrative interface both fail with it, which looks like a much larger outage than it is. And supabase-vector mounts the Docker socket in order to read container logs.

The application database is not called postgres

The production database is named admin, not postgres.

Connecting without specifying a database lands you in the wrong one, where the application's tables do not exist. This has wasted time more than once. Always pass the database name explicitly.

The application container

Built from the Dockerfile in this repository, in three stages: production dependencies are installed in one, the application is built with the full toolchain in a second, and the runtime image copies only the built output and production dependencies into a slim Node image.

The runtime runs serve.mjs, which hosts the fetch handler emitted by the build and serves the built client assets. It listens on port 3000 and binds to all interfaces.

A health check polls the container so that a broken deployment never replaces a working one. See Health checks, which also explains why it must address 127.0.0.1 rather than localhost.

The documentation container

This site is the second application built from the same repository: a VitePress build served by nginx on port 80, from docs/Dockerfile. The image is around 105MB, and being static it costs almost nothing to run.

Resource reality

The server is not large. Two constraints shape decisions:

Disk fills up. Docker images and build caches accumulate, and the disk has reached 83 percent occupancy once already, triggering an alert. Old images need pruning periodically, and automated cleanup should be enabled in Coolify.

There is no CDN. Every image is served from this one server. That is why post images are resized in the browser before upload rather than transformed on the server: the processing happens on the poster's machine, once, instead of on the server for every viewer.

Email does not work

A Stalwart mail server is deployed, but outbound mail is blocked: the hosting provider blocks the standard mail ports by default, and the mail server sits on a different Docker network from Supabase, so container-to-container delivery does not work either.

The consequence is that email verification is disabled and no transactional email is sent. See Authentication for what that means for account security.

Internal engineering documentation.