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
CopyOnce 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;
CopyStep 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
CopyI 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
CopyTo make it globally available:
mv update-container $HOME/.local/bin/
chmod +x $HOME/.local/bin/update-container
CopyStep 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
CopyThis 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.