diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4fa0dd358d..f319898e81 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,6 +88,24 @@ pnpm run dokploy:dev Go to http://localhost:3000 to see the development server +### Teardown + +When you're done, stop the Swarm services and the standalone Traefik container spun up by `dokploy:setup`: + +```bash +pnpm run dokploy:teardown +``` + +This removes the `dokploy-postgres`, `dokploy-redis` swarm services, the `dokploy-traefik` container, and the `dokploy-network` overlay. Volumes (your local database and Redis data) are left intact — delete them manually if you want a clean slate. + +### Exposing your dev server via a tunnel + +If you need to expose the local dev server publicly (e.g. to test GitHub/GitLab webhooks or better-auth social callbacks), set `DEV_ALLOWED_ORIGINS` in `apps/dokploy/.env` to a comma-separated list of extra origins. Both Next's dev server and better-auth's trusted origins will pick them up: + +```bash +DEV_ALLOWED_ORIGINS="https://your-tunnel.ngrok-free.app,https://your-tunnel.trycloudflare.com" +``` + > [!NOTE] > This project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off. diff --git a/apps/dokploy/.env.example b/apps/dokploy/.env.example index 8f801196e7..9c2e642382 100644 --- a/apps/dokploy/.env.example +++ b/apps/dokploy/.env.example @@ -1,3 +1,7 @@ DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy" PORT=3000 NODE_ENV=development + +# Comma-separated list of extra dev origins (e.g. ngrok / trycloudflare tunnels) +# that should be trusted by better-auth and allowed by Next's dev server. +# DEV_ALLOWED_ORIGINS="https://your-tunnel.ngrok-free.app,https://your-tunnel.trycloudflare.com" diff --git a/apps/dokploy/next.config.mjs b/apps/dokploy/next.config.mjs index 2f12eccee9..e6d557b584 100644 --- a/apps/dokploy/next.config.mjs +++ b/apps/dokploy/next.config.mjs @@ -3,8 +3,16 @@ * for Docker builds. */ +const devAllowedOrigins = (process.env.DEV_ALLOWED_ORIGINS ?? "") + .split(",") + .map((origin) => origin.trim()) + .filter(Boolean); + /** @type {import("next").NextConfig} */ const nextConfig = { + ...(devAllowedOrigins.length > 0 && { + allowedDevOrigins: devAllowedOrigins, + }), reactStrictMode: true, typescript: { ignoreBuildErrors: true, diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index c7622a3dc3..1fe4738908 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -10,6 +10,7 @@ "build-server": "tsx esbuild.config.ts", "build-next": "next build --webpack", "setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run", + "teardown": "tsx -r dotenv/config teardown.ts", "wait-for-postgres": "node -r dotenv/config dist/wait-for-postgres.mjs", "wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts", "reset-password": "node -r dotenv/config dist/reset-password.mjs", diff --git a/apps/dokploy/teardown.ts b/apps/dokploy/teardown.ts new file mode 100644 index 0000000000..46df560d5c --- /dev/null +++ b/apps/dokploy/teardown.ts @@ -0,0 +1,87 @@ +import { exit } from "node:process"; +import { docker } from "@dokploy/server/constants"; + +const SWARM_SERVICES = ["dokploy-postgres", "dokploy-redis"]; +const STANDALONE_CONTAINERS = ["dokploy-traefik"]; +const NETWORK_NAME = "dokploy-network"; + +const removeSwarmService = async (name: string) => { + try { + await docker.getService(name).remove(); + console.log(`Removed service ${name}`); + } catch (error: any) { + if (error?.statusCode === 404) { + console.log(`Service ${name} not running`); + return; + } + console.warn(`Failed to remove service ${name}:`, error?.message ?? error); + } +}; + +const removeContainer = async (name: string) => { + try { + await docker.getContainer(name).remove({ force: true }); + console.log(`Removed container ${name}`); + } catch (error: any) { + if (error?.statusCode === 404) { + console.log(`Container ${name} not running`); + return; + } + console.warn( + `Failed to remove container ${name}:`, + error?.message ?? error, + ); + } +}; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +const removeNetwork = async (name: string) => { + // Swarm service removal is async — tasks can still be detaching from the + // network when we get here, so retry the 403 ("in use") case briefly + // instead of forcing the user to run teardown twice. + const maxAttempts = 6; + const delayMs = 1000; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + await docker.getNetwork(name).remove(); + console.log(`Removed network ${name}`); + return; + } catch (error: any) { + if (error?.statusCode === 404) { + console.log(`Network ${name} not found`); + return; + } + if (error?.statusCode === 403 && attempt < maxAttempts) { + await sleep(delayMs); + continue; + } + if (error?.statusCode === 403) { + console.log(`Network ${name} still in use, skipping`); + return; + } + console.warn( + `Failed to remove network ${name}:`, + error?.message ?? error, + ); + return; + } + } +}; + +(async () => { + try { + for (const service of SWARM_SERVICES) { + await removeSwarmService(service); + } + for (const container of STANDALONE_CONTAINERS) { + await removeContainer(container); + } + await removeNetwork(NETWORK_NAME); + console.log("Dokploy teardown completed"); + exit(0); + } catch (error) { + console.error("Error in dokploy teardown", error); + exit(1); + } +})(); diff --git a/package.json b/package.json index 8e0141a7b3..c728210cc0 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ ], "scripts": { "dokploy:setup": "pnpm --filter=dokploy run setup", + "dokploy:teardown": "pnpm --filter=dokploy run teardown", "dokploy:dev": "pnpm --filter=dokploy run dev", "dokploy:build": "pnpm --filter=dokploy run build", "dokploy:start": "pnpm --filter=dokploy run start", diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index 65dd1b01d1..a416987cd9 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -92,7 +92,10 @@ const { handler, api } = betterAuth({ process.env.NODE_ENV === "development" ? [ "http://localhost:3000", - "https://absolutely-handy-falcon.ngrok-free.app", + ...(process.env.DEV_ALLOWED_ORIGINS ?? "") + .split(",") + .map((origin) => origin.trim()) + .filter(Boolean), ] : []; return [