# Deploying Property Management Network on Coolify This app is a Next.js 16 (App Router) server that needs: - a **PostgreSQL** database, - a **persistent volume** for uploaded files (documents/photos are stored on local disk under `STORAGE_DIR`), - a few third-party API keys (Stripe, OpenAI, Resend), - **scheduled tasks** for the rent/lease cron jobs (Coolify replaces `vercel.json` crons). The repo ships a production `Dockerfile` (standalone output), a `/api/health` liveness probe, and an entrypoint that runs database migrations on boot. There are two ways to deploy. **Path A (Dockerfile + separate Postgres) is recommended.** --- ## Path A — Dockerfile build pack + Coolify Postgres (recommended) ### 1. Create the database In your Coolify project: **+ New → Database → PostgreSQL**. Once created, copy its **internal connection string** (looks like `postgres://postgres:PASSWORD@:5432/postgres`). Use the internal host — the app talks to it over Coolify's private network. ### 2. Create the application **+ New → Application → Public/Private Git Repository**, point it at this repo, and set **Build Pack = Dockerfile**. ### 3. Set environment variables Under the app's **Environment Variables**, add everything from [`.env.production.example`](.env.production.example). At minimum: | Variable | Notes | |---|---| | `DATABASE_URL` | Internal Postgres URL from step 1. | | `DATABASE_SSL` | TLS policy. Use `disable` for Coolify's internal/private-network Postgres; `require` (default) for managed/external DBs; `no-verify` for self-signed certs. | | `BETTER_AUTH_SECRET` | `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"` | | `BETTER_AUTH_URL` | Your public URL, e.g. `https://propertymanagement.network` | | `NEXT_PUBLIC_APP_URL` | Same public URL. **Also mark as a Build Variable** (see below). | | `NEXT_PUBLIC_APP_NAME` | `Property Management Network` (Build Variable too). | | `CRON_SECRET` | Random string; protects the cron endpoints. | | `RESEND_API_KEY`, `RESEND_FROM_EMAIL` | Email sending. | | `STRIPE_*` | Billing (optional to start). | | `OPENAI_API_KEY` | AI assistant (optional to start). | > **Build Variables:** `NEXT_PUBLIC_APP_URL` and `NEXT_PUBLIC_APP_NAME` are inlined into the browser bundle at build time. In Coolify, set them so they're **available at build** (toggle "Build Variable" / "Available at Buildtime"). They're passed to the image via `ARG`/`--build-arg`. ### 4. Add a persistent volume for uploads Uploaded files are written to `STORAGE_DIR` (default `/app/storage`). Without a volume they're lost on every redeploy. Under the app's **Storages → Add**: mount a persistent volume at the container path **`/app/storage`**. ### 5. Domain & port - Set the app's **Domain** to your URL; Coolify provisions HTTPS automatically. - The container listens on **port 3000** (already `EXPOSE`d). Coolify usually detects this; set the port to `3000` if asked. ### 6. Health check The image has a built-in Docker `HEALTHCHECK` hitting `/api/health`. You can also set Coolify's health check path to `/api/health`. ### 7. Deploy Click **Deploy**. On boot the entrypoint runs `scripts/migrate.mjs` to apply migrations, then starts the server. Watch the deploy logs for `[migrate] Migrations applied successfully.` followed by the Next.js ready line. --- ## Path B — Docker Compose (app + Postgres bundled) Use the included [`docker-compose.yml`](docker-compose.yml) with Coolify's **Docker Compose** build pack. It defines the `app` and a `db` (Postgres 17) plus named volumes `app-storage` and `db-data`. Set these env vars in Coolify (mark `NEXT_PUBLIC_*` and `POSTGRES_*` as available at build time): ``` POSTGRES_USER=pmn POSTGRES_PASSWORD= POSTGRES_DB=pmn BETTER_AUTH_SECRET= BETTER_AUTH_URL=https://your-domain NEXT_PUBLIC_APP_URL=https://your-domain NEXT_PUBLIC_APP_NAME=Property Management Network CRON_SECRET= RESEND_API_KEY=... # plus STRIPE_*, OPENAI_API_KEY as needed ``` `DATABASE_URL` is composed automatically from the `POSTGRES_*` values inside the compose file. The app waits for the DB healthcheck before starting and migrations retry while Postgres comes up. --- ## Database migrations Migrations live in `lib/db/migrations` (Drizzle). They run automatically on container start via the entrypoint. - To **disable** auto-migrate (e.g. when running more than one replica), set `RUN_MIGRATIONS_ON_START=false` and run them as a one-off instead: ```sh # From a Coolify terminal/exec into the container: node scripts/migrate.mjs ``` --- ## Scheduled tasks (cron) Coolify does not read `vercel.json`. Recreate the two jobs under the app's **Scheduled Tasks**. Each runs a command inside the container; authenticate with the `CRON_SECRET` env var that's already present there. | Name | Schedule (UTC) | Command | |---|---|---| | Daily (rent reminders, overdue, lease expiry) | `0 9 * * *` | `wget -q -O- --header="Authorization: Bearer $CRON_SECRET" http://127.0.0.1:3000/api/cron/daily` | | Late fees | `0 8 * * *` | `wget -q -O- --header="Authorization: Bearer $CRON_SECRET" http://127.0.0.1:3000/api/cron/late-fees` | (The `daily` route already combines rent reminders, overdue marking, and 60/30/7-day lease-expiry emails.) --- ## Stripe webhook (if using billing) Point a Stripe webhook at `https:///api/stripe/webhook` and put its signing secret in `STRIPE_WEBHOOK_SECRET`. Subscribe to: `checkout.session.completed`, `customer.subscription.created/updated/deleted`, `invoice.payment_failed`, `payment_intent.succeeded`. --- ## Post-deploy checklist - [ ] `https:///api/health` returns `{"status":"ok",...}` - [ ] Home page shows **Property Management Network** branding - [ ] Sign up / log in works (verifies `DATABASE_URL` + `BETTER_AUTH_*`) - [ ] Upload a document, redeploy, confirm it persists (verifies the `/app/storage` volume) - [ ] Trigger the `daily` scheduled task manually and confirm a 200 in logs - [ ] (If billing) Stripe webhook delivers successfully --- ## Notes - **Google OAuth:** set `GOOGLE_CLIENT_ID/SECRET` and add `/api/auth/callback/google` as an authorized redirect URI. - **Scaling:** with more than one replica, disable per-instance auto-migration (`RUN_MIGRATIONS_ON_START=false`) and note that local-disk storage is per-container — move uploads to object storage (e.g. S3) if you scale horizontally. - **TLS:** the app and migrator default to encrypted + certificate-verified Postgres connections. Set `DATABASE_SSL=disable` for Coolify's internal private-network Postgres (and the bundled compose DB), `require` for managed/external DBs with a public CA, or `no-verify` for self-signed certs (optionally supply `DATABASE_CA`).