Coolify GHCR Deployment
Coolify - Docker Compose Deployment
I love to jump from tool to tool to understand what’s out there and have a broader view when it comes to doing things efficiently or in a nicer way.
Today it’s Coolify, tomorrow it’s Dokploy — next week it’s something else…
To enable this flexibility, I stick to certain standards. For instance, I keep my notes in markdown files so they are portable and can be used anywhere. Similarly, as in the following example, I keep the build process out of the deployment/hosting tool.
This makes my work transferable, even if a platform decides to drop support or change licensing.
Architecture
graph LR
Push[git push] --> GA[GitHub Actions]
GA -->|Build & Push| GHCR[GitHub Container Registry]
GA -->|Webhook| Coolify
Coolify -->|Pull| GHCR
Coolify -->|Deploy| Container
Prerequisites
- Coolify Instance: A running instance of Coolify.
- GitHub Repository: Hosting your code.
- Coolify Token: An API token from Coolify (User Settings -> API Tokens).
1. Container Registry (GHCR)
Instead of letting Coolify build the image, we build it in CI and push to a registry. This speeds up deployments and ensures the exact same image is tested and deployed.
Dockerfile Best Practices
- Use multi-stage builds to keep images small.
- Run as a non-root user for security.
- Implement a healthcheck endpoint (e.g.,
/healthor/).
2. GitHub Actions Workflow
Create a workflow (e.g., .github/workflows/deploy.yml) to build and push the image.
Key Steps:
- Log in to GHCR.
- Build and push the Docker image.
- Trigger the Coolify webhook.
# Simplified example step
- name: Trigger Coolify Deployment
run: |
curl --request GET '${{ vars.COOLIFY_WEBHOOK_URL }}' \
--header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}'
3. Coolify Configuration
- Create a new Docker Compose resource.
- Docker Compose File:
services:
app:
image: ghcr.io/username/repo:latest
pull_policy: always # CRITICAL: Pull latest image on each deploy
container_name: my-app
restart: unless-stopped
environment:
- NODE_ENV=production
# Magic Variable: Routes traffic to container port 8080
- SERVICE_FQDN_APP_8080
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8080/health"]
interval: 30s
timeout: 10s
retries: 3
labels:
- "coolify.managed=true"
Key Configurations
pull_policy: always: for pre-built images. Without this, Docker Compose will reuse cached images and your deployments won’t update. This tells Docker to always pull the latest image from the registry before starting the container.- No Port Mapping: Do not use
ports: - "host:container"unless you explicitly need to bypass the proxy. Coolify handles internal routing. - Magic Environment Variables: Use
SERVICE_FQDN_<SERVICE_NAME>_<PORT>(e.g.,SERVICE_FQDN_APP_8080) to tell Coolify’s proxy (Traefik) which internal port to route traffic to. - Healthchecks: valid healthchecks ensure Coolify knows when the service is ready to receive traffic. Use
127.0.0.1inside the container.
4. Webhook Setup
- In Coolify Resource -> Webhooks.
- Copy the “Deploy” webhook URL.
- Add it as a variable (e.g.,
COOLIFY_WEBHOOK_URL) in your GitHub Repository settings. IMPORTANT: you need to set the force update option to true - when you copy the webhook URL from the coolify UI it has?force=falseat the end - ensure
COOLIFY_TOKENis added as a Secret.
Examples
This pattern was successfully implemented for the blog:
- Blog Deployment Setup (See specific implementation details)
- Docker
- Github Actions
Notes
I had to struggle a bit with Coolify’s Docker Compose support, as the documentation is a bit sparse. The key point is to ensure that the image is always pulled fresh from the registry on each deployment, which is achieved with the pull_policy: always setting. The force update option is still not fully clear to me, but setting it to true in the webhook URL seems to work reliably.