Skip to content

Self Hosting

Trent Bauer edited this page Jul 12, 2026 · 2 revisions

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.com staying 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

Docker Compose

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.

Adding default profiles (optional)

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.yaml

Refresh 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).

Format

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.

Customizing formats and processes (advanced)

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).

Clone this wiki locally