No description
  • Python 72.9%
  • HTML 13.3%
  • Nix 9.6%
  • JavaScript 3.4%
  • Handlebars 0.8%
Find a file
Janne Peltola fe8ab83227 Describe the per-instance deployment in the docs
APP.md's deployment section still implied one shared everything; instances now
have their own user, database and state directory, which is what let staging get
an ad server of its own. CLAUDE.md still listed ghost.py, deleted when auth moved
to Authelia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QZMBg7kqYj7HKaP2DmPiE
2026-09-06 11:49:24 +03:00
ads Authenticate the manager through Authelia 2026-09-06 10:40:19 +03:00
ghost/partials First Claude pass 2026-08-30 16:18:06 +03:00
nix Give each instance its own user, state and runtime directory 2026-09-06 11:38:55 +03:00
verde Authenticate the manager through Authelia 2026-09-06 10:40:19 +03:00
.gitignore First Claude pass 2026-08-30 16:18:06 +03:00
APP.md Describe the per-instance deployment in the docs 2026-09-06 11:49:24 +03:00
CLAUDE.md Describe the per-instance deployment in the docs 2026-09-06 11:49:24 +03:00
flake.lock First Claude pass 2026-08-30 16:18:06 +03:00
flake.nix First Claude pass 2026-08-30 16:18:06 +03:00
manage.py First Claude pass 2026-08-30 16:18:06 +03:00
pyproject.toml First Claude pass 2026-08-30 16:18:06 +03:00
README.md Give each instance its own user, state and runtime directory 2026-09-06 11:38:55 +03:00

Verde Adserver

Minimal display ad server for a Ghost site. See APP.md for the spec.

Development

nix develop
verde-dev-db start          # local PostgreSQL in .state/pg
./manage.py migrate
./manage.py runserver 8000  # UI at http://localhost:8000/ghost/ads/
./manage.py test            # integration tests (need the dev database running)
./manage.py test ads.tests.test_serving.ServeTests.test_serves_live_ad   # one test

The dev shell sets VERDE_DEBUG=1 and VERDE_AUTH_BYPASS=1, so the UI needs no Authelia in front and media is served by Django.

Configuration

Everything comes from VERDE_* environment variables:

Variable Meaning
VERDE_DATABASE_URL postgres://user:pass@host/db or postgres:///db?host=/run/postgresql (required)
VERDE_SECRET_KEY / VERDE_SECRET_KEY_FILE Django secret (required unless VERDE_DEBUG)
VERDE_SITE_URL Public Ghost URL; redirect target for stale click links
VERDE_GHOST_ORIGIN Ghost's origin, for CORS on the serving API (default: origin of site URL)
VERDE_ALLOWED_GROUPS Authelia groups allowed to manage ads (default: admins,editors)
VERDE_STATE_DIR Holds media/ (assets) and static/
VERDE_TIME_ZONE Zone for ad start/end days (default $TZ, then Europe/Helsinki)
VERDE_ALLOWED_ORIGINS Extra comma-separated origins allowed to call the serving API
VERDE_ALLOWED_HOSTS Comma-separated ALLOWED_HOSTS override
VERDE_DEBUG, VERDE_AUTH_BYPASS Development only

Deployment

Import nixosModules.default from the flake and declare instances:

services.verde-adserver.instances.prod = {
  siteUrl = "https://example.com";
  databaseUrl = "postgres:///verde-adserver-prod?host=/run/postgresql";
  secretKeyFile = "/run/secrets/verde-prod-secret";
};

Each instance is a gunicorn service on /run/verde-adserver-<name>/server.sock, migrates on start, and purges expired impressions daily. Assets live in /var/lib/verde-adserver-<name>/media.

Instances get their own user and group, verde-adserver-<name>. That keeps two of them from reading each other's assets, and it is what lets each own its own database: PostgreSQL peer authentication maps the system user to the database user, and NixOS only grants ownership when the database shares that name. So name the database after the instance's user. Add Caddy's user to each instance's group, since it serves the assets and opens the socket.

Caddy, alongside the existing Ghost site block. What is public and what is staff-only is decided here: the assets and the serving API must stay anonymous, or no reader ever sees an advert.

handle_path /ghost/ads/media/* {
    root * /var/lib/verde-adserver-prod/media
    header Cache-Control "public, max-age=31536000, immutable"
    file_server
}
handle_path /ghost/ads/static/* {
    root * /var/lib/verde-adserver-prod/static
    file_server
}
# Serving, the render beacon and the click redirect: readers, not staff.
# `handle` takes one matcher, so several paths need a named one.
@ads-public path /ghost/ads/api/* /ghost/ads/c/*
handle @ads-public {
    reverse_proxy unix//run/verde-adserver-prod/server.sock
}
# Everything left is the manager.
handle /ghost/ads/* {
    forward_auth http://127.0.0.1:9091 {
        uri /api/authz/forward-auth
        copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
    }
    reverse_proxy unix//run/verde-adserver-prod/server.sock
}

If Ghost's own admin gate covers /ghost/* on the same host, exclude /ghost/ads/* from it, or it authenticates the serving API before any of this is reached.

Ghost theme

  1. Copy ghost/partials/verde-ad.hbs into the theme's partials/.
  2. Load the script once in default.hbs: <script src="/ghost/ads/static/ads/verde.js" defer></script>
  3. Place ads with {{> verde-ad id="front-sidebar" label="MAINOS"}} and register front-sidebar as a placement in the ad manager at /ghost/ads/.

The manager accepts anyone Authelia signs in who is in the admins or editors group.