The moment a backup matters is rarely the moment when you can read up on it calmly. The server is gone, a fresh Hetzner box sits next to it, the dumps are in the S3 bucket, the Coolify docs are open. The pg_restore runs through, the dashboard comes up, the login works. And on the first click onto a configured server it says “The MAC is invalid.” and throws a 500.
Exactly this case is documented several times over, among others in issue #3761 in the Coolify repository dated 7 October 2024. After a restore onto a new server, the login with 2FA failed there with a 500 at first, and once the login went through, every call to a configured S3 storage, a server or a key returned the same message. In discussion #1684 a user reports that pg_restore completed without errors but the dashboard simply had not changed.
One sentence up front, because the story often gets told too broadly. A database dump from Coolify is not worthless without the APP_KEY. It is an ordinary file. The key decides something else.
Photo: albertstoynov / Unsplash
What Coolify backs up and what it does not
There are two things that both go by the name “backup”. One is the scheduled backups of your application databases. The other is the backup of the Coolify instance itself, meaning a pg_dump of the Postgres database coolify, in which Coolify keeps its entire configuration.
The docs are clear about the distinction. On the instance backup they say literally: “This only backs up and restores the Coolify instance itself - not your application data.” And, in addition: “All settings from your Coolify dashboard will be restored, but application data (such as volume mounts) must be backed up and restored manually.”
On the host, both kinds end up under /data/coolify/backups. Application databases under databases/<team-slug>-<team-id>/<directory>/, the backup of Coolify itself under coolify/coolify-db-<server-ip>/. The file names follow the scheme pg-dump-<db>-<unix-timestamp>.dmp, and correspondingly mysql-dump-..., mariadb-dump-... and mongo-dump-<db>-<timestamp>.tar.gz. The timestamp is a Unix timestamp, not a readable date. Anyone hunting for the right point in time under pressure gets to do arithmetic first.
The upload to S3 does not run through an AWS SDK but through a temporary helper container with the MinIO CLI. Coolify sets an mc alias there with the stored credentials, copies the file with mc cp and removes the container again. If disable_local_backup is set, the local copy is deleted after a successful upload.
The dump itself is unencrypted
This is the point where the panic sorts itself out. For Postgres, DatabaseBackupJob.php simply calls:
docker exec <container> pg_dump --format=custom --no-acl --no-owner \
--username <user> <db>
# with dump_all:
docker exec <container> pg_dumpall --username <user> | gzip
MySQL and MariaDB work the same way with mysqldump and mariadb-dump respectively, MongoDB with mongodump --gzip --archive. There is no encryption stage at application level. That file can be loaded anywhere:
pg_restore --list pg-dump-<db>-<timestamp>.dmp | head
pg_restore --clean --no-acl --no-owner -U postgres -d targetdb \
pg-dump-<db>-<timestamp>.dmp
No Coolify needed, no key needed. Two consequences follow. First, your data recovery does not hang on Coolify as long as you can reach the bucket. Second, Coolify does not protect these files against anyone else who reaches the bucket.
Photo: jb_baxter / Unsplash
The APP_KEY and why it is never inside the backup
The key lives in /data/coolify/source/.env on the host:
cat /data/coolify/source/.env | grep '^APP_KEY='
# APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=
It is generated once in the installer, on line 842 of scripts/install.sh:
update_env_var "APP_KEY" "base64:$(openssl rand -base64 32)"
So 32 random bytes, base64 encoded, with a prefix. The helper function update_env_var does not overwrite a value that is already set, it only takes effect when the line is empty or the key is missing entirely:
update_env_var() {
local key="$1"
local value="$2"
if grep -q "^${key}=$" "$ENV_FILE"; then
sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE"
elif ! grep -q "^${key}=" "$ENV_FILE"; then
printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE"
fi
}
Running the install script again therefore does not rotate the key, as long as the .env exists and contains the key. What is also relevant in practice is that the script writes a dated copy before every merge. On a host that has been running for a while there may therefore be files such as .env-20260727-113000, and those can contain an older key:
ls -la /data/coolify/source/.env-*
grep -h '^APP_KEY=' /data/coolify/source/.env-* | sort -u
The key is not in the backup. The instance backup is a pg_dump of the database, and the .env is not part of it. The docs warn at that point, word for word: “Save this APP_KEY safely. Without it, you cannot restore your backup.” Issue #350 in the Coolify docs, opened on 31 August 2025 and still open, explicitly recommends backing up .env files separately from the database backups as a preventive measure.
What Coolify encrypts
Coolify stores individual columns of its own database using Laravel’s encrypter, recognisable by the 'encrypted' cast in the models:
// PrivateKey.php
protected $casts = ['private_key' => 'encrypted'];
// EnvironmentVariable.php
protected $casts = ['key' => 'string', 'value' => 'encrypted', /* ... */];
// StandalonePostgresql.php
protected $casts = [/* ... */ 'postgres_password' => 'encrypted', /* ... */];
// S3Storage.php
protected $casts = ['is_usable' => 'boolean', 'key' => 'encrypted', 'secret' => 'encrypted'];
On top of that come the logdrain keys in the Server model. Taken together, that is the SSH keys for every target server, all environment variables of all applications, the database passwords and the S3 credentials.
Anyone who, like me, sets all environment variables of production apps consistently through Coolify rather than maintaining them in local files turns the APP_KEY into a single point of failure. That is the clean way to run things, because the secrets then live nowhere in the repository. It simply carries exactly this price.
Photo: 2hmedia / Unsplash
What happens without the key
Laravel encrypts with AES-256-CBC and signs every value with a MAC. If the key does not match, the encrypter throws a DecryptException with one of three messages:
throw new DecryptException('The MAC is invalid.'); // Encrypter.php, line 188
throw new DecryptException('Could not decrypt the data.'); // line 192
throw new DecryptException('The payload is invalid.'); // lines 235 and 244
The symptom pattern from issue #350 is characteristic. The dashboard login works, new resources can be created, existing ones can no longer be opened. The docs cover this under the troubleshooting case “500 Error on Login or Project Access”. In issue #11056 of 27 July 2026 (Coolify v4.1.2, Ubuntu 24.04.4) a service page returns HTTP 500 with “The payload is invalid.”, and the stack trace runs from Encrypter.php:244 into EnvironmentVariable.php:354. The reporter used Crypt::decryptString() in artisan tinker to show that practically every row of the environment_variables.value column could no longer be decrypted under the active key, and found no copy of the old key. Whether a rotated APP_KEY really was the cause there is open, there is no maintainer response so far, and the logic of update_env_var argues against it.
One feedback loop is particularly awkward. Coolify can restore directly from S3 through the interface, via resource, configuration, “Import Backup”, “Select from S3”. That path fetches the credentials through S3Storage::ownedByCurrentTeam()->findOrFail($this->s3StorageId), and key and secret of that model are encrypted. Without a matching key, Coolify cannot read its own S3 credentials. The convenient restore path therefore drops out precisely when you need it. Anyone who has noted the S3 credentials independently bypasses this and downloads the dumps directly.
The documented way back
The docs describe eight steps. Trigger a backup, save the APP_KEY, save the SSH keys from /data/coolify/ssh/keys (files ssh_key@<random_id>) plus the public keys from ~/.ssh/authorized_keys, install a new server with the same Coolify version, load the dump, put the keys back, enter APP_PREVIOUS_KEYS, run the install script again.
docker stop coolify coolify-redis coolify-realtime coolify-proxy
cat /path/to/your_backup_file | docker exec -i coolify-db \
pg_restore --verbose --clean --no-acl --no-owner -U coolify -d coolify
Warnings about existing foreign keys or sequences can usually be ignored according to the docs. Then the keys:
rm -f /data/coolify/ssh/keys/*
# copy the old ssh_key@<random_id> files back in, then:
sudo chown -R root:root /data/coolify
The chown is not a cosmetic step but the documented remedy against “Permission denied” on server access.
And then the spot with the format trap:
APP_KEY="base64:J63qRTDLub5NuZvP+kb8YIorGS6qFYHKVo6u7179stY="
APP_PREVIOUS_KEYS="base64:2nLsGFGzyoae2ax3EF2Lyq/hH6QghBGLIq5uL+Gp8/w="
APP_PREVIOUS_KEYS is a Laravel feature. Encryption always uses the current key, and when decrypting Laravel first tries the current one and then works through all the listed old ones, comma separated. The Coolify docs write the value without the prefix at this point (APP_PREVIOUS_KEYS=your_previous_app_key_here), while Laravel and issue #350 show it with base64:. Copy the value exactly as it stood in the old .env.
What the backup does not contain
Volumes. The migration docs put it unmistakably: “Coolify does not have a built-in option to migrate applications from one server to another.” The documented route is a throwaway container:
docker run --rm -v "$VOLUME_NAME":/volume -v "$(pwd)/$BACKUP_DIR":/backup \
busybox tar czf /backup/"$BACKUP_FILE" -C /volume .
docker run --rm -v "$TARGET_VOLUME":/volume -v "$(pwd)/$BACKUP_DIR":/backup \
busybox sh -c "cd /volume && tar xzf /backup/$BACKUP_FILE"
That affects every persistent storage volume attached through the API. An uploads volume mounted at /app/uploads holding uploaded customer files falls under no database schedule at all. If you have only configured database backups, you do not have those files.
On import through the interface, Redis, KeyDB, Dragonfly and ClickHouse are additionally marked as unsupported. Issue #7529 documents the matrix according to which StandalonePostgresql, Mysql, Mariadb and Mongodb could do both backup and restore, while ServiceDatabase from Docker Compose deployments could only do backup. The issue was closed on 1 April 2026, and I found no merge evidence for an implementation. I would verify this for my own environment rather than assume it.
Photo: ubahnverleih / Unsplash
A restore drill you can actually carry out
There is no official verification procedure. From the documented steps, though, you can assemble a drill that genuinely touches the interesting spots.
First, collect the material and store it outside the server, in a password manager or an encrypted vault. That includes the APP_KEY, all .env-* copies with a differing key, the files from /data/coolify/ssh/keys, the S3 credentials in plain text and the Coolify version in use.
Then a second server with the same version, load the instance dump, put the keys back, enter the old key as APP_PREVIOUS_KEYS. The actual test starts after that, and it does not consist of a successful login. Deliberately open an S3 storage, a server, a private key and the environment variables of an application. Those four views are exactly the ones that tip into a 500 with the wrong key, and everything else looks intact.
Second part, independent of that: load an application dump from S3 into an empty Postgres outside Coolify and roll a volume tarball back into a fresh volume. Then throw the second server away.
With Coolify I always verify by curl whether a change has actually reached the container, instead of believing the success message in the dashboard. It pays off with container labels, because Docker only reads them at create time. With a restore it pays off more. A green pg_restore says nothing about the part that decides the outcome. If you are sorting out your server landscape anyway, my look at Hetzner’s AI inference API covers a neighbouring corner of the same infrastructure question.
If you run Coolify yourself and are not sure whether your backup holds up in an emergency, I am happy to take a look. A glance at the .env, the backup schedules and the question of which volumes currently sit in no schedule is usually enough for a clear answer. You can reach me through the contact form or by email.
FAQ
Is a Coolify backup in S3 worthless without the APP_KEY?+
No, that only applies to the backup of the Coolify instance itself. Coolify creates the dumps of your application databases with pg_dump, mysqldump, mariadb-dump or mongodump and stores them without any additional encryption. If you have noted the S3 credentials independently of Coolify, you can reach those files at any time and load them into any database by hand. What is lost without the key is the content of Coolify's own database, meaning SSH keys, environment variables, database passwords and S3 credentials.
Where do I find the APP_KEY of my Coolify instance?+
In the file /data/coolify/source/.env on the Coolify host, as the line APP_KEY=base64:... The documentation names exactly this step in the restore procedure. On top of that, the install script writes a dated copy of the file before every merge, so a host that has been running for a while may also hold files such as .env-20260727-113000 containing older keys.
What is APP_PREVIOUS_KEYS for?+
That is a Laravel feature, not a Coolify feature. Laravel always encrypts with the current APP_KEY, but when decrypting it first tries the current key and then works through everything listed in APP_PREVIOUS_KEYS, comma separated. When restoring onto a new server you enter the old key there. What matters is copying the value exactly, including the base64 prefix, because the Coolify docs show an example without the prefix at that point.
Does Coolify back up Docker volumes as well?+
No. The migration documentation states explicitly that Coolify has no built-in way to migrate applications from one server to another, and it requires databases and volumes to be copied manually. The documented route for volumes is a throwaway container that mounts the volume and a backup directory and archives the contents with tar.
Want to know more?
In a free intro call we discuss how you can use these topics for your company. Not a sales pitch, but an honest assessment.
Book a free intro call



