Gallery Docs
Documentation version
Gallery Docs

Developer guide

The dev container is the supported way to develop the backend (gallery) and web frontend (gallery-ui). It bundles the exact Go/Node/Yarn versions CI uses, plus go vet, gofmt/goimports, golangci-lint, govulncheck, nilaway, the OpenAPI codegen tools (redocly, swagger-typescript-api, oapi-codegen), and the media-processing runtime deps (ffmpeg, mkvtoolnix, yt-dlp) — so what passes locally is what passes in CI, and your host needs only Docker, not a matching Go/Node toolchain. Your working copy is bind-mounted in; nothing is baked into the image, so you edit with your normal host tools and the container just runs it.

Native mobile/TV development (gallery-app, gallery-tv) can’t happen inside the container — it needs Xcode and a simulator/device on your Mac — so those still run on the host directly; see their sections below.

Running the backend/web frontend directly on your host without the container is possible but unsupported: see Local (Non-Container) Setup if you know what you’re doing and want to go that route anyway.

Table of Contents#


Quick Start#

Prerequisites: Docker Desktop (or another docker compose-compatible engine) and a HeroUI Pro CI/CD Token (not your Personal Token — see dashboard), needed to fetch @heroui-pro/react’s Pro components.

git clone <REPO_URL>
cd gallery

echo 'HEROUI_AUTH_TOKEN=your-token-here' >> .env   # repo root, gitignored

./gallery.sh bootstrap   # one-time: creates ./.dev-data/opt-gallery (gitignored) on the host
./gallery.sh dev-up      # pulls (or builds) and starts the dev container
./gallery.sh rebuild     # builds gallery-ui + the backend, (re)starts the server

http://localhost:8080 is now live, serving the real app against your data. Keep editing — after any change, ./gallery.sh rebuild rebuilds and hot-swaps in the new binary/frontend without recreating the container.

bootstrap copies your existing /opt/gallery (config, database, media cache) into a gitignored ./.dev-data/opt-gallery inside the repo, since Docker Desktop on Mac won’t bind-mount arbitrary host paths like /opt/gallery directly. The container mounts that copy back to /opt/gallery internally, so the dev config/DB paths (which are hardcoded to /opt/gallery/..., see configuration.md) work unmodified. If /opt/gallery doesn’t exist on your host yet, bootstrap just creates an empty data dir and the app starts fresh.

