Debugging Dan

Tech enthusiast, avid side project builder. 🚀

06/20/2025

Migrating from Coolify to Manual Docker Compose: A Practical Guide

When self-hosting applications, it's tempting to rely on platforms like Coolify to simplify deployments. But sometimes, you want more control over your infrastructure, more transparency, and a deeper understanding of what's running on your machine. That was exactly the case when I decided to migrate away from Coolify and go fully manual with Docker Compose.

In this blog post, I’ll walk through the process of extracting app information from Coolify, building reusable Docker Compose setups, and streamlining deployment using shell scripts. Whether you’re trying to leave Coolify behind or just want to understand Docker Compose better, this guide is for you.

Step 1: Exploring Coolify Internals

Coolify stores app data in a PostgreSQL database. To extract that, you need access to the database credentials. You can usually find them by inspecting the .env file inside the running Coolify container:

docker exec -it coolify sh
cat .env
Copy

Once you have the DATABASE_URL, connect using a tool like psql or TablePlus. I found that app data is stored in the applications table. To explore the columns and see all fields for a specific row:

SELECT * FROM applications LIMIT 1;
Copy

Step 2: Building a Reusable Docker Compose Template

Instead of relying on Coolify's UI, I wanted to define my services explicitly using Docker Compose. Here’s a simple, reusable template:

version: '3.8'
services:
  app:
    image: your-image:${VERSION}
    container_name: your-container-name
    env_file:
      - .env
    labels:
      - "app.label=my-app"
    ports:
      - "8080:8080"
    volumes:
      - ./data:/app/data
    restart: unless-stopped
Copy

I use a version.txt file to specify which image tag/version to deploy. This allows me to switch versions without editing the Compose file.

Step 3: Automating Deployment with a Script

To update containers cleanly, I created a Bash script named update-container that:

  • Reads the version from version.txt
  • Extracts the image name from docker-compose.yml
  • Pulls the new version
  • Stops and removes the old container
  • Recreates the container

Here’s the final script:

#!/bin/bash
set -e
VERSION=$(cat version.txt)
export VERSION
IMAGE=$(grep 'image:' docker-compose.yml | awk '{print $2}' | head -n 1)
IMAGE_WITH_VERSION="${IMAGE%%:*}:$VERSION"
docker pull "$IMAGE_WITH_VERSION"
docker compose stop
docker compose rm -y
docker compose up -d
Copy

To make it globally available:

mv update-container $HOME/.local/bin/
chmod +x $HOME/.local/bin/update-container
Copy

Step 4: Firewall Rules with UFW

I wanted to expose MySQL (3306) to local services but block external access. UFW made that simple:

sudo ufw allow 3306
sudo ufw deny in on eth0 to any port 3306
Copy

This blocks external access via eth0 while allowing local and Docker bridge traffic.

Final Thoughts

Leaving Coolify gave me more visibility and flexibility. With Compose and simple scripts, I now manage my containers confidently and can reproduce setups with ease.

If you’re using Coolify and considering taking full control, this process is approachable and rewarding. Feel free to adapt the examples here to fit your stack.