Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,74 @@ jobs:
files: dist/prosper202-*.zip
generate_release_notes: true
fail_on_unmatched_files: true

# Publish the production container image (the Dockerfile's default `app`
# stage) to GitHub Container Registry. This is what image-based deployments
# — including the Coolify service template in build/coolify-service-template/
# — pull instead of building from a git checkout. Uses the built-in
# GITHUB_TOKEN; no external registry account. NOTE: the ghcr.io package is
# private after the very first push — flip it to public once in the package's
# settings on GitHub so anonymous `docker pull` works.
docker-image:
runs-on: ubuntu-latest
Comment on lines +101 to +102

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate image publication on release validation

The docker-image job has no dependency on release, so both jobs run independently. When a v* tag fails the version check or the release artifact build, this job can still push that commit under both its version tag and latest, making an invalid release available to the Coolify template even though no corresponding GitHub Release was published; make image publication depend on successful release validation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — without the dependency, a tag failing the tag-vs-version.php check could still ship an image and move latest with no corresponding Release. Fixed in c3a4c32: docker-image now needs: release.


Generated by Claude Code

# Publish only after the release job succeeds: its tag-vs-version.php check
# and artifact build are the release validation — without this dependency a
# bad tag could still ship an image (and move `latest`) with no
# corresponding GitHub Release.
needs: release
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up QEMU (arm64 emulation)
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
if: startsWith(github.ref, 'refs/tags/v')
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# The versioned image tag comes straight from the git tag (which the
# release job this job depends on has already validated against
# version.php) rather than a semver parser: version.php's format permits
# values strict SemVer rejects (e.g. leading zeros), and a parser would
# silently skip the versioned tag for those.
- name: Derive version from tag
id: version
if: startsWith(github.ref, 'refs/tags/v')
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

- name: Compute image tags
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ steps.version.outputs.version }},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
# `latest` only for stable versions: version.php permits suffixed
# prereleases (e.g. v2.0.0-beta), and those must not replace the
# production image the Coolify template pulls by `:latest`.
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') }}
# Non-tag (workflow_dispatch) runs build without pushing; give the
# local build a deterministic name so the tag list is never empty.
type=sha,enable=${{ !startsWith(github.ref, 'refs/tags/v') }}

# A workflow_dispatch run from a branch builds without pushing, so the
# image build stays verifiable outside the release flow.
- name: Build (and push on tags)
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ startsWith(github.ref, 'refs/tags/v') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
1 change: 1 addition & 0 deletions build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ on first boot, so the setup wizard opens with the database step already done.
| `test-install-config.php` | Pre-baked `202-config.php` mounted into the **test-install** stack (`docker-compose.test-install.yml`). Points at the `db-test` service. Credentials (`root` / `root_password`) are throwaway container defaults reachable only inside the compose network — not secrets. Committed on purpose so the test stack is one command. |
| `staging-config.php` | Same idea for the **staging** stack (`docker-compose.staging.yml`), pointing at the `db2` service. *Gitignored* — `cp build/staging-config.sample.php build/staging-config.php` before bringing the staging stack up. |
| `staging-config.sample.php` | Committed template for the above; defaults already match the staging compose stack, so the copy works as-is. |
| `coolify-service-template/` | Ready-to-submit kit for listing Prosper202 in Coolify's one-click service catalog: the image-based service template, logo SVG, and submission checklist (see its README). Derived from `../docker-compose.coolify.yaml` — keep the two in sync. |
| `apache/my-tracking-proxy.conf` | Example **host-side** Apache reverse-proxy vhost (port 80/443 → the app container) so you can hit a real hostname like `my.tracking202.com` with TLS during local dev. Not part of any compose file; drop it into a host Apache and adjust the `ProxyPass` target port to whatever you bound the app to. |

---
Expand Down
57 changes: 57 additions & 0 deletions build/coolify-service-template/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Coolify one-click service template (submission kit)

