Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from 2d04e45 on 2026-09-15. Licensed under the Functional Source License (FSL-1.1-ALv2).
4.8 KiB
title, section, order, summary, role, keywords
| title | section | order | summary | role | keywords |
|---|---|---|---|---|---|
| Install the Community edition | selfhost | 1 | What the server needs, the four settings that must be set, starting it with Docker Compose, the HTTPS proxy in front, and how to tell it is up. | Self-hosting admin | install, self-host, docker, compose, community edition, server, port, proxy, caddy, https, health check, requirements |
What you need
The Community edition runs as three containers from one docker-compose.yml. Before you start you need:
- A Linux server with Docker and Docker Compose. The README suggests 2 CPU and 2 GB of memory to start.
- A hostname pointing at that server, for example
uniforms.example.health. - An HTTPS reverse proxy in front of it (Caddy, nginx or Traefik).
HTTPS is not optional. The app runs with NODE_ENV=production, and in production the session cookie is marked Secure. A browser on another machine will not send that cookie over plain HTTP, so nobody can sign in.
The three services are:
| Service | Image | What it does |
|---|---|---|
db |
postgres:16-alpine |
The database, in the named volume db |
migrate |
built locally | Runs npx prisma migrate deploy once, then exits |
app |
built locally | The product, on port 3000, photos in the named volume photos |
Clone and set the four settings
git clone https://github.com/pricehq/threadcount-community.git
cd threadcount-community
cp .env.example .env
Open .env and set these four. The rest can stay blank.
| Setting | What to put |
|---|---|
SESSION_SECRET |
A long random string: openssl rand -base64 48. The server refuses to start while it still says change-me. |
POSTGRES_PASSWORD |
Any long password. Compose refuses to start without it. |
NEXT_PUBLIC_SITE_URL |
The address people will type: https://uniforms.example.health. |
EDITION |
community |
NEXT_PUBLIC_SITE_URL is compiled into the app when the image is built, and it is the base of every link in an email (password resets, approval links). If it is blank the build uses http://localhost:3000. If you change it later you must rebuild.
Careful The image build does not read
.env: onlyNEXT_PUBLIC_SITE_URLandNEXT_PUBLIC_TURNSTILE_SITEKEYare passed in as build arguments. OtherNEXT_PUBLIC_values in.envdo not reach a Docker install. See the configuration reference.
Build and start
docker compose up -d --build
The first start builds the image, which takes a few minutes. Compose then starts db and waits for pg_isready, runs migrate to create the schema, and starts app only once migrate has finished without an error.
Watch progress with:
docker compose ps
docker compose logs -f app
If the app stops at once with Refusing to start:, the message names the setting that is missing or unsafe. In the Community edition it checks two: DATABASE_URL and SESSION_SECRET. DATABASE_URL is filled in by docker-compose.yml, so you do not set it.
The port and the proxy
Inside the container the app always listens on 3000. On the server it is published on APP_PORT, which defaults to 3000. Change APP_PORT in .env if 3000 is taken.
Point your proxy at that port. A Caddyfile for Caddy is:
uniforms.example.health {
reverse_proxy 127.0.0.1:3000
}
Docker publishes the port on every interface of the server, not only 127.0.0.1. Firewall it, or set APP_PORT=127.0.0.1:3000 so only a proxy on the same machine can reach it.
The sign-in, sign-up and reset limits count per address. The app reads the address from the last entry of the X-Forwarded-For header, then from X-Real-IP. Caddy sets X-Forwarded-For for you. If neither header arrives, every visitor counts as the same address, and one person's failed attempts count against everyone.
Check it is up
The app answers /api/health without signing in. It asks the database a question and replies:
| Answer | Meaning |
|---|---|
200 with {"ok":true} |
The app is serving and the database answers |
503 with {"ok":false} |
The app is running but cannot reach the database |
curl -fsS https://uniforms.example.health/api/health
The image runs the same check itself every 30 seconds, so docker compose ps shows app as healthy once it passes.
/api/app-info is also public. It returns "product": "threadcount", "edition": "community" and the release in version, which is read from the COMMUNITY_VERSION file. The Android apps call it before they will point at your server.
Next
Open https://uniforms.example.health. The address / sends you to /auth. Go on to first run to create the facility, then set up email and backups before anyone relies on it.