Add caddy (#90)

* Add Nginx and Certbot services to Docker Compose

- Introduced Nginx service with custom configuration for handling HTTP and HTTPS traffic, including volume mounts for templates and entrypoint script.
- Added Certbot service for automated SSL certificate management, with a renewal loop and environment variable support for domain and email.
- Updated existing services to include restart policies and removed unnecessary port mappings for backend and client services.
- Enhanced volume management by adding certbot-conf and certbot-www for certificate storage and challenge handling.

* remove volume

* Replace Nginx and Certbot with Caddy in Docker Compose

- Removed Nginx and Certbot services, streamlining the configuration for SSL management.
- Introduced Caddy service with automatic HTTPS support and simplified volume management.
- Updated environment variables for domain and email configuration, ensuring compatibility with Caddy's setup.
- Enhanced volume definitions for persistent data and configuration storage.

* Fix

* Fix

* Remove Nginx and Certbot configurations from Docker setup

- Deleted Nginx and Certbot Dockerfiles, entrypoint scripts, and associated configuration files to streamline the project.
- Updated docker-compose.yml to remove Certbot email configuration, reflecting the transition to Caddy for SSL management.

* Remove version declaration from docker-compose.yml to simplify configuration

* Refactor login page to simplify account creation prompt

- Removed conditional rendering for the sign-up link, ensuring it is always displayed for better user accessibility.

* Update self-hosting documentation for Frogstats

- Revamped the self-hosting guide to provide clearer instructions for setting up Frogstats.
- Added prerequisites section detailing requirements such as a Linux VPS, domain name, Docker, and Git.
- Enhanced setup steps with detailed commands for installing Docker and Git, cloning the repository, and running the setup script.
- Included a callout for compatibility with Ubuntu 24 LTS and emphasized the importance of HTTPS for tracking scripts.
This commit is contained in:
Bill Yang 2025-04-16 23:04:17 -07:00 committed by GitHub
parent f890ec9b2b
commit d10017686f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 178 additions and 122 deletions

24
Caddyfile Normal file
View file

@ -0,0 +1,24 @@
# Caddyfile
# Use the domain name passed from docker-compose environment
{$DOMAIN_NAME} {
# Enable compression
encode zstd gzip
# Proxy API requests to the backend service
handle_path /api/* {
reverse_proxy backend:3001
}
# Proxy all other requests to the client service
handle {
reverse_proxy client:3002
}
# Optional: Add security headers (example)
# header {
# Strict-Transport-Security max-age=31536000;
# X-Content-Type-Options nosniff
# X-Frame-Options DENY
# Referrer-Policy strict-origin-when-cross-origin
# }
}

View file

@ -17,7 +17,6 @@ COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1
ARG NEXT_PUBLIC_BACKEND_URL
ENV NEXT_PUBLIC_BACKEND_URL=${NEXT_PUBLIC_BACKEND_URL}

View file

@ -147,14 +147,12 @@ export default function Page() {
</Alert>
)}
{IS_CLOUD && (
<div className="text-center text-sm">
Don't have an account?{" "}
<Link href="/signup" className="underline">
Sign up
</Link>
</div>
)}
<div className="text-center text-sm">
Don't have an account?{" "}
<Link href="/signup" className="underline">
Sign up
</Link>
</div>
</div>
</form>
</CardContent>

View file

@ -1,6 +1,24 @@
version: '3.8'
services:
caddy:
image: caddy:latest
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # Needed for HTTP/3
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile # Mount Caddy config file
- caddy_data:/data # Mount persistent data volume for certs etc.
- caddy_config:/config # Mount persistent config volume
environment:
# Pass domain name for use in Caddyfile
# Email is configured via Caddyfile global options
- DOMAIN_NAME=${DOMAIN_NAME}
depends_on:
- backend
- client
clickhouse:
container_name: clickhouse
image: clickhouse/clickhouse-server:latest
@ -19,6 +37,7 @@ services:
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
postgres:
image: postgres:latest
@ -27,18 +46,15 @@ services:
POSTGRES_USER: frog
POSTGRES_PASSWORD: frog
POSTGRES_DB: analytics
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
restart: unless-stopped
backend:
container_name: backend
build:
context: ./server
dockerfile: Dockerfile
ports:
- "3001:3001"
environment:
- NODE_ENV=production
- CLICKHOUSE_HOST=http://clickhouse:8123
@ -59,6 +75,7 @@ services:
condition: service_healthy
postgres:
condition: service_started
restart: unless-stopped
client:
container_name: client
@ -67,15 +84,16 @@ services:
dockerfile: Dockerfile
args:
NEXT_PUBLIC_BACKEND_URL: ${BASE_URL}
ports:
- "3002:3002"
environment:
- NODE_ENV=production
- NEXT_PUBLIC_BACKEND_URL=${BASE_URL}
- CLOUD=${CLOUD}
depends_on:
- backend
restart: unless-stopped
volumes:
clickhouse-data:
postgres-data:
caddy_data: # Persistent volume for Caddy's certificates and state
caddy_config: # Persistent volume for Caddy's configuration cache (optional but good practice)

View file

@ -1,105 +1,68 @@
import { Steps } from 'nextra/components'
import { Callout } from 'nextra/components'
# Self Hosting
# Self Hosting Frogstats
You can self-host Frogstats by following the steps below.
This guide will walk you through setting up your own instance of Frogstats.
## Prerequisites
## Create manually
Before you begin, ensure you have the following:
Nextra works like a Next.js plugin, and it accepts a theme config (layout) to
render the page. To start: [^3]
- **A Linux VPS:** We use [Hetzner Cloud](https://hetzner.cloud/?ref=QEdVqVpTLBDP) for everything (referral link - but Hetzner is legitimately the best value). CX11 for ~$4/month will be fine for small-medium sized projects.
- **A Domain Name:** You'll need a domain or subdomain (e.g., `tracking.yourdomain.com`) pointed to your VPS's IP address. HTTPS is required because browsers block tracking scripts served over insecure HTTP.
- **Docker Engine:** Docker is used to run the application stack. Installation instructions are in Step 1.
- **Git:** Needed to clone the repository.
<Callout type="info">
This guide has been tested on Ubuntu 24 LTS. While it should work on other distributions, your mileage may vary.
</Callout>
## Setup Steps
<Steps>
### Install Next.js, Nextra and React [^1]
### 1. Install Docker and Git
{/* ```sh npm2yarn
npm i react react-dom next nextra
``` */}
First, connect to your VPS via SSH.
### Install the docs theme [^2]
**Install Docker Engine:** Follow the official instructions for your Linux distribution:
[https://docs.docker.com/engine/install/](https://docs.docker.com/engine/install/)
```sh npm2yarn
npm i nextra-theme-docs
Make sure to complete the [post-installation steps for Linux](https://docs.docker.com/engine/install/linux-postinstall/) as well, particularly adding your user to the `docker` group so you don't have to use `sudo` for every Docker command.
**Install Git:** If Git is not already installed, install it using your distribution's package manager. For Debian/Ubuntu:
```bash
sudo apt update && sudo apt install -y git
```
### Create the following Next.js config and theme config under the root directory
### 2. Clone the Frogstats Repository
```js filename="next.config.mjs"
import nextra from 'nextra'
const withNextra = nextra({
theme: 'nextra-theme-blog',
themeConfig: './theme.config.js'
})
export default withNextra()
Clone the project repository from GitHub:
```bash
git clone https://github.com/goldflag/frogstats.git
cd frogstats
```
### Create a `theme.config.js` file for the docs theme
### 3. Run the Setup Script
```js filename="theme.config.js"
export default {
project: {
link: 'https://github.com/shuding/nextra' // GitHub link in the navbar
},
docsRepositoryBase: 'https://github.com/shuding/nextra/blob/master', // base URL for the docs repository
getNextSeoProps: () => ({ titleTemplate: '%s Nextra' }),
navigation: true,
darkMode: true,
footer: {
text: `MIT ${new Date().getFullYear()} © Shu Ding.`
},
editLink: {
text: 'Edit this page on GitHub'
},
logo: (
<>
<svg>...</svg>
<span>Next.js Static Site Generator</span>
</>
),
head: (
<>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Nextra: the next docs builder" />
<meta name="og:title" content="Nextra: the next docs builder" />
</>
),
primaryHue: {
dark: 204,
light: 212
}
}
The repository includes a setup script that configures the necessary environment variables (including generating a secure secret) and starts the application using Docker Compose.
Run the script, replacing `your.domain.name` with the domain or subdomain you configured in the prerequisites:
```bash
chmod +x setup.sh
./setup.sh your.domain.name
```
> [!NOTE]
>
> More configuration options for the docs theme can be found
> [here](/themes/docs/configuration).
The script will create a `.env` file and then build and start the containers. This might take a few minutes the first time.
### You are good to go! Run `next dev` to start
### 4. Sign Up
Once the services are running, Caddy (the webserver) will automatically obtain an SSL certificate for your domain.
Open your browser and navigate to `https://your.domain.name/signup` (using the domain you provided to the setup script).
Create your admin account. You can then log in and start adding your websites!
</Steps>
---
<span id="sidebar-and-anchor-links" />
> [!NOTE]
>
> Any `.md` or `.mdx` file will turn into a doc page and be displayed in
> sidebar. You can also create a `_meta.js` file to customize the page order and
> title. <br /> Check the source code: https://github.com/shuding/nextra for
> more information.
> [!TIP]
>
> You can also use
> [`<style jsx>`](https://nextjs.org/docs/basic-features/built-in-css-support#css-in-js)
> to style elements inside `theme.config.js`.
[^1]: Install Next.js, Nextra and React.
[^2]: Install the docs theme.
[^3]: To start.
Congratulations! You have successfully self-hosted Frogstats.

View file

@ -1,9 +1,6 @@
#!/bin/sh
set -e
# Docker Compose already ensures services are ready using healthchecks
# and dependency conditions in the docker-compose.yml file
# Run migrations explicitly using the npm script
echo "Running database migrations...."
npm run db:push

View file

@ -4,6 +4,7 @@ import { db } from "../../db/postgres/postgres.js";
import { member } from "../../db/postgres/schema.js";
import { getSitesUserHasAccessTo } from "../../lib/auth-utils.js";
import { getSubscriptionInner } from "../stripe/getSubscription.js";
import { IS_CLOUD } from "../../lib/const.js";
// Default event limit for users without an active subscription
const DEFAULT_EVENT_LIMIT = 10_000;
@ -13,12 +14,20 @@ export async function getSites(req: FastifyRequest, reply: FastifyReply) {
// Get sites the user has access to
const sitesData = await getSitesUserHasAccessTo(req);
// Enhance sites data - removing usage limit information for now
const enhancedSitesData = await Promise.all(
sitesData.map(async (site) => {
let isOwner = false;
let ownerId = "";
if (!IS_CLOUD) {
return {
...site,
monthlyEventCount: 0,
eventLimit: Infinity,
overMonthlyLimit: false,
isOwner: true,
};
}
// Determine ownership if organization ID exists
if (site.organizationId) {
const orgOwnerResult = await db

View file

@ -1,8 +1,9 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { stripe } from "../../lib/stripe.js";
import { eq } from "drizzle-orm";
import { FastifyReply, FastifyRequest } from "fastify";
import Stripe from "stripe";
import { db } from "../../db/postgres/postgres.js";
import { user as userSchema } from "../../db/postgres/schema.js";
import { eq } from "drizzle-orm";
import { stripe } from "../../lib/stripe.js";
interface CheckoutRequestBody {
priceId: string;
@ -49,7 +50,7 @@ export async function createCheckoutSession(
// 2. If the user doesn't have a Stripe Customer ID, create one
if (!stripeCustomerId) {
const customer = await stripe.customers.create({
const customer = await (stripe as Stripe).customers.create({
email: user.email,
metadata: {
userId: user.id, // Link Stripe customer to your internal user ID
@ -65,7 +66,7 @@ export async function createCheckoutSession(
}
// 4. Create a Stripe Checkout Session
const session = await stripe.checkout.sessions.create({
const session = await (stripe as Stripe).checkout.sessions.create({
payment_method_types: ["card"],
mode: "subscription",
customer: stripeCustomerId,

View file

@ -3,6 +3,7 @@ import { stripe } from "../../lib/stripe.js";
import { db } from "../../db/postgres/postgres.js";
import { user as userSchema } from "../../db/postgres/schema.js";
import { eq } from "drizzle-orm";
import Stripe from "stripe";
interface PortalRequestBody {
returnUrl: string;
@ -44,7 +45,9 @@ export async function createPortalSession(
}
// 2. Create a Stripe Billing Portal Session
const portalSession = await stripe.billingPortal.sessions.create({
const portalSession = await (
stripe as Stripe
).billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: returnUrl, // The user will be redirected here after managing their billing
});

View file

@ -34,7 +34,7 @@ export async function getSubscriptionInner(userId: string) {
}
// 2. List active subscriptions for the customer from Stripe
const subscriptions = await stripe.subscriptions.list({
const subscriptions = await (stripe as Stripe).subscriptions.list({
customer: user.stripeCustomerId,
status: "active", // Only fetch active subscriptions
limit: 1, // Users should only have one active subscription in this model

View file

@ -29,7 +29,7 @@ export async function handleWebhook(
return reply.status(400).send("Webhook error: No raw body available");
}
event = stripe.webhooks.constructEvent(
event = (stripe as Stripe).webhooks.constructEvent(
rawBody,
sig as string,
webhookSecret

View file

@ -5,6 +5,7 @@ import { eq, inArray, and } from "drizzle-orm";
import { db } from "../db/postgres/postgres.js";
import { processResults } from "../api/analytics/utils.js";
import { stripe } from "../lib/stripe.js";
import Stripe from "stripe";
// Default event limit for users without an active subscription
const DEFAULT_EVENT_LIMIT = 10_000;
@ -69,7 +70,7 @@ async function getUserSubscriptionInfo(userData: {
try {
// Fetch active subscriptions for the customer from Stripe
const subscriptions = await stripe.subscriptions.list({
const subscriptions = await (stripe as Stripe).subscriptions.list({
customer: userData.stripeCustomerId,
status: "active",
limit: 1,

View file

@ -5,13 +5,8 @@ dotenv.config();
const secretKey = process.env.STRIPE_SECRET_KEY;
if (!secretKey) {
throw new Error(
"Stripe secret key is not defined in environment variables. Please set STRIPE_SECRET_KEY."
);
}
export const stripe = new Stripe(secretKey, {
// apiVersion: "2024-06-20", // Use the latest API version - Removed to use SDK default
typescript: true, // Enable TypeScript support
});
export const stripe = secretKey
? new Stripe(secretKey, {
typescript: true, // Enable TypeScript support
})
: null;

48
setup.sh Normal file
View file

@ -0,0 +1,48 @@
#!/bin/bash
# Exit immediately if a command exits with a non-zero status.
set -e
# Check if domain name argument is provided
if [ -z "$1" ]; then
echo "Usage: $0 <domain_name>"
echo "Example: $0 myapp.example.com"
exit 1
fi
DOMAIN_NAME="$1"
BASE_URL="https://${DOMAIN_NAME}"
# Generate a secure random secret for BETTER_AUTH_SECRET
# Uses OpenSSL if available, otherwise falls back to /dev/urandom
if command -v openssl &> /dev/null; then
BETTER_AUTH_SECRET=$(openssl rand -hex 32)
elif [ -e /dev/urandom ]; then
BETTER_AUTH_SECRET=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)
else
echo "Error: Could not generate secure secret. Please install openssl or ensure /dev/urandom is available." >&2
exit 1
fi
# Create or overwrite the .env file
echo "Creating .env file..."
cat > .env << EOL
# Variables configured by setup.sh
DOMAIN_NAME=${DOMAIN_NAME}
BASE_URL=${BASE_URL}
BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
# Defaulting to empty strings to suppress docker-compose warnings
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
CLOUD=
EOL
echo ".env file created successfully with domain ${DOMAIN_NAME}."
# Build and start the Docker Compose stack
echo "Building and starting Docker services..."
docker compose up --build -d
echo "Setup complete. Services are starting in the background."
echo "You can monitor logs with: docker compose logs -f"