Everything needed to get Prosper202 listed in Coolify's one-click service
catalog (<https://coolify.io/services>), prepared and ready to submit. The
catalog is generated from the [`coollabsio/coolify`](https://github.com/coollabsio/coolify)
repository — listing means getting a PR merged there.

## Contents

| File | Submits to (in coollabsio/coolify) |
|------|-------------------------------------|
| `prosper202.yaml` | `templates/compose/prosper202.yaml` |
| `svgs/prosper202.svg` | `svgs/prosper202.svg` |

The template mirrors the repo's `docker-compose.coolify.yaml`, except it pulls
the published image (`ghcr.io/tracking202/prosper202`) instead of building
from git — one-click services never clone the repository.

## Prerequisites (in order)

1. **A published container image.** The release workflow
(`.github/workflows/release.yml`, `docker-image` job) publishes
`ghcr.io/tracking202/prosper202:{version,latest}` (amd64 + arm64) on every
`vX.Y.Z` tag using the built-in `GITHUB_TOKEN` — no registry account
needed. **One manual step after the first tagged release:** the GHCR
package is created private; open the package's settings on GitHub and set
it to **Public** so anonymous `docker pull` works.
2. **Coolify's star requirement.** The contribution guide
(<https://coolify.io/docs/get-started/contribute/service>) asks that the
service's repository have **at least 1,000 GitHub stars**. Until the repo
crosses that bar, either open a GitHub Discussion with the Coolify
maintainers making the case for an exception, or hold the submission.
Everything else here stays valid either way.

## Submission checklist

1. Fork `coollabsio/coolify` on GitHub.
2. Copy `prosper202.yaml` to `templates/compose/prosper202.yaml` and
`svgs/prosper202.svg` to `svgs/prosper202.svg`. (Replace the placeholder
SVG with official vector brand art if available — the metadata header's
`logo:` line already points at the right path.)
3. Regenerate/reference the parsed templates per their contribution guide
(`templates/service-templates.json`).
4. **Test first**: on any Coolify instance, create a resource via
**Docker Compose Empty**, paste `prosper202.yaml` (without the metadata
comments), set a domain on the `prosper202` service, deploy, and complete
the setup wizard.
5. Open the PR. After it merges, add the docs page
(`/docs/services/prosper202.md` in their docs) so the service appears on
the website listing.

## Keeping the template in sync

`prosper202.yaml` is derived from `../../docker-compose.coolify.yaml`. If that
stack changes (new env vars, volumes, services), update this template in the
same commit — and after the catalog listing exists, upstream the same change
to `coollabsio/coolify`.
144 changes: 144 additions & 0 deletions build/coolify-service-template/prosper202.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# documentation: https://prosper202.com
# slogan: Self-hosted PPC and affiliate conversion tracking platform (ClickServer).
# category: analytics
# tags: tracking, affiliate, ppc, analytics, marketing, conversion, clickserver
# logo: svgs/prosper202.svg
# port: 80

# Ready-to-submit Coolify one-click service template for Prosper202.
# Submission target: templates/compose/prosper202.yaml in coollabsio/coolify
# (see the README next to this file for the submission checklist).
#
# Mirrors the repo's docker-compose.coolify.yaml, except it pulls the
# published image instead of building from a git checkout — one-click
# services never clone the repository. The container self-writes its
# database config from the environment on boot, so the setup wizard opens
# with the database step already completed.

services:
prosper202:
image: ghcr.io/tracking202/prosper202:latest
environment:
# FQDN routes the assigned domain to this service on port 80; URL is the
# same domain with the scheme, declared so Coolify generates it — the
# worker consumes it as P202_PUBLIC_ORIGIN for webhook download links.
- SERVICE_FQDN_PROSPER202_80
Comment on lines +21 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Declare the generated service URL for the worker

Only SERVICE_FQDN_PROSPER202_80 is declared on the routed service, while the worker later consumes SERVICE_URL_PROSPER202 as P202_PUBLIC_ORIGIN. Coolify generates the FQDN and URL variants independently when declared—the existing docker-compose.coolify.yaml deliberately declares both—so webhook exports from this catalog deployment can fall back to a configured tracking domain or localhost instead of producing links for the assigned Coolify URL; declare SERVICE_URL_PROSPER202 alongside the FQDN.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — this was a lesson already applied to docker-compose.coolify.yaml that didn't get carried into the template. Fixed in c3a4c32: SERVICE_URL_PROSPER202 is now declared on the routed service alongside the FQDN, and the worker consumes it as ${SERVICE_URL_PROSPER202:-}. Also applied the companion hardening from the same commit: ${SERVICE_PASSWORD_MYSQL:?} fails fast at all three sites (verified: docker compose config passes with the password set and errors loudly without it).


Generated by Claude Code

- SERVICE_URL_PROSPER202
- APP_ENV=production
Comment thread
Copilot marked this conversation as resolved.
# In-app 1-click upgrade writes to the ephemeral container filesystem;
# upgrades arrive as new image tags instead.
- P202_DISABLE_AUTO_UPGRADE=1
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL:?}
- MYSQL_DATABASE=${MYSQL_DATABASE:-prosper202}
- DB_HOST=db
- MC_HOST=memcached
# REST API v3 job/idempotency state — persisted outside the docroot.
- P202_SERVER_STATE_DIR=/var/lib/prosper202/api-v3-state
# Uploaded MaxMind ISP databases — persisted outside the docroot.
- P202_GEO_DIR=/var/lib/prosper202/geo
volumes:
- prosper202-exports:/var/www/html/202-config/temp/attribution-exports
- prosper202-api-state:/var/lib/prosper202/api-v3-state
- prosper202-geo:/var/lib/prosper202/geo
depends_on:
db:
condition: service_healthy
memcached:
condition: service_started
healthcheck:
test: ["CMD", "php", "-r", "exit(@file_get_contents('http://127.0.0.1/health/') === false ? 1 : 0);"]
interval: 10s
timeout: 5s
retries: 12
start_period: 30s

db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL:?}
- MYSQL_DATABASE=${MYSQL_DATABASE:-prosper202}
volumes:
- prosper202-mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p\"$${MYSQL_ROOT_PASSWORD}\""]
interval: 5s
timeout: 5s
retries: 24

