# Coolify Deployment Standard

> **Scope:** nextjs-neon
> **Layer:** 2 (on keyword)
> **Keywords:** coolify, docker, deploy, deployment, container, vps, ssh
> **Load When:** coolify or docker deployment keywords detected

**Verified against:** .NET 10 + Next.js 15 + Node 22 (Docker base images). Last-verified: 2026-07-07.

---

Stack: Next.js 15 + Neon PostgreSQL + .NET Backend

## What is Coolify and why we use it

[Coolify](https://coolify.io) is a self-hosted, open-source PaaS -- an alternative to Heroku/Vercel/Railway that runs on your own VPS. It manages Docker-based deployments end-to-end: builds from a Dockerfile or docker-compose, reverse-proxies traffic (Traefik under the hood), issues and renews Let's Encrypt SSL certificates, and exposes a REST API for automation.

Coolify is Polymorphism Tech's standard self-hosted deployment platform for Docker workloads.

There is no MCP server or CLI tool for Coolify. All access is via **SSH into the VPS**, from which the Coolify API is reached at `http://localhost:8000/api/v1/` (it is not exposed publicly). The API token lives server-side only (e.g. `~/.coolify-token`, `chmod 600`) -- never print it, never commit it, never let it leave the SSH session.

```bash
# Canonical access pattern — token is read and used entirely server-side
ssh <vps-ssh-alias> 'T=$(cat ~/.coolify-token); curl -s -H "Authorization: Bearer $T" http://localhost:8000/api/v1/projects'
```

Useful endpoints: `/version`, `/projects`, `/servers`, `/applications`, `/deploy?uuid={uuid}`. Changing a domain: `PATCH /applications/{uuid}` with `{"domains":"https://x,https://www.x"}`, then `GET /deploy?uuid={uuid}` to redeploy.

## Core Rules

- ALWAYS use multi-stage Docker builds for minimal image size
- ALWAYS configure health checks for zero-downtime deploys
- NEVER hardcode secrets in Dockerfiles -- use Coolify environment variables
- ALWAYS use `.dockerignore` to exclude node_modules, .git, .env files
- SSL is automatic via Let's Encrypt -- no manual certificate management
- NEVER call the Coolify API directly from your local machine or from CI without going through SSH -- the API port is not publicly reachable and the token must stay server-side

## .NET 10 Dockerfile

```dockerfile
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN adduser --disabled-password --gecos "" appuser
USER appuser
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]
```

## Next.js Standalone Dockerfile

```dockerfile
FROM node:22-alpine AS base

FROM base AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000 HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
```

Required: `output: "standalone"` in `next.config.ts`.

## .dockerignore

```
node_modules
.next
.git
.env*
*.md
.vscode
coverage
test
__tests__
```

## Coolify Service Config

| Setting | .NET Backend | Next.js Frontend |
|---------|-------------|-----------------|
| Source | GitHub | GitHub |
| Build method | Dockerfile | Dockerfile |
| Dockerfile path | `./backend/Dockerfile` | `./frontend/Dockerfile` |
| Port | 8080 | 3000 |
| Domain | api.example.com | app.example.com |

### GitHub Integration

Coolify deploys straight from a GitHub repo (via GitHub App install or a deploy key) and redeploys automatically on push when auto-deploy is enabled for the application.

1. In Coolify, add the GitHub source (GitHub App install or deploy key)
2. Create the Application, pointing it at the repo/branch and the Dockerfile path
3. Enable "Auto Deploy" for webhook-triggered deploys on push to the tracked branch
4. Record the application's `uuid` -- every subsequent API call (`/deploy?uuid=`) targets it

### Domain and SSL

DNS setup (A record to the Coolify server):
```
app.example.com  A      <vps-ip>
api.example.com  A      <vps-ip>
```

SSL via Let's Encrypt is automatic (Coolify runs Traefik as the reverse proxy). Force HTTPS enabled by default.

## Environment Variables

### .NET Backend

```env
ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://+:8080
ConnectionStrings__DefaultConnection=Host=ep-xxx.us-east-2.aws.neon.tech;Database=neondb;Username=...;Password=...;SslMode=Require
```

### Next.js Frontend

```env
DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require
NEXT_PUBLIC_API_URL=https://api.example.com
```

## Health Check Endpoints

```csharp
// .NET: Program.cs
builder.Services.AddHealthChecks()
    .AddNpgSql(connectionString, name: "database");
app.MapHealthChecks("/health");
```

```ts
// Next.js: app/api/health/route.ts
export async function GET() {
  return Response.json({ status: "healthy", timestamp: new Date().toISOString() });
}
```

## Zero-Downtime Deploys

Coolify uses the same Docker HEALTHCHECK contract to determine container readiness:

1. New container starts alongside old container
2. Health check passes after start-period + retries
3. Traffic shifts to new container (Traefik)
4. Old container stopped

| HEALTHCHECK Param | Value | Purpose |
|-------------------|-------|---------|
| `--interval` | 30s | Time between checks |
| `--timeout` | 5s | Max response wait |
| `--start-period` | 10s | Startup grace period |
| `--retries` | 3 | Failures before unhealthy |

## Monitoring

- **Logs**: `docker logs <container>` over SSH, or Coolify's live log viewer in the UI
- **Metrics**: CPU, memory, network via Coolify's dashboard, or `docker stats` over SSH
- **Restart**: Auto-restart on crash (default)

Structured logging:

```csharp
// .NET: Serilog with JSON output
builder.Host.UseSerilog((ctx, cfg) => cfg
    .ReadFrom.Configuration(ctx.Configuration)
    .WriteTo.Console(new JsonFormatter()));
```

```ts
// Next.js: pino
import pino from "pino";
const logger = pino({ level: process.env.LOG_LEVEL ?? "info" });
```

## Triggering a Deploy via SSH + API

Since there is no MCP server or CLI for Coolify, every deploy operation (manual redeploy, CI/CD, or troubleshooting) goes through the same SSH + curl pattern used for all other Coolify access:

```bash
# Redeploy an existing application after a new image/commit is ready
ssh <vps-ssh-alias> 'T=$(cat ~/.coolify-token); curl -s -X GET -H "Authorization: Bearer $T" "http://localhost:8000/api/v1/deploy?uuid=<application-uuid>"'
```

In GitHub Actions this becomes an SSH step using a deploy key stored as a secret (see the `github-workflow-deploy-coolify` template) -- never a direct HTTP call to the Coolify API from the runner, since the API port is not reachable outside the VPS.

## Deployment Checklist

| Step | Action |
|------|--------|
| 1 | Verify `output: "standalone"` in next.config.ts |
| 2 | Test Docker build locally |
| 3 | Test health endpoint |
| 4 | Configure env vars in Coolify (UI or API) |
| 5 | Set up custom domain + DNS |
| 6 | Verify SSL certificate |
| 7 | Enable auto-deploy from GitHub |
| 8 | Push to main, verify deployment via SSH (`docker ps` / Coolify logs) |
