A small, self-maintaining Docker image for Spotweb,
built on nginx + PHP-FPM (Alpine). It tracks the upstream develop branch and rebuilds only
when upstream actually changes. Deploy files are included for Coolify and for a standalone host
such as a Synology NAS.
The goal is a set-and-forget setup: new upstream commits are picked up, tested and published automatically, and you are only notified when the build breaks.
Published to both registries, multi-arch (linux/amd64, linux/arm64):
ghcr.io/istoutjesdijk/spotwebdocker.io/istoutjesdijk/spotweb
Tags:
latest— the most recent successful build.sha-<shortsha>— the upstream Spotweb commit the image was built from.YYYYMMDD— the build date, useful for pinning and rollback.
Work lands on develop, which is the default branch. main only ever holds commits whose image
has been built, tested and published, and it is the branch Coolify deploys.
upstream-updateruns daily. It compares the upstreamdevelopHEAD against the commit pinned in.upstream-ref. If they differ, it commits the new commit id todevelop.- A push to
developstartsbuild, which builds the image, runs a smoke test, and — only if the test passes — publishes the new image to both registries. It then fast-forwardsmainto the same commit. - Builds run one at a time, so
latestalways matches the commitmainpoints at. - If a build fails, the workflow run is marked failed. GitHub emails the repository owner, and
the workflow also opens (or comments on) a
ci-failureissue. Nothing is published andmaindoes not move, solatestand the deployment keep running the last working image. - Dependabot keeps the workflow actions and the Alpine base image current with monthly pull
requests against
develop.
GitHub does not let the workflow's own token push changes to files under .github/workflows/,
not even as a fast-forward. A commit that changes a workflow therefore builds and publishes as
usual, but the "Promote to main" step fails. Move main by hand once the build is green:
git push origin develop:mainNever commit to main directly. A commit there deploys without having been built or tested, and
the next promotion fails because main is no longer behind develop.
On the deployment side, updates are pulled in one of two ways:
- Standalone with Watchtower: Watchtower polls the registry and pulls + restarts Spotweb when a new image appears.
- Coolify: auto-deploy on the
mainbranch. The push that movesmainarrives after the image is published, so the redeploy picks up the newlatest.
cp .env.example .env # edit MYSQL_PASSWORD at least
docker compose -f deploy/standalone/docker-compose.yml up -dThe UI is served on http://<host>:8081 by default (change SPOTWEB_PORT in .env). This
variant bundles MariaDB and does not update itself.
For automatic image updates, use the Watchtower variant instead:
docker compose -f deploy/standalone/docker-compose.watchtower.yml up -dOn Synology, import either compose file in Container Manager as a new project, place the values
from .env.example in the project's environment, and start it.
The Coolify stack contains only Spotweb and connects to a Coolify-managed database.
- Create a Resource → Database → MariaDB. Note its host, database name, user and password.
- Create a Resource → Docker Compose and paste
deploy/coolify/docker-compose.yml. - In the resource's environment variables, set
SPOTWEB_DB_HOST,SPOTWEB_DB_NAME,SPOTWEB_DB_USERandSPOTWEB_DB_PASSto the database values.SERVICE_FQDN_SPOTWEB_8080makes Coolify assign a domain and proxy HTTPS to port 8080. - Deploy.
Health check: the image exposes /healthz, served directly by nginx, and the compose file
declares a matching healthcheck. Coolify uses it to mark the resource healthy once the web
server is up, independent of database state.
Auto-deploy: point the resource at the main branch with auto-deploy on. Coolify then redeploys
whenever build promotes a tested commit to main, which happens only after its image is
published. Do not point it at develop: that branch moves before the image exists, so Coolify
would pull the previous latest.
The container creates the database schema and a default admin account automatically on first
start, so you do not need the install.php wizard.
- Default login: username
admin, passwordadmin. Change the password immediately under Settings after logging in. - Configure a Usenet (NNTP) provider under Settings before spots can be retrieved.
To reset the admin password from the host if needed:
docker compose exec spotweb su-exec www-data php /var/www/spotweb/bin/upgrade-db.php --reset-password adminThis sets it back to spotweb.
| Variable | Default | Description |
|---|---|---|
TZ |
Etc/UTC |
Container timezone. |
SPOTWEB_DB_TYPE |
pdo_mysql |
Database engine: pdo_mysql or pdo_pgsql. |
SPOTWEB_DB_HOST |
— | Database host. |
SPOTWEB_DB_PORT |
engine default | Database port (3306 for MySQL, 5432 for PostgreSQL). |
SPOTWEB_DB_NAME |
— | Database name. |
SPOTWEB_DB_USER |
— | Database user. |
SPOTWEB_DB_PASS |
— | Database password. |
SPOTWEB_CRON_RETRIEVE |
*/15 * * * * |
Cron schedule for retrieving spots. Empty disables it. |
SPOTWEB_CRON_CACHE_CHECK |
0 4 * * * |
Cron schedule for the cache check. Empty disables it. |
SPOTWEB_FORCE_HTTPS |
false |
Generate https:// URLs regardless of what the proxy reports. See below. |
A /config volume is mounted for persistence. If it contains dbsettings.inc.php or
ownsettings.php, those files take precedence over the environment variables.
Spotweb builds absolute URLs from the scheme the proxy reports in X-Forwarded-Proto,
including the <base href> on every page. A proxy that sets that header correctly needs no
configuration here.
Set SPOTWEB_FORCE_HTTPS=true when TLS is terminated further out than the proxy that talks to
the container, so it forwards X-Forwarded-Proto: http while the browser is on https. A
Cloudflare Tunnel in front of a Coolify http:// domain does exactly that: Traefik replaces the
header the tunnel sent, Spotweb emits http:// asset URLs, and the browser blocks them as mixed
content. Forcing the scheme fixes it without changing the proxy chain.
Upstream Spotweb takes one newsserver per task: retrieving spots and comments, NZB files and images, and posting. This image adds fallback newsservers with a priority, along the lines of SABnzbd. Set them in Spotweb under Settings, Newsservers, below the regular newsservers.
- Priority 0 is highest, 99 lowest. The regular newsserver has priority 0 and goes first on a tie.
- Article fetches by message-id (NZBs, images, full spots, comments) go down the list per article. A server that answers 430 "no such article" or fails passes the article on to the next one, so a block account fills the gaps of a provider with short retention.
- A server that cannot be connected to is skipped for a while: 10 minutes for a timeout or bad credentials, 3 minutes otherwise.
- Header retrieval works with article numbers, and those differ per server. Each run therefore stays on the first server that accepts the newsgroup. Spotweb resyncs by message-id when the next run is on a different server; the first run after a switch takes longer.
- Posting only moves to the next server when connecting or logging in fails, so nothing gets posted twice.
Every failover shows up as NNTP fallback (<task>): ... in the retrieve output. Without fallback
servers, Spotweb behaves exactly as upstream.
The change is written as an upstream contribution: patches/0001-nntp-fallback-servers.patch is
a git format-patch of one commit on upstream Spotweb, with its own PHPUnit tests and
docs/nntp-fallback-servers.md. It can go to spotweb/spotweb as a pull request as it is; once
merged there, the patch can be dropped.
- Standalone: back up the
spotweb-dbvolume (the database holds all spots, comments and settings) and, if you customised it,spotweb-config. - Coolify: use the managed database resource's backup feature.
The image carries local changes on top of upstream as patches in patches/, applied at build
time. If an upstream update stops a patch from applying, the build fails and nothing is
published. Rebase the patch onto the new .upstream-ref commit (git am it in a checkout of
that commit, resolve, git format-patch -1) and push it to develop.
Images publish to GHCR out of the box using the built-in GITHUB_TOKEN; no secret is required.
Set the spotweb package visibility to public in the package settings if you want
unauthenticated pulls.
To also publish to Docker Hub (optional), add under Settings → Secrets and variables → Actions:
- Variable
DOCKERHUB_USERNAME— your Docker Hub username. - Secret
DOCKERHUB_TOKEN— a Docker Hub access token with write access.
Without these, the build still succeeds and publishes to GHCR only.
The workflows request the permissions they need at the job level, so the repository-wide Actions "Workflow permissions" setting does not need to be changed from its default.
- Watchtower and
:latestdeployments update on their own. - To pin a specific version, use a
YYYYMMDDorsha-<shortsha>tag as the image instead oflatest. - To roll back, deploy an earlier date tag; every published build stays available.
docker build --build-arg SPOTWEB_REF="$(cat .upstream-ref)" -t spotweb:test .
SPOTWEB_IMAGE=spotweb:test ./test/smoke.shPass SPOTWEB_REF the way CI does: the patches in patches/ are made against that commit.
The smoke test boots the standalone stack, waits for the container to become healthy, and checks
/healthz, the main page, a clean schema bootstrap and that the patches are in place. The
fallback newsservers have their own PHPUnit tests in the patch; run them in a checkout of
upstream Spotweb with the patch applied (they need the pcntl extension, which the image
leaves out).
Spotweb is developed at spotweb/spotweb. This repository only packages and maintains a container image around it.