A production-ready Docker Compose stack for running Revive Adserver on a single VPS, with automatic Let's Encrypt SSL, persistent MariaDB storage, PHP tuned for ad delivery, and operator docs for two common tasks: wiring a VAST tag into a Bunny.net video player and configuring cookie-based frequency capping.
🇮🇱 מדריך התקנה בעברית עבור Kamatera + DuckDNS:
docs/DEPLOY-KAMATERA.he.md
Revive Adserver ships no official Docker image. This repo builds Revive from a local source ZIP committed to
docker/revive/revive-adserver-6.0.6.zipon top ofphp:8.2-apache— seedocker/revive/Dockerfile. To upgrade Revive, drop the new ZIP indocker/revive/, bumpREVIVE_VERSIONindocker-compose.yml, anddocker compose up -d --build revive.Install is fully automatic on first container boot — no web installer to click through. See
docker/revive/install-cli.phpanddocker/revive/entrypoint.sh. The bundled web installer (/www/admin/install.php) has a known redirect-loop bug in 6.0.6 that this approach side-steps.
┌────────────────────────────────┐
client (browser / │ nginx-proxy + acme-companion │ :80 / :443 (host)
Bunny.net player) ─▶│ Let's Encrypt termination │
└─────────────┬──────────────────┘
│ HTTP, docker bridge
▼
┌──────────────────────┐
│ revive (PHP/Apache)│
└─────────┬────────────┘
│ TCP 3306
▼
┌──────────────────────┐
│ db (MariaDB 10.11) │
└──────────────────────┘
revive will not start until db reports healthy (MariaDB's healthcheck.sh --connect --innodb_initialized). Only nginx-proxy publishes host ports — the database is never reachable from the public internet.
- A VPS with a public IPv4 and ports 80 and 443 open.
- Docker Engine 24+ and Docker Compose v2 installed (
docker compose version). - A domain you control, with an A record pointing to the VPS IP. Let's Encrypt's HTTP-01 challenge needs this to resolve before you bring the stack up.
# 1. Clone
git clone https://github.com/<your-user>/ad-server.git
cd ad-server
# 2. Configure secrets
cp .env.example .env
$EDITOR .env # set REVIVE_DOMAIN, LETSENCRYPT_EMAIL, all *_PASS values
# 3. Verify the compose file parses with your .env
docker compose config >/dev/null
# 4. Bring it up (first run builds the Revive image — ~2–3 min)
docker compose up -d --build
# 5. Watch the install + cert (first boot, ~60–120s after DNS resolves)
docker compose logs -f revive
# look for: "[install-cli] Done." then "Starting Apache..."
# Ctrl-C once you see it, then:
docker compose logs -f acme
# look for: "Creating/renewal of certificates for <your-domain>" → "success"
# 6. Open https://<REVIVE_DOMAIN>/ → login page (NOT an install wizard).
# Use ADMIN_USER / ADMIN_PASSWORD from your .env.Revive's delivery stats, priority recalculation, and email reports run via a maintenance script. Add this to the host crontab (crontab -e):
*/5 * * * * docker compose -f /path/to/ad-server/docker-compose.yml exec -T revive php /var/www/html/scripts/maintenance/maintenance.php >/dev/null 2>&1The end goal is a single URL of the form https://<REVIVE_DOMAIN>/www/delivery/fc.php?zoneid=<N>&cb=... that you paste into Bunny.net Stream's VAST Tag URL field.
-
Log in to Revive at
https://<REVIVE_DOMAIN>/withADMIN_USER/ADMIN_PASSWORD. -
Create a Website — Inventory → Websites → Add new website. Give it a name like
bunny-videoand your site URL. -
Create a Video Zone under that website — Add new zone → Type: Video (NOT banner). Save and note the Zone ID shown in the URL/sidebar.
-
Create an Advertiser — Inventory → Advertisers → Add new advertiser.
-
Create a Campaign under that advertiser, then a Banner of type "Video (VAST)":
- Paste a remote VAST XML URL (from your DSP / creative provider), or upload your own VAST 2.0/4.x XML.
-
Link the banner to the video zone — open the banner → Linked Zones → check the video zone you created → Save.
-
Grab the invocation code — open the video zone → Invocation Code tab → set Invocation Type to Video tag — VAST 2.0/4.x → copy the URL Revive displays. Example:
https://ads.example.com/www/delivery/fc.php?zoneid=7&cb=INSERT_RANDOM_NUMBER_HERE -
Configure Bunny.net — Bunny.net Stream → your video library → Player tab → Advertising section → paste the URL into VAST Tag URL. Bunny.net automatically substitutes its cache-buster token in place of
INSERT_RANDOM_NUMBER_HERE; if your version of the Bunny.net UI requires a literal token, use{random}or whichever token their docs specify. -
Verify — play the video. The pre-roll should fire. Within ~1 minute Revive's Statistics → Zones → <your zone> will show an impression.
| Symptom | Likely cause |
|---|---|
| Bunny.net player shows "no ad" | Banner not linked to zone, or campaign weight/priority is 0. |
| Pre-roll plays but no impression in Revive | Cache-buster not replaced — every request looks identical to Revive's anti-double-count logic. Make sure Bunny.net is substituting the token. |
| CORS error in browser console | Revive must be reachable over HTTPS (it is, in this setup) and the VAST XML you serve must itself be HTTPS. |
Revive caps per-user impressions via the OAID cookie it drops on the viewer's browser. Caps are set on the campaign, not on the banner or zone.
- Inventory → Campaigns → <your campaign> → Delivery Options tab.
- Set Total impressions per user — e.g.
3. - Set Time period —
per day,per hour, orper session. - (Optional) set Total clicks per user and Total conversions per user in the same panel.
- Save.
That's it. Revive now reads the OAID cookie on every delivery and skips this campaign for users who have hit the cap.
Because the Bunny.net player loads from a different origin (e.g. iframe.mediadelivery.net) than your ad server (ads.example.com), the OAID cookie must be set with SameSite=None; Secure for it to be sent on the third-party VAST request. This stack guarantees both conditions:
- TLS is always on in production (Let's Encrypt, no plaintext fallback).
config/php/revive.inisetssession.cookie_samesite = "None"andsession.cookie_secure = 1.
If frequency capping silently doesn't work, open the browser devtools → Application → Cookies on the player page and verify OAID is present for ads.example.com with Secure and SameSite=None.
# Dump the DB (writes to ./backup-YYYYMMDD.sql.gz on the host)
docker compose exec -T db \
mariadb-dump -u root -p"$DB_ROOT_PASS" --single-transaction --routines --triggers "$DB_NAME" \
| gzip > "backup-$(date +%Y%m%d).sql.gz"
# Banner creatives live in the revive_banners volume — back that up too:
docker run --rm -v revive-adserver_revive_banners:/data -v "$PWD":/backup alpine \
tar czf /backup/banners-$(date +%Y%m%d).tar.gz -C /data .Bump REVIVE_VERSION under services.revive.build.args in docker-compose.yml (e.g. 6.0.6 → 6.1.0), then:
git pull # pick up repo changes (Dockerfile, compose tweaks)
docker compose pull # refresh mariadb / nginx-proxy / acme
docker compose up -d --build revive # rebuild the Revive image with the new versionRun Revive's database upgrade by visiting https://<REVIVE_DOMAIN>/www/admin/ after the rebuild — Revive auto-detects schema changes and walks you through them.
docker compose logs -f revive # app + Apache access/error
docker compose logs -f db
docker compose logs -f acme # cert issuance / renewal
docker compose logs -f nginx-proxy| Problem | Fix |
|---|---|
acme log shows urn:ietf:params:acme:error:dns or connection refused |
DNS A record hasn't propagated, or port 80 is firewalled. Verify dig +short $REVIVE_DOMAIN matches your VPS IP, and that nothing else binds :80. |
| Browser shows self-signed cert warning | acme hasn't issued the real cert yet. Wait 1–2 min, then docker compose restart nginx-proxy. |
| Revive installer asks for DB host repeatedly | Use literal hostname db (the service name), not localhost or 127.0.0.1. |
revive container exits with DB connection error |
Check docker compose ps — db must show (healthy). If not, docker compose logs db for the root cause (most often a malformed password containing shell-special chars; quote it in .env). |
| Need to re-run the installer | Stop the stack, remove the revive_var volume (docker volume rm revive-adserver_revive_var), bring it back up. This does not wipe the DB. Also drop the Revive schema if you want a truly clean install. |
| Cert renewal | Automatic. acme renews 30 days before expiry. You can force it with docker compose exec acme /app/force_renew. |
.
├── docker-compose.yml # 4-service stack, healthcheck-gated startup
├── .env.example # template — copy to .env, fill in, never commit
├── .gitignore # keeps .env and OS cruft out of git
├── config/
│ └── php/
│ └── revive.ini # PHP + OPcache tuning, mounted into revive
└── README.md # this file
The configuration in this repository is released under the MIT License. Revive Adserver itself is licensed under the GNU GPL v2; consult upstream for the application's licensing terms.