memcached:
image: memcached:1.6-alpine
healthcheck:
test: ["CMD-SHELL", "nc -z 127.0.0.1 11211"]
interval: 10s
timeout: 5s
retries: 6

# Polls the scheduled-jobs endpoint once a minute (it has its own lock and
# frequency tiers), replacing a host crontab.
cron:
image: alpine:3.20
restart: unless-stopped
depends_on:
prosper202:
condition: service_started
command: >
sh -c 'while true; do
wget -q -O /dev/null http://prosper202/202-cronjobs/index.php || true;
sleep 60;
done'
healthcheck:
test: ["CMD", "true"]
interval: 30s

# Standalone CLI workers the HTTP cron endpoint does not cover: attribution
# export queue and API v3 sync worker every minute, snapshot rebuild hourly.
# Runs the same image as the web service, as the web user, sharing the
# export/state volumes.
worker:
image: ghcr.io/tracking202/prosper202:latest
restart: unless-stopped
user: www-data
environment:
- APP_ENV=production
- P202_DISABLE_AUTO_UPGRADE=1
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL:?}
- MYSQL_DATABASE=${MYSQL_DATABASE:-prosper202}
- DB_HOST=db
- MC_HOST=memcached
- P202_SERVER_STATE_DIR=/var/lib/prosper202/api-v3-state
- P202_GEO_DIR=/var/lib/prosper202/geo
# Public origin for webhook download links (no request context in CLI).
- P202_PUBLIC_ORIGIN=${SERVICE_URL_PROSPER202:-}
volumes:
- prosper202-exports:/var/www/html/202-config/temp/attribution-exports
- prosper202-api-state:/var/lib/prosper202/api-v3-state
- prosper202-geo:/var/lib/prosper202/geo
depends_on:
db:
condition: service_healthy
memcached:
condition: service_started
command:
- bash
- -c
- |
last_rebuild=0
while true; do
php 202-cronjobs/attribution-export.php || true
php 202-cronjobs/sync-worker.php || true
now=$$(date +%s)
if [ $$((now - last_rebuild)) -ge 3600 ]; then
php 202-cronjobs/attribution-rebuild.php || true
last_rebuild=$$now
fi
sleep 60
done
healthcheck:
test: ["CMD", "true"]
interval: 30s

volumes:
prosper202-mysql-data:
prosper202-exports:
prosper202-api-state:
prosper202-geo:
13 changes: 13 additions & 0 deletions build/coolify-service-template/svgs/prosper202.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading