Skip to main content

RSSHub Production Shadow Deployment

What this phase proves​

RSSHub is the VPS's first real application workload. It has a public HTTPS name, a working real route, access control, and monitoring, but it remains a shadow deployment because no production consumer has moved:

Public canary
`-- HTTPS -> Caddy -> infra-edge -> VPS RSSHub

existing feed consumer
`-- local RSSHub -> existing production path and fallback

This proves the server path before Phase 4 changes the consumer. A failed shadow instance can be removed from ingress without interrupting the existing consumer.

Responsibility map​

ComponentResponsibilityExplicit non-responsibility
Authoritative DNSResolve the RSS subdomain to the VPSDoes not proxy application traffic or manage containers
CaddyOwn public 80/443, TLS, redirects, and proxyingDoes not store the raw RSSHub ACCESS_KEY
infra-edgeGive Caddy and RSSHub an internal path and service discoveryDoes not publish port 1200 to the host or Internet
RSSHubFetch sources, produce RSS, and protect real routesDoes not store consumer data
Uptime KumaCheck public /healthz without a keyContains no ACCESS_KEY and exposes no public management UI
Local RSSHubContinue serving the current feed consumerIs not stopped or removed in Phase 3

The public route is:

rss.example.com
|
v
Caddy :80/:443
|
v
infra-edge
|
v
rsshub-prod:1200

Only Caddy publishes ports on public host addresses; Uptime Kuma publishes TCP 3001 only on 127.0.0.1, and RSSHub publishes no host port. RSSHub's 1200/tcp exists inside Docker; neither the public nor Tailscale host address accepts a direct connection to it.

Why the deployment stays small​

The representative Bilibili route already returned valid RSS and non-zero items on the local RSSHub without Redis, Browserless, or Chromium. The VPS therefore starts with the same minimum:

  • the official RSSHub image pinned by immutable digest;
  • CACHE_TYPE=memory;
  • no Redis;
  • no Browserless or Chromium;
  • no database or application data volume.

An optional service appearing in an upstream example is not evidence that a personal single-node workload needs it. Add one only after a required route proves the dependency.

Files and secret boundary​

The /srv/rsshub/ directory needs only:

  • compose.yaml for the reviewable image, health check, network, and restart policy;
  • .env with mode 600, never committed or expanded in reports.

The env file contains only the required variables: a newly generated ACCESS_KEY and the UID/cookie pair required by the representative Bilibili route. Variable names may be documented; values must not enter chat, Git, the website, or shared Compose output.

The following excerpt shows the image, environment, restart policy, and network connection; it is not the complete deployment configuration. It omits the container health check and Caddy configuration discussed below. Those must be configured before using the later checks that expect running healthy and a public /healthz response:

services:
rsshub:
image: diygod/rsshub@sha256:<immutable-digest>
restart: unless-stopped
env_file: [.env]
environment:
NODE_ENV: production
CACHE_TYPE: memory
DISALLOW_ROBOT: "1"
networks:
infra-edge:
aliases: [rsshub-prod]

networks:
infra-edge:
external: true

There is intentionally no ports:, host networking, privileged mode, Docker socket, database, or persistent application volume.

Health and access control are different jobs​

Real RSS routes require a valid ACCESS_KEY and return 403 without it. Monitoring must remain keyless so Kuma does not store a high-value credential.

In the RSSHub access-control implementation referenced here, setting ACCESS_KEY also protects /healthz. A route-bound code is the MD5 digest of the request pathname followed by the access key. This deployment computes the /healthz code in the container healthcheck and gives Caddy that derived code instead of the raw key.

GET /healthz -> 200, caller supplies no key
GET /real/rss/route -> 403
GET /real/rss/route?key=<valid-key> -> 200

The public health request still traverses Caddy, infra-edge, and RSSHub; Caddy does not fabricate a static response. The derived code value is not published either.

Routine checks​

On the VPS:

[REMOTE: VPS]
cd /srv/rsshub
docker compose ps
docker compose logs --tail 100 rsshub

RSSHUB_ID=$(docker compose ps -q rsshub)
docker inspect "$RSSHUB_ID" \
--format '{{.State.Status}} {{.State.Health.Status}} {{json .HostConfig.PortBindings}}'

docker network inspect infra-edge

The accepted result is running healthy, a binding map of {}, and both Caddy and RSSHub on infra-edge.

From WSL2:

[LOCAL: WSL2]
curl --fail https://rss.example.com/healthz
curl -I http://rss.example.com/healthz

# Both must time out or refuse; neither may return RSSHub content.
curl --connect-timeout 5 --max-time 7 \
"http://$VPS_PUBLIC_IPV4:1200/"
curl --connect-timeout 5 --max-time 7 \
"http://$TAILSCALE_IPV4:1200/"

A real-route check should report only status, feed format, channel-identity match, and item count. Never paste a full URL containing the key, private UID, or cookie into logs or notes.

Controlled restart​

Phase 3 does not reboot the whole VPS. Restart only the new application:

[REMOTE: VPS]
cd /srv/rsshub
docker compose restart rsshub
docker compose ps

After health returns, recheck public health, unauthenticated 403, the authenticated feed, Caddy, Kuma, Tailscale SSH, and public-SSH blocking. Caddy and Kuma container IDs and start times should not change during an RSSHub-only restart.

Rollback​

A healthy deployment does not need a destructive rollback demonstration. The mechanical sequence is:

  1. Restore Caddyfile from /srv/caddy/Caddyfile.pre-rsshub-p3.
  2. Validate the complete restored Caddy configuration.
  3. Reload Caddy without restarting its container.
  4. Run docker compose -f /srv/rsshub/compose.yaml down.
  5. Keep /srv/rsshub/compose.yaml and the protected .env.
  6. Leave the local RSSHub running; remove DNS later only if desired.

Never use docker system prune -a, volume prune, or network prune for this rollback.

What has not happened​

Phase 3 did not change the feed consumer's production endpoint, persistent data, scheduler, or source configuration, and it did not stop the local RSSHub. Phase 4 must treat consumer cutover, persistent-data backup and restore, scheduler migration, and fallback retirement as one controlled change.

Explore connectionsOpen network