End-to-end Docker setup for StashSphere with:
- reproducible backend and frontend Docker image builds
- a ready-to-run
docker-compose.ymlthat uses a hosted image - auto-generated default config on first start (optional but enabled by default)
- GitHub Actions workflow to build and publish a public image to GHCR
-
Reliable Docker build
- Builds the backend binary from upstream StashSphere source (
STASHSPHERE_REF) - Multi-stage image for a lean runtime
- Container entrypoint that can auto-generate config and run migrations
- Builds the backend binary from upstream StashSphere source (
-
Reliable GitHub Action (public image hosting)
- Builds on PRs
- Builds + pushes backend and frontend images to public GHCR on
mainandv*tags - No redeploy hooks, no post-build deployment triggers
-
Runtime deployment with Traefik
- Compose consumes hosted images (
STASHSPHERE_BACKEND_IMAGE,STASHSPHERE_FRONTEND_IMAGE) - Runtime is driven by
.envvariables - Traefik labels included (
entrypoints=https, external networkproxy) with split routers for frontend/backend paths
- Compose consumes hosted images (
-
Config + improvements
- Config bind mount via
./config:/config - Image store bind mounts via:
./data/image_store:/data/image_store./data/image_cache:/data/image_cache
- If no config exists, a ready-to-edit default
config/stashsphere.yamlis generated automatically
- Config bind mount via
Dockerfile– multi-stage build for StashSphere backendDockerfile.frontend– multi-stage build for StashSphere frontenddocker/entrypoint.sh– runtime bootstrap (default config + migration + serve)docker/default-config.yaml– template used for first-start config generationdocker/frontend-entrypoint.sh– generates frontendconfig.json(apiHost) at container startdocker/frontend-nginx.conf– SPA static serving config for the frontenddocker-compose.yml– Traefik-ready runtime stack for hosted image.github/workflows/docker-image.yml– CI build/publish to GHCR.env.example– env template for compose/runtimeconfig/stashsphere.yaml.example– editable non-secret config exampleconfig/secrets.yaml.example– editable secret config example
- Review
.env(already created with placeholders/defaults). - Ensure the Traefik network exists:
docker network create proxy
- Start the stack:
docker compose up -d
- Check logs:
docker compose logs -f frontend backend
On first start, if config/stashsphere.yaml is missing and STASHSPHERE_AUTO_CREATE_CONFIG=true, the container creates it automatically.
The entrypoint expects:
- main config:
/config/stashsphere.yaml(configurable) - optional secrets file:
/config/secrets.yaml
At runtime:
- both files are passed as chained
--confvalues when present - migrations run before
servewhenSTASHSPHERE_AUTO_MIGRATE=true - image paths default to
/data/image_storeand/data/image_cache
The container runs as user stashsphere by default (uid=10001, gid=10001).
So bind-mounted paths like ./config, ./data/image_store, and
./data/image_cache must be writable for that UID/GID.
If your host filesystem blocks writes (for example first boot on some Docker Desktop setups), you can temporarily run as root by setting:
STASHSPHERE_CONTAINER_UID=0STASHSPHERE_CONTAINER_GID=0
After first-start initialization, you can switch back to 10001:10001.
The stack configures two routers:
- Frontend router
stashsphere-frontend- rule:
Host(${TRAEFIK_HOST}) - service port:
80
- rule:
- Backend router
stashsphere-backend-api- rule:
Host(${TRAEFIK_HOST}) && (PathPrefix(/api) || PathPrefix(/swagger) || (PathPrefix(/assets/) && !PathRegexp(^/assets/.*\..*))) - service port:
8081
- rule:
Both use:
- entrypoint:
https - TLS enabled
- certresolver:
${TRAEFIK_CERTRESOLVER}(defaultletsencrypt) - docker network:
proxy
The frontend container injects /config.json with apiHost using STASHSPHERE_PUBLIC_API_HOST (must be a full URL like https://stash.example.com).
Why this matters: frontend bundles are served under /assets/*.js|css. The backend router intentionally excludes file-extension asset paths so static frontend files are not routed to backend and do not trigger 401 responses.
Workflow file: .github/workflows/docker-image.yml
- None to add manually for GHCR in the same repository.
- The workflow uses
${{ secrets.GITHUB_TOKEN }}.
- The workflow uses
- Actions permissions must allow package publishing (workflow already requests
packages: write). - Ensure the published GHCR package visibility is set to public.
BACKEND_IMAGE_NAME(default:stashsphere-backend)- final image:
ghcr.io/<owner>/<BACKEND_IMAGE_NAME>
- final image:
FRONTEND_IMAGE_NAME(default:stashsphere-frontend)- final image:
ghcr.io/<owner>/<FRONTEND_IMAGE_NAME>
- final image:
STASHSPHERE_REF(default:main)- upstream ref to build from (branch, tag, or commit)
- This setup intentionally does not trigger any redeploys after image build.
- If you prefer split config/secrets, copy and edit:
config/stashsphere.yaml.example->config/stashsphere.yamlconfig/secrets.yaml.example->config/secrets.yaml
- For production, set secure domains, SMTP, and a strong DB password.