- Python 72.9%
- HTML 13.3%
- Nix 9.6%
- JavaScript 3.4%
- Handlebars 0.8%
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 |
||
|---|---|---|
| ads | ||
| ghost/partials | ||
| nix | ||
| verde | ||
| .gitignore | ||
| APP.md | ||
| CLAUDE.md | ||
| flake.lock | ||
| flake.nix | ||
| manage.py | ||
| pyproject.toml | ||
| README.md | ||
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
- Copy
ghost/partials/verde-ad.hbsinto the theme'spartials/. - Load the script once in
default.hbs:<script src="/ghost/ads/static/ads/verde.js" defer></script> - Place ads with
{{> verde-ad id="front-sidebar" label="MAINOS"}}and registerfront-sidebaras a placement in the ad manager at/ghost/ads/.
The manager accepts anyone Authelia signs in who is in the admins or editors group.