Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- The Documentation and Support links in Settings, About now open the
documentation site.

- Small System Summary tiles now label each figure, as the medium size does.

- The Stats widget is now two widgets, System Summary and Disk Health. Existing
Expand Down
66 changes: 14 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,13 @@
Most dashboards are a wall of numbers and charts. Stackyard is the opposite: a calm, launcher-style grid of app tiles, folders, and a small number of
*genuinely useful* widgets, running in a single container. Built to be glanced at a hundred times a day without feeling cluttered.

## Contents

- [Why Stackyard](#why-stackyard)
- [Widgets](#widgets)
- [Live activity badges](#live-activity-badges)
- [Icons](#icons)
- [Getting started](#getting-started)
- [Security](#security)
- [Building from source](#building-from-source)
- [Contributing](#contributing)
- [Changelog](#changelog)
- [License](#license)
**Documentation: [stackyard.sandobserver.com/docs](https://stackyard.sandobserver.com/docs/)**

## Why Stackyard

- **Attention goes where it's needed, not everywhere at once.** A calm grid, no charts or counters. Health badges only appear when something's wrong.
- **A glance should tell you more than a number would.** Widgets are small visuals, not readouts.
- **Anything can be a badge.** Point Stackyard at any API, pick a value from the response, and show it as a [live activity badge](#live-activity-badges). No custom widget, no code.
- **Anything can be a badge.** Point Stackyard at any API, pick a value from the response, and show it as a [live activity badge](https://stackyard.sandobserver.com/docs/badges/). No custom widget, no code.
- **Configured by clicking, not by editing files.** Everything is set up in the web UI, with config import and export.
- **No dependencies.** Review it once and stop worrying about the supply chain.
- **Six languages, right-to-left included.** Contrast and screen-reader labels are covered by tests.
Expand All @@ -56,25 +45,12 @@ Widgets and the services they read:
- **Backup**: Duplicati, Kopia
- **Connections**: Gluetun, Psiphon Conduit, Netbird, Plausible, Umami

A Glances running in Docker reports its own filesystems, not the host's. Mount
the host paths into the Glances container for a disk slot to read them.

Adding one is a folder plus one registry entry, with no changes to the rest of the app. See [docs/widgets.md](docs/widgets.md).

## Live activity badges

Give Stackyard an API endpoint and it lists the numbers in the response, so you pick the one you want on the tile. Point it at Sonarr's queue and the Sonarr tile carries a count of episodes still downloading. **Show From** sets a floor, so a queue that is never quite empty stays quiet until it matters.

## Icons

App icons resolve automatically by name from the community [dashboard-icons](https://github.com/homarr-labs/dashboard-icons) set. You can also upload your own; custom icons are stored in `./icons`.
Each widget is documented at [Widgets](https://stackyard.sandobserver.com/docs/widgets/). Adding one is a folder plus one registry entry, with no changes to the rest of the app; see [docs/widgets.md](docs/widgets.md).

## Getting started

You need [Docker](https://docs.docker.com/get-started/get-docker/).

**Using Docker Compose:**

```yaml
services:
stackyard:
Expand All @@ -92,41 +68,27 @@ services:
docker compose up -d
```

**Or with `docker run`:**

```sh
docker run -d \
--name stackyard \
--restart unless-stopped \
-p 8700:80 \
-v ./data:/data \
-v ./icons:/icons \
ghcr.io/sandobserver/stackyard:latest
```

**On Unraid:** install it from [Community Apps](https://ca.unraid.net/apps/stackyard-0ara4ku0sjjwqy).

Then open `http://localhost:8700` and set everything up at `/admin`. Config and uploaded icons persist in `./data` and `./icons`.

On Unraid, install it from [Community Apps](https://ca.unraid.net/apps/stackyard-0ara4ku0sjjwqy).

The same image is on Docker Hub as `sandobserver/stackyard`. Prefer `ghcr.io`: it is the registry the [release signature](docs/security.md#verifying-a-release-image) covers.

The repo's [`docker-compose.yml`](docker-compose.yml) is the recommended version: it adds resource limits, dropped capabilities, and commented options for a reverse proxy, host access, and Docker health checks.

Every section of the admin UI is shown in [docs/screenshots.md](docs/screenshots.md).
Full instructions, including building from source, are at [Installation](https://stackyard.sandobserver.com/docs/installation/docker/).

## Security

Stackyard never returns stored secrets to the browser, guards the URLs you test in the admin UI against SSRF and pins the resolved IP, and bounds every upstream call so one slow service cannot hang the dashboard. Some features trade safety for convenience and are opt-in with warnings. Read [docs/security.md](docs/security.md) before exposing Stackyard beyond your LAN.
## Documentation

## Building from source
- [First setup](https://stackyard.sandobserver.com/docs/first-setup/)
- [Settings reference](https://stackyard.sandobserver.com/docs/settings-reference/)
- [Badges](https://stackyard.sandobserver.com/docs/badges/)
- [Customization](https://stackyard.sandobserver.com/docs/customization/)
- [Troubleshooting](https://stackyard.sandobserver.com/docs/troubleshooting/) and [Support](https://stackyard.sandobserver.com/docs/support/)

```sh
git clone https://github.com/SandObserver/stackyard.git
cd stackyard
docker build -t stackyard:local .
```
## Security

Then run `stackyard:local` the same way as above. For working on the code without Docker, see [CONTRIBUTING.md](CONTRIBUTING.md).
Stackyard never returns stored secrets to the browser, guards the URLs you test in the admin UI against SSRF and pins the resolved IP, and bounds every upstream call so one slow service cannot hang the dashboard. Some features trade safety for convenience and are opt-in with warnings. Read [docs/security.md](docs/security.md) before exposing Stackyard beyond your LAN.

## Contributing

Expand Down
32 changes: 0 additions & 32 deletions docs/screenshots.md

This file was deleted.

223 changes: 2 additions & 221 deletions docs/support.md
Original file line number Diff line number Diff line change
@@ -1,224 +1,5 @@
# Support

## Supporting Stackyard
Moved to <https://stackyard.sandobserver.com/docs/support/>.

- Like it? Tell someone about it.
- Found a bug, or have an idea? [Open an issue](https://github.com/SandObserver/stackyard/issues).
- Want to buy me a coffee? https://buymeacoffee.com/sandobserver

## Where to get help

- Bugs and feature requests: https://github.com/SandObserver/stackyard/issues
- Questions and setup help: open an issue with the `question` label.

Search existing issues first.

## Filing a bug report

Include the version (Settings app, About), how you deployed and the image tag,
what you expected and what happened, logs from around the problem, and steps to
reproduce. For anything visual, add your browser and a screenshot.

Redact secrets before pasting logs or config.

## Reading the logs

The API logs to the container's stdout. Nginx logs request errors to stderr and
keeps no access log, so both reach the same place:

```
docker logs <container-name>
```

With Compose:

```
docker compose logs -f
```

In Portainer, open the container and use the **Logs** view.

### What a log line looks like

Every API record is one line:

```
<ISO-8601 UTC timestamp> <LVL> msg=<message> key=value
```

`LVL` is one of `DBG`, `INF`, `WRN`, `ERR` or `AUD`. `AUD` marks a
security-relevant event and is never filtered by the level setting.

- Warnings are emitted at both `warn` and `error`, so choosing Errors in
Settings still shows them.
- The startup banner is not a record. It has no timestamp and no level, and it
always prints.
- Secrets are never logged. Values that could carry one, such as the address of
a service that failed a connection test, are reduced to the host.
- Lines from supervisord and Nginx have their own shapes and are not Stackyard
records.

The format is logfmt, so anything shipping to Loki or Grafana parses it with
`| logfmt` and no custom rules.

To change how much is logged, see the `LOG_LEVEL` row in
[architecture.md](architecture.md#environment).

## Common problems

### I can't log in, or I get bounced back to the login screen

The session cookie sets `Secure` only on HTTPS. Behind a TLS-terminating reverse
proxy, the browser is sent a cookie it refuses to store and login fails
silently. Set `TRUST_PROXY=true` and make the proxy send
`X-Forwarded-Proto: https`. Only set it when a proxy you control is actually in
front of the app; see [security.md](security.md).

Logins are rate-limited to 5 attempts per IP per 15 minutes.

### One person's failed logins lock everyone out

Behind another reverse proxy, Stackyard's nginx sees that proxy as the client, so
every request through it shares one rate-limit bucket. Set `TRUSTED_PROXY` to
where the proxy is, for example `TRUSTED_PROXY=172.18.0.0/16`; see
[security.md](security.md).

### A setting has no effect

Values typed into a Docker UI's environment editor, such as Portainer's stack
environment panel, do not reach the container by themselves. Compose uses them
to substitute `${NAME}` in the Compose file, and passes nothing else through.
The shipped `docker-compose.yml` carries a `${NAME}` line for every operator
setting, so the panel works. A Compose file that hardcodes values ignores the
panel. Confirm what the container actually received:

```
docker exec stackyard env
```

### An upload or a save is rejected as too large

Stackyard limits request sizes so a single request cannot consume the memory of
a small machine:

| What | Limit | Why |
|---|---|---|
| Icon upload | 2 MB | every icon shipped with Stackyard is under 34 KB |
| Config save | 2 MB | a 300-app dashboard is about 155 KB |

nginx allows 3 MB, above both, so the API is always the component that refuses
an oversized request and can say what the limit is.

### A widget shows "Blocked: ... is a private address"

The proxy blocks private and loopback addresses by default as an SSRF safeguard.
Most homelab services live on private IPs, so set `ALLOW_PRIVATE_IPS=true`.

Docker service names (hostnames with no dot, such as `adguard`) are trusted and
are not blocked, so linking to containers on the same network works without it.

### Every app with a container shows as unhealthy

The container list comes from a Docker socket proxy, a separate container you
run yourself. Stackyard only stores its address. When that address cannot be
reached no container is found, and a container that cannot be found counts as
not running, so every app checked by container name turns red at once.

Settings refuses an address that is plainly wrong when you save it, and names the
reason. An address that is accepted but stops working later shows as
`container health fetch failed` in `docker logs`.

Two addresses that look interchangeable are not, and they fail for opposite
reasons.

**A service name**, such as `http://socket-proxy:2375`, is resolved by Docker's
own DNS, which answers only for containers that share a network. Add the proxy's
network to the Stackyard service and the name resolves:

```yaml
services:
stackyard:
networks:
- socket_proxy_network

networks:
socket_proxy_network:
external: true
name: socket_proxy_network
```

A service on `network_mode: bridge` is on Docker's default bridge, which has no
name resolution at all, so no service name can work from there.

**An IP address** reaches only what the proxy published. The usual socket proxy
compose publishes on the host's loopback:

```yaml
ports:
- "127.0.0.1:2375:2375"
```

That port exists on the host and nowhere else. A container's own `127.0.0.1` is
not the host's, and the host's LAN address does not carry the port either, so no
IP reaches the proxy from inside a container. Because packets to it are dropped
rather than refused, this appears as a timeout rather than a connection error.

To use an IP, republish the proxy's port on an interface containers can reach.
The Docker bridge address, usually `172.17.0.1`, reaches containers on that host
without exposing anything to the network:

```yaml
ports:
- "172.17.0.1:2375:2375"
```

Publishing on `0.0.0.0` also works and is worth avoiding. Even with `POST=0` and
`EXEC=0`, it offers an unauthenticated read of every container, image, network
and log on that host to anyone who can reach the port.

Reaching a proxy on a **different host** needs that port published on the host's
LAN address, with the exposure above. Running a socket proxy on each host and
pointing each Stackyard at its own is usually better, since container names on
one host mean nothing on another.

### A widget says "Not configured" or shows an error instead of data

- Confirm the server URL and credentials in the Settings app. Secret fields show
as "set" without revealing the value; leaving them untouched keeps the secret.
- Confirm the container can reach the URL you entered (right network, right
port, no firewall in between).
- For HTTPS services with self-signed certificates, enable the per-widget or
global TLS-skip option.

### A widget briefly shows "Unavailable" then recovers

Widgets keep the last good reading through a transient failure and only surface
an error after repeated failures. A flash that clears on its own means one poll
timed out.

### My dashboards disappeared after a restart

If the config fails to parse on startup, Stackyard copies it to
`apps.json.corrupt` and starts empty rather than overwriting the broken file.
Your previous config is preserved there, in the data volume.

### An icon I re-uploaded still shows the old image

Icons are served with revalidation, so a re-upload should appear on the next
load. If it does not, hard refresh.

### I updated the image but the UI looks the same

UI files ship inside the image. Pull the new image and recreate the container
(in Portainer, redeploy the stack). A browser refresh alone will not do it.

### Container health check is failing

The health check runs through Nginx to the API, so it covers both processes.
Check the logs for either failing to start. A common cause is the data volume
not being writable by the container's `node` user.

## Security issues

Do not post suspected vulnerabilities in public issues. Report them privately to
the maintainer. See [security.md](security.md).
Troubleshooting is at <https://stackyard.sandobserver.com/docs/troubleshooting/>.
4 changes: 3 additions & 1 deletion scripts/dockerhub-description.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ const HEADER = `# Stackyard
Try it: **[demo.sandobserver.com](https://demo.sandobserver.com)**
The first demo visit may take up to a minute due to Render's free-tier cold start.

Source, issues and full documentation: **[github.com/${REPO}](https://github.com/${REPO})**
Documentation: **[stackyard.sandobserver.com/docs](https://stackyard.sandobserver.com/docs/)**

Source and issues: **[github.com/${REPO}](https://github.com/${REPO})**
`;

/** Everything before the first `##` heading, replaced by the markdown header.
Expand Down
Loading