dev-up tries to pull the published dev-latest image first and only falls back to building locally if that fails (offline, or no dev image has been published yet — see OpenAPI Codegen note on CI below). Force a local build regardless with ./gallery.sh dev-build (e.g. to pick up a Dockerfile.dev/dev/* change before it’s pushed by CI).

Other Commands#

./gallery.sh dev-status  # shows supervisord's status for the server process
./gallery.sh dev-logs    # tails the server logs
./gallery.sh dev-shell   # drops into a shell in the running container - go vet, fmtcheck, utest, etc. all run here
./gallery.sh stop        # stops the dev container

The server inside the container is kept alive by supervisord (dev/supervisord.conf), which restarts it if it exits or is killed — rebuild uses this (supervisorctl restart gallery) to hot-swap in a freshly built binary.


Configuration#

Everything below is set via the .env file (gitignored) in the repo root — docker compose loads it automatically, so echo 'KEY=value' >> .env is enough for any of these. ./gallery.sh rebuild (and dev-up) recreate the container if .env has changed since it was last created, so an updated value (e.g. a new HEROUI_AUTH_TOKEN) is picked up automatically the next time you run either.

Variable Default What it does
HEROUI_AUTH_TOKEN (empty) HeroUI Pro CI/CD Token — see Quick Start. rebuild checks for this up front and fails fast with instructions if it’s missing.
GALLERY_DEV_PORT 8080 Host port published to the container’s (fixed, hardcoded-in-Go) internal port 8080. Override if something else on your machine already has 8080 bound.
GALLERY_DEV_DATA_DIR ./.dev-data/opt-gallery Host path bind-mounted to /opt/gallery inside the container — where the DB and config.toml persist. ./gallery.sh bootstrap creates this (and copies in an existing real /opt/gallery if one exists) before first use. Override to point at a real /opt/gallery on a Linux host, for example.
GALLERY_DEV_MEDIA_DIR ./.dev-data/media Host path bind-mounted to /config inside the container — where generated album/item files, thumbnails, and caches persist (the default Data.BasePath is /config/gallery/). Without it, that data is lost whenever the container is recreated. Docker creates the directory on first dev-up.
DOCKERHUB_USERNAME mutablemango Combined with CONTAINER_NAME to form the local image tag (<user>/<name>:dev-latest). Only matters if you want the locally-built image tagged to match what CI would push.
CONTAINER_NAME gallery See above.

A few things that are configurable but only by editing files directly (no .env plumbing for these yet):

  • Toolchain versions — Dockerfile.dev pins GO_VERSION (should track gallery/go.mod), NODE_MAJOR, and YARN_VERSION (should track gallery-ui’s packageManager field) as build ARGs with defaults at the top of the file.
  • Named volumes (gallery-ui-node-modules, gallery-app-node-modules, go-mod-cache, etc. in docker-compose.dev.yml) — these shadow node_modules/build caches so a host-built (e.g. macOS) node_modules never leaks into the Linux container. Delete them with docker volume rm (or docker compose -f docker-compose.dev.yml down -v) to force a totally clean reinstall.
  • supervisord behavior (autorestart, log destination) — dev/supervisord.conf.

The dev image itself is published by a separate CI pipeline (build_dev_amd64/build_dev_arm64/create_dev_manifest in .gitlab-ci.yml), triggered by pushing a dev-vX.Y.Z git tag — see the comment block above those jobs for details.


Day-to-Day Workflow#

  1. Edit code on your host as normal (gallery/, gallery-ui/, etc.) — the container only sees your checkout via the bind mount, so there’s nothing to sync.
  2. Run ./gallery.sh rebuild to build gallery-ui and the Go backend inside the container and hot-swap the running server.
  3. Refresh http://localhost:8080.

For quick backend-only iteration without rebuilding the frontend, use ./gallery.sh dev-shell to get a shell in the container, then run go build -o gallery-dev-bin && supervisorctl restart gallery directly from /workspace/gallery.

Everything gallery.sh offers for linting/testing is available from your host — no need to dev-shell in first:

./gallery.sh vet         # go vet
./gallery.sh fmtcheck    # gofmt + goimports check
./gallery.sh lint        # golangci-lint
./gallery.sh utest       # unit tests
./gallery.sh itest       # integration tests

vet, fmtcheck, lint, govulncheck, modcheck, utest, and itest all transparently run inside the dev container (starting it if needed) rather than on your host, so the only prerequisite on your machine is Docker — not a matching Go toolchain, goimports, or golangci-lint. codegen and nilaway always run in the container too, CI or not. (In CI, vet/fmtcheck/lint/govulncheck/modcheck/utest/itest run natively instead, since CI jobs already run in a plain Go image with no dev container available — see gallery.sh’s $CI check.)

These are exactly what CI runs, so a clean run of all five is your best signal a branch is ready to push. A change to gallery isn’t done until all five pass — see Linting below for what golangci-lint checks and how to fix or justify a finding.


OpenAPI Codegen#

The API contract lives in gallery/openapi/. Never edit bundled-openapi.yaml directly — it is generated.

After changing any file in gallery/openapi/, regenerate all clients and Go types:

./gallery.sh codegen

This always runs inside the dev container (starting it if needed) — no need to have redocly/swagger-typescript-api/oapi-codegen installed on your host.

This command:

  1. Bundles the split OpenAPI spec into gallery/openapi/bundled-openapi.yaml via redocly.
  2. Generates the TypeScript API client for the web frontend (gallery-ui/src/GalleryApiClient).
  3. Generates the TypeScript API client for the mobile app (gallery-app/GalleryApiClient).
  4. Generates Go types into gallery/types/api_types.gen.go via oapi-codegen.

After regenerating, run ./gallery.sh rebuild to pick up the changes.


The mobile app uses Expo with React Native and targets iOS and Android. It builds and runs natively on your Mac — it does not run inside the dev container. All commands below are run from the gallery-app/ directory.

Prerequisites#

  • Xcode (from the Mac App Store) with iOS simulators installed, Command Line Tools (xcode-select --install)
  • CocoaPods: sudo gem install cocoapods
  • Yarn/Node installed on your host (see Local (Non-Container) Setup)
cd gallery-app && yarn install

Run on iOS Simulator#

cd gallery-app
yarn ios

This runs expo prebuild (generates the native iOS project), installs CocoaPods dependencies, builds the app, and boots the default iOS simulator.

To choose a specific simulator, pass the --simulator flag:

yarn ios --simulator "iPhone 16 Pro"

List available simulators with:

xcrun simctl list devices available

Run on a Physical iOS Device#

Connect your iPhone via USB (or use wireless pairing from Xcode → Window → Devices and Simulators), then:

yarn ios --device

If you have multiple devices connected, Expo will prompt you to choose one. The device must be registered in your Apple Developer account and the bundle identifier (com.mutablemango.gallery) must be provisioned.

Starting the Metro Bundler Separately#

If you need to run the bundler without triggering a full native build:

yarn start

The app talks to the backend over the network, so point it at your dev container’s address (http://<your-lan-ip>:8080 from a physical device, or http://localhost:8080 from a simulator).


The TV app targets Apple TV using react-native-tvos. It uses the same Expo workflow but the native project builds a tvOS target instead of iOS. Like gallery-app, it runs natively on your Mac, not in the dev container.

All commands below are run from the gallery-tv/ directory.

Prerequisites#

Same as the mobile app — Xcode with tvOS simulators installed, CocoaPods available.

cd gallery-tv && yarn install

Run on tvOS Simulator#

cd gallery-tv
yarn ios

This builds and launches the app in the default Apple TV simulator.

To target a specific simulator:

yarn ios --simulator "Apple TV 4K (3rd generation)"

List available tvOS simulators:

xcrun simctl list devices available | grep -i "apple tv"

Run on a Physical Apple TV#

Connect the Apple TV to the same network as your Mac and pair it via Xcode → Window → Devices and Simulators. Then:

yarn ios --device

As with the mobile app, the device must be registered in your Apple Developer account and the bundle identifier (com.mutablemango.gallerytv) provisioned.

Key Differences from the Mobile App#

gallery-app gallery-tv
react-native package react-native react-native-tvos (aliased)
Target OS iOS / Android tvOS
Orientation Portrait Landscape (forced)
Prebuild step expo prebuild in yarn ios script handled automatically by yarn ios

Backend Testing#

Always use gallery.sh instead of calling go test directly. The script cleans the cache, sets up the right working directories, and — unless $CI is set — runs the tests inside the dev container (starting it if needed), so your host needs only Docker, not a Go toolchain.

Unit Tests#

# Run all unit tests
./gallery.sh utest

# Run a single test by function name
./gallery.sh utest TestFunctionName

# Run all tests in a specific file (path relative to gallery/utest/)
./gallery.sh utest example_test.go

Integration Tests#

# Run all integration tests
./gallery.sh itest

# Run a single test by function name
./gallery.sh itest TestFunctionName

# Run all tests in a specific file (path relative to gallery/itest/)
./gallery.sh itest example_test.go

New tests go in gallery/utest/ or gallery/itest/ — not next to the implementation files. See CLAUDE.md for the full testing policy.


Linting#

./gallery.sh lint

Runs golangci-lint over gallery/, configured by gallery/.golangci.yml. This is separate from ./gallery.sh vet (plain go vet) and ./gallery.sh fmtcheck (gofmt/goimports) — all three are required, and CI runs all three (see lint/vet/fmtcheck jobs in .gitlab-ci.yml) alongside utest/itest on every branch and tag.

The config currently enables golangci-lint’s default linter set:

  • errcheck — flags ignored error return values.
  • govet — the same checks as go vet, run through golangci-lint for consistency with the rest of the report.
  • ineffassign — flags assignments whose value is never read before being overwritten. This has repeatedly caught real bugs in this codebase, not just dead code — variable shadowing that silently swallowed an error (see git log around metadata/tmdb.go and middleware/permissions.go for examples) shows up here first. Don’t dismiss an ineffassign finding as cosmetic without reading the surrounding control flow.
  • staticcheck — a broad static-analysis suite (bug detection, deprecated-API usage, and style/simplification suggestions).
  • unused — flags dead functions, types, and variables. If a finding here is genuinely unused, delete it; don’t suppress it to preserve dead code (see CLAUDE.md).

This is a deliberately moderate starting set — see the comment at the top of gallery/.golangci.yml. Expect it to gain more linters (e.g. gosec, bodyclose) over time as the codebase is cleaned up for each one; don’t add a new linter to the enable list without first running it locally and either fixing or triaging what it finds, the same way this initial set was rolled out.

Test files are exempt from errcheck (gallery/.golangci.yml excludes _test.go, itest/, and utest/), since defer-cleanup calls like resp.Body.Close() or a test server’s Stop() are routinely ignored in test code industry-wide and checking every one is noise, not signal. Every other linter, including ineffassign and staticcheck, still runs on tests.

Fixing a finding:

  • Most errcheck findings on cleanup calls (Close, Remove, RemoveAll) should be made explicit rather than silently allowed to slide: defer func() { _ = f.Close() }(). If the call is on the write side of a file (you just wrote to it) and a logger is already in scope, log the close error instead of discarding it — a failed close on a write path can mean data never made it to disk.
  • Most staticcheck “quick fix” (QF...) and style (S.../ST...) findings are safe, behavior-preserving rewrites — apply the suggested fix as-is.
  • If a finding is a real false positive or requires a design decision out of scope for the current change, suppress it narrowly with a //nolint:<linter> comment on that line, with a short reason (see the ASD-STE100 comment style in CLAUDE.md) — never disable the linter globally to silence one finding.

golangci-lint results are cached; if a fix doesn’t seem to take effect, golangci-lint cache clean (installed as part of ./gallery.sh lint’s toolchain) rules out stale cache before you go looking for a bug in the config.


Local (Non-Container) Setup#

Unsupported, no guarantees — the dev container is what’s tested and what matches CI. Use this only if you specifically need to avoid Docker and you’re comfortable debugging toolchain/version mismatches yourself.

Note that ./gallery.sh vet/fmtcheck/lint/govulncheck/modcheck/ utest/itest always shell out to the dev container (unless $CI is set), and codegen/nilaway always do regardless of $CI — none of them work under this setup. Run go vet, gofmt/goimports, golangci-lint, go test ./utest/..././itest/..., etc. directly instead.

Install via Homebrew:

brew install yarn node golang ffmpeg redocly-cli

Install global Node tools:

yarn global add swagger-typescript-api
yarn global add typescript

Install the Go OpenAPI code generator:

go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest

Add the Go bin directory to your shell’s PATH (add this to ~/.zshrc):

export PATH="$HOME/go/bin:$PATH"

Clone and install:

git clone <REPO_URL>
cd gallery
cd gallery-ui && yarn install && cd ..
cd gallery-app && yarn install && cd ..
cd gallery-tv && yarn install && cd ..

The backend reads its configuration from a TOML file. By convention the development config lives at /opt/gallery/config/config.toml. Create the directory and a minimal config if it does not already exist:

sudo mkdir -p /opt/gallery/config /opt/gallery/data

A minimal /opt/gallery/config/config.toml to get started:

[Server]
Port = 8080
SecureCookie = false

[Database]
Type = "SQLite"

[Database.SQLite]
Path = "/opt/gallery/config/gallery.db"

[Data]
BasePath = "/opt/gallery/"

[Auth]
UseSimple = true
UseSSO = false

[Auth.Simple]
JWTSecret = "change-me-in-production"

See configuration.md for the full reference of every available option.

Run the backend from the repository root:

cd gallery
go run main.go

The server accepts an optional -config / -c flag to point at a non-default config path:

go run main.go -c /path/to/config.toml

Run the web frontend:

cd gallery-ui
yarn dev

The Vite dev server starts on http://localhost:5173 by default and proxies API calls to the backend. To verify the production build:

yarn build

Always run yarn build after UI changes before considering work complete — it runs TypeScript compilation and catches type errors that the dev server may not surface.

Debugging with Delve#

go install github.com/go-delve/delve/cmd/dlv@latest
cd gallery
dlv debug

Set breakpoints inside dlv with:

break middleware/permissions.go:42