raycer/deploy/COOLIFY.md

5.3 KiB

Deploying raycer to Coolify

raycer runs in production at https://raycer.altweb.me as a Docker Compose application managed by Coolify v4 on the cool2026 instance.

Where things live

  • Coolify dashboard: https://cool2026.altweb.me
  • Server: personal (Linode 91853095, Amsterdam, 2 GB)
    • server UUID: locg048kwko4sws8wcggc0o4
    • public IP: 172.235.183.140
  • Project: tools (UUID u8wooo0wwk4k8wcw48ww8oo8)
  • Application UUID: zsookwggwcss08wkkc4gookg
  • Source: Forgejo, https://forgejo-rko8sk40400wscowk4scko0w.altweb.me/spencer/raycer.git, branch main (mirrored to GitLab spencerflagg/raycer)
  • Compose file: /docker-compose.coolify.yaml (the local-dev docker-compose.yml is for raycer.test only and is not used in prod)

Containers

Service Image source Internal port Public Memory limit
backend built from ./backend 3000 (internal) 256 MiB
frontend built from ./frontend 80 raycer.altweb.me 64 MiB

The frontend's nginx reverse-proxies /api/* to http://backend:3000.

Required env + per-service domain

The frontend's public domain is set on the application record itself (not as a normal env var) via the docker_compose_domains field. Coolify reads this and generates the Traefik labels for the frontend service:

{ "frontend": { "name": "frontend", "domain": "https://raycer.altweb.me" } }

Coolify also auto-fills these env vars for the running containers (no manual entry required):

Variable Value Source
SERVICE_FQDN_FRONTEND https://raycer.altweb.me derived from docker_compose_domains.frontend.domain
SERVICE_FQDN_FRONTEND_80 https://raycer.altweb.me declared in compose, overridden to literal value

The backend's NODE_ENV, PORT, and DB_PATH are set inside the compose file.

Persistent storage

A named Docker volume raycer-data is mounted into the backend at /data and holds raycer.sqlite. Coolify creates and manages this volume; it survives redeploys.

To inspect or back up:

ssh root@172.235.183.140
docker volume inspect <coolify-prefixed-name>   # find via: docker volume ls | grep raycer
docker run --rm -v <volume>:/data -v $(pwd):/backup alpine \
  tar czf /backup/raycer-sqlite-$(date +%F).tgz -C /data .

Healthchecks

Both containers have a HEALTHCHECK (backend hits /api/health, frontend hits /). Coolify's "deployment healthy" gate uses these.

DNS

A record raycer.altweb.me -> 172.235.183.140 (Linode domain ID 1544692, TTL 300). To recreate:

linode-cli domains records-create 1544692 \
  --type A --name raycer --target 172.235.183.140 --ttl_sec 300

How the app was created

The Coolify v4 dashboard does not have a great "create from existing config" path, so the original create call hit the API directly:

TOKEN=$(jq -r '.instances[] | select(.name=="cool2026") | .token' \
  ~/.config/coolify/config.json)

curl -X POST https://cool2026.altweb.me/api/v1/applications/public \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "project_uuid": "u8wooo0wwk4k8wcw48ww8oo8",
    "server_uuid": "locg048kwko4sws8wcggc0o4",
    "environment_name": "production",
    "git_repository": "https://forgejo-rko8sk40400wscowk4scko0w.altweb.me/spencer/raycer.git",
    "git_branch": "main",
    "build_pack": "dockercompose",
    "docker_compose_location": "/docker-compose.coolify.yaml",
    "name": "raycer",
    "instant_deploy": false
  }'

After creation, the per-service domain was bound (this is the part that makes Coolify emit Traefik labels for the frontend service and request a Let's Encrypt cert for raycer.altweb.me):

APP_UUID=zsookwggwcss08wkkc4gookg
curl -X PATCH https://cool2026.altweb.me/api/v1/applications/$APP_UUID \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "docker_compose_domains": {
      "frontend": { "name": "frontend", "domain": "https://raycer.altweb.me" }
    }
  }'

Then deployed:

curl -X POST "https://cool2026.altweb.me/api/v1/deploy?uuid=$APP_UUID&force=true" \
  -H "Authorization: Bearer $TOKEN"

Re-deploy

Pushing to main on Forgejo triggers an automatic redeploy via the Coolify webhook (configured at app create time). To force a manual redeploy:

coolify deploy <APP_UUID> --context cool2026
# or
curl -X POST "https://cool2026.altweb.me/api/v1/deploy?uuid=<APP_UUID>" \
  -H "Authorization: Bearer $TOKEN"

Verification

curl https://raycer.altweb.me/api/health         # {"ok":true,...}
curl https://raycer.altweb.me/api/goals          # both seeded goals
coolify app list --context cool2026 --format json | jq '.[] | select(.name=="raycer")'

Resource notes

The personal server is 2 GB and tightly accounted for; mem_limit values in docker-compose.coolify.yaml must be respected. Backend's better-sqlite3 requires a brief native compile during the image build (~10 s, ~100 MB peak); first deploy may take a minute longer than subsequent ones.