-
Notifications
You must be signed in to change notification settings - Fork 0
Self Hosting
Since all your saved film and lab profiles live in your browser's localStorage, the
live version works perfectly well for most people — there's
nothing wrong with just using it. You might prefer self-hosting if you:
- Want to seed the app with a fixed set of default film stocks and labs (e.g. for a household, club, or team to share a common starting point)
- Don't want to depend on
filmcalc.trentbauer.comstaying online, or want a version you control the uptime of - Run it on a local network / homelab alongside your other self-hosted tools, without needing internet access to use it
- Want to inspect or modify the source yourself with full control over the deployment
services:
app:
image: ghcr.io/trentnbauer/filmcalc:latest
ports:
- ${WEBPORT:-8080}:80
volumes:
# Only config.yaml lives in the named volume — mounting the whole html
# directory here would shadow index.html and everything else baked
# into the image with an empty volume on first run.
- config:/usr/share/nginx/html/config.yaml
restart: unless-stopped
labels:
- filmcalcheal=true
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 5s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
autoheal:
image: willfarrell/autoheal:latest
restart: unless-stopped
environment:
- AUTOHEAL_CONTAINER_LABEL=filmcalcheal
volumes:
- /var/run/docker.sock:/var/run/docker.sock
volumes:
config:By default the app runs on port 8080 — set a WEBPORT environment variable (or a .env file) if
you'd like a different port.
The healthcheck pings the app every 30 seconds and marks it unhealthy after 3 failed attempts. On
its own, Docker's restart policy only restarts a container on a hard crash — it won't act on a
failed healthcheck. The autoheal sidecar watches for any container labelled filmcalcheal=true and
force-restarts it if Docker marks it unhealthy, so a broken/unresponsive app container gets rebooted
automatically.
This app doesn't ship with any default film stocks, labs, or settings baked in — it just starts empty
and lets you build up profiles via the UI, which are saved in your browser's localStorage.
If you'd rather seed it with defaults (e.g. for a shared/self-hosted instance), copy your own
config.yaml straight into the running container:
docker compose cp config.yaml app:/usr/share/nginx/html/config.yamlRefresh the page and the app will pick it up automatically — no restart or rebuild needed. Anyone using the app can also do this themselves from Settings → Write Live Config to Server, which appears automatically once the app detects the endpoint supports it (it's hidden entirely on the static GitHub Pages build).
settings:
upgradeThresholdPercent: 10 # see "Recommended Pick Threshold" in the Wiki
films:
- name: Kodak Gold
boxSpeed: 200
maxPushPull: 1 # optional; defaults to 1. Warns in Film Lookup if pushed/pulled further than this
process: C41 # optional; C41 (default), BW, E6, or ECN2 — only matched against lab tiers of the same process
format: 35mm # optional; defaults to 35mm — one of 35mm, 120, 110, 127, 220, sheet
hidden: false # optional; true hides it from every calculator while keeping it manageable in the Library
bundles: # one entry per pack size — a single roll, a 3-pack, a bulk 10-pack are three bundles under one film
- rolls: 1
exposures: 36
filmCost: 24.95
storeName: Example Camera Store # optional; shown in the buy link
buyLink: https://www.example.com/kodak-gold-200 # optional
- rolls: 10
exposures: 36
filmCost: 210
storeName: Example Bulk Store
buyLink: https://www.example.com/kodak-gold-200-bulk
labs:
- name: Irohas Melbourne
hidden: false # optional
address: 123 Example Street, Melbourne VIC 3000, Australia # optional; powers the Directions link — needs to be Google-Maps-findable
phone: (03) 1234 5678 # optional
email: hello@example.com # optional
website: https://example.com # optional
services: # one entry per service tier — not one per lab
- devCost: 17
pushPullCost: 5
pushPullType: per_stop # or flat
turnaroundTime: next_day # next_day | same_week | longer
highResScan: true # marks this tier as offering high-res scans
noPushPull: false # true only if this tier can't push/pull at all
processes: # list — a tier can cover more than one process at the same price
- C41
- name: Walkens Melbourne
services:
- devCost: 16
pushPullCost: 10
pushPullType: flat
turnaroundTime: same_week
highResScan: false
processes: [C41]
- devCost: 33
pushPullCost: 10
pushPullType: flat
turnaroundTime: next_day
highResScan: true
processes: [C41]All three top-level keys (settings, films, labs) are optional — include only what you want to
seed. settings.upgradeThresholdPercent acts as a factory default for the Recommended Pick Threshold
and only applies if the person hasn't already changed it themselves. This is the exact same format
Settings → Export Everything writes inside the app, so the easiest way to hand-author one is to
build it in the UI first and export it.
The film formats and dev processes offered throughout the app (dropdowns, lab service-tier buttons)
come from options.yaml, baked into the image alongside index.html. To add a format or process the
built-in list doesn't cover, fork the repo, edit options.yaml, and build your own image — or, for a
quick unofficial test on a running container, docker compose cp a modified copy the same way as
config.yaml above (note it isn't volume-mounted, so it reverts to the image's baked-in version if
the container gets recreated).