<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://mediawiki.comfac.net/index.php?action=history&amp;feed=atom&amp;title=Portainer_to_Docker_Compose_Migration_Guide</id>
	<title>Portainer to Docker Compose Migration Guide - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://mediawiki.comfac.net/index.php?action=history&amp;feed=atom&amp;title=Portainer_to_Docker_Compose_Migration_Guide"/>
	<link rel="alternate" type="text/html" href="https://mediawiki.comfac.net/index.php?title=Portainer_to_Docker_Compose_Migration_Guide&amp;action=history"/>
	<updated>2026-06-05T09:46:39Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.45.1</generator>
	<entry>
		<id>https://mediawiki.comfac.net/index.php?title=Portainer_to_Docker_Compose_Migration_Guide&amp;diff=216&amp;oldid=prev</id>
		<title>Justinaquino: Created page with &quot;= Migrating from Portainer to Docker Compose =  == A Field-Tested Guide with Real Mistakes and How to Fix Them ==  &#039;&#039;&#039;Context:&#039;&#039;&#039; This tutorial was built from a real migration of a home server (Dell Optiplex 3070 Micros) running multiple services in Portainer, moved to pure Docker Compose under &lt;code&gt;/opt/stacks/&lt;/code&gt;. It includes every mistake made along the way and how to recover.  &#039;&#039;&#039;Target audience:&#039;&#039;&#039; Someone who knows basic Linux and Docker but may forget steps,...&quot;</title>
		<link rel="alternate" type="text/html" href="https://mediawiki.comfac.net/index.php?title=Portainer_to_Docker_Compose_Migration_Guide&amp;diff=216&amp;oldid=prev"/>
		<updated>2026-04-02T12:07:31Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;= Migrating from Portainer to Docker Compose =  == A Field-Tested Guide with Real Mistakes and How to Fix Them ==  &amp;#039;&amp;#039;&amp;#039;Context:&amp;#039;&amp;#039;&amp;#039; This tutorial was built from a real migration of a home server (Dell Optiplex 3070 Micros) running multiple services in Portainer, moved to pure Docker Compose under &amp;lt;code&amp;gt;/opt/stacks/&amp;lt;/code&amp;gt;. It includes every mistake made along the way and how to recover.  &amp;#039;&amp;#039;&amp;#039;Target audience:&amp;#039;&amp;#039;&amp;#039; Someone who knows basic Linux and Docker but may forget steps,...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;= Migrating from Portainer to Docker Compose =&lt;br /&gt;
&lt;br /&gt;
== A Field-Tested Guide with Real Mistakes and How to Fix Them ==&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Context:&amp;#039;&amp;#039;&amp;#039; This tutorial was built from a real migration of a home server (Dell Optiplex 3070 Micros) running multiple services in Portainer, moved to pure Docker Compose under &amp;lt;code&amp;gt;/opt/stacks/&amp;lt;/code&amp;gt;. It includes every mistake made along the way and how to recover.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Target audience:&amp;#039;&amp;#039;&amp;#039; Someone who knows basic Linux and Docker but may forget steps, mix up machines, or make assumptions that get them in trouble.&lt;br /&gt;
&lt;br /&gt;
== The Big Picture ==&lt;br /&gt;
&lt;br /&gt;
=== What we are doing ===&lt;br /&gt;
Moving services managed by Portainer (which has its own hidden config layer) into clean, self-contained Docker Compose stacks stored under &amp;lt;code&amp;gt;/opt/stacks/&amp;lt;/code&amp;gt;. Each service gets its own folder with a &amp;lt;code&amp;gt;compose.yml&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; file.&lt;br /&gt;
&lt;br /&gt;
=== Why this matters for backup ===&lt;br /&gt;
Everything lives under &amp;lt;code&amp;gt;/opt/stacks/&amp;lt;/code&amp;gt; — one folder to back up, one folder to restore. No hunting for Portainer&amp;#039;s hidden volumes or config files.&lt;br /&gt;
&lt;br /&gt;
=== The 3-2-1 backup goal ===&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Copy 1:&amp;#039;&amp;#039;&amp;#039; Live data on PC03 (primary server)&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Copy 2:&amp;#039;&amp;#039;&amp;#039; Warm backup on PC02 (sleeps, wakes via WoL 3x/day and on failure)&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Copy 3:&amp;#039;&amp;#039;&amp;#039; Synology NAS (cold archive, always on)&lt;br /&gt;
&lt;br /&gt;
== Before You Start: Know Your Environment ==&lt;br /&gt;
&lt;br /&gt;
=== Check which machine you are on ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
hostname&lt;br /&gt;
# Should print something like PC03dell3050&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Common mistake:&amp;#039;&amp;#039;&amp;#039; Running commands on PC02 when you think you are on PC03, or vice versa. Always check &amp;lt;code&amp;gt;hostname&amp;lt;/code&amp;gt; before doing anything destructive.&lt;br /&gt;
&lt;br /&gt;
=== Know your folder structure ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
/opt/stacks/&lt;br /&gt;
├── wordpress/&lt;br /&gt;
│   ├── compose.yml&lt;br /&gt;
│   ├── .env&lt;br /&gt;
│   ├── wordpress_data/    ← webroot files&lt;br /&gt;
│   └── db_data/           ← database files&lt;br /&gt;
├── mediawiki/&lt;br /&gt;
├── zulip/&lt;br /&gt;
├── openwebui/&lt;br /&gt;
└── ...&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
All data lives INSIDE the stack folder as bind mounts. No named Docker volumes. This makes backup and restore straightforward.&lt;br /&gt;
&lt;br /&gt;
=== Know your user situation ===&lt;br /&gt;
Most of these operations require root. If you are logged in as a regular user:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo -i&lt;br /&gt;
# or&lt;br /&gt;
su -&lt;br /&gt;
# enter root password when prompted&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;If you cannot write to /opt/stacks/,&amp;#039;&amp;#039;&amp;#039; you are not root. Do not proceed until you are.&lt;br /&gt;
&lt;br /&gt;
== Step 0: Preparation Checklist ==&lt;br /&gt;
&lt;br /&gt;
Before migrating any service, do these once:&lt;br /&gt;
&lt;br /&gt;
=== Create the shared Docker network ===&lt;br /&gt;
All services and NPM (your reverse proxy) share this network:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create npm_proxy&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Safe to run even if it already exists — it will just say it already exists.&lt;br /&gt;
&lt;br /&gt;
=== Create the base stacks directory ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir -p /opt/stacks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 1: Read the Portainer Stack ==&lt;br /&gt;
&lt;br /&gt;
In Portainer, go to &amp;#039;&amp;#039;&amp;#039;Stacks → [stack name] → Editor&amp;#039;&amp;#039;&amp;#039; and copy the entire compose content.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;What to look for:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Field !! What it tells you&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;image:&amp;lt;/code&amp;gt; || The Docker image and version&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ports:&amp;lt;/code&amp;gt; || Host port → container port mapping&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;volumes:&amp;lt;/code&amp;gt; || Where data lives — see below&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;environment:&amp;lt;/code&amp;gt; || Credentials and config — &amp;#039;&amp;#039;&amp;#039;do not lose these&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;container_name:&amp;lt;/code&amp;gt; || The exact name Docker uses — you will need this&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Understanding volumes — the most important thing ===&lt;br /&gt;
&lt;br /&gt;
Portainer stacks can use two types of volumes:&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Type 1 — Named volumes&amp;#039;&amp;#039;&amp;#039; (Portainer manages them, hidden location):&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot;&amp;gt;&lt;br /&gt;
volumes:&lt;br /&gt;
  - mydata:/var/lib/data&lt;br /&gt;
volumes:&lt;br /&gt;
  mydata:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
The actual files are at &amp;lt;code&amp;gt;/var/lib/docker/volumes/STACKNAME_mydata/_data/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Type 2 — Bind mounts&amp;#039;&amp;#039;&amp;#039; (explicit path on the host):&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot;&amp;gt;&lt;br /&gt;
volumes:&lt;br /&gt;
  - /home/user/myapp/data:/var/lib/data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
The files are exactly where the path says.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;How to tell which type you have:&amp;#039;&amp;#039;&amp;#039; If the volume line has a full path starting with &amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt;, it is a bind mount. If it is just a name with no &amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt;, it is a named volume.&lt;br /&gt;
&lt;br /&gt;
== Step 2: Find Where the Data Actually Is ==&lt;br /&gt;
&lt;br /&gt;
=== For named volumes — Portainer renames them ===&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Critical:&amp;#039;&amp;#039;&amp;#039; Portainer adds the stack name as a prefix. If your stack is named &amp;lt;code&amp;gt;wp240927&amp;lt;/code&amp;gt; and your volume is named &amp;lt;code&amp;gt;db&amp;lt;/code&amp;gt;, the actual Docker volume name is &amp;lt;code&amp;gt;wp240927_db&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Find the real names:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls | grep -i KEYWORD&lt;br /&gt;
# Example:&lt;br /&gt;
docker volume ls | grep -i wordpress&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Find the actual path on disk:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume inspect VOLUMENAME&lt;br /&gt;
# Look for &amp;quot;Mountpoint&amp;quot; in the output&lt;br /&gt;
# Example output:&lt;br /&gt;
# &amp;quot;Mountpoint&amp;quot;: &amp;quot;/var/lib/docker/volumes/wp240927_db/_data&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== For bind mounts ===&lt;br /&gt;
Just check if the path exists:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ls /home/user/wp250223-8070/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Create the New Stack Files ==&lt;br /&gt;
&lt;br /&gt;
=== Directory structure ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir -p /opt/stacks/SERVICENAME/data_folder_name&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Create compose.yml ===&lt;br /&gt;
Use &amp;lt;code&amp;gt;cat &amp;gt;&amp;lt;/code&amp;gt; to write the file directly in the terminal. This avoids file transfer issues:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cat &amp;gt; /opt/stacks/SERVICENAME/compose.yml &amp;lt;&amp;lt; &amp;#039;EOF&amp;#039;&lt;br /&gt;
services:&lt;br /&gt;
  app:&lt;br /&gt;
    image: someimage:latest&lt;br /&gt;
    restart: unless-stopped&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;PORT:PORT&amp;quot;&lt;br /&gt;
    environment:&lt;br /&gt;
      SOME_VAR: ${SOME_VAR}&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ./data:/app/data&lt;br /&gt;
    networks:&lt;br /&gt;
      - proxy&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  proxy:&lt;br /&gt;
    external: true&lt;br /&gt;
    name: npm_proxy&lt;br /&gt;
EOF&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Key changes from the Portainer version:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* Remove &amp;lt;code&amp;gt;version:&amp;lt;/code&amp;gt; line (obsolete in modern Compose)&lt;br /&gt;
* Change &amp;lt;code&amp;gt;restart: always&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;restart: unless-stopped&amp;lt;/code&amp;gt;&lt;br /&gt;
* Move credentials out of &amp;lt;code&amp;gt;environment:&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; using &amp;lt;code&amp;gt;${VAR_NAME}&amp;lt;/code&amp;gt; syntax&lt;br /&gt;
* Change named volume paths to &amp;lt;code&amp;gt;./foldername&amp;lt;/code&amp;gt; (relative bind mounts)&lt;br /&gt;
* Add the &amp;lt;code&amp;gt;npm_proxy&amp;lt;/code&amp;gt; network so NPM can reach the service&lt;br /&gt;
&lt;br /&gt;
=== Create .env file ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cat &amp;gt; /opt/stacks/SERVICENAME/.env &amp;lt;&amp;lt; &amp;#039;EOF&amp;#039;&lt;br /&gt;
DB_NAME=mydb&lt;br /&gt;
DB_USER=myuser&lt;br /&gt;
DB_PASSWORD=change_this_password&lt;br /&gt;
EOF&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Critical:&amp;#039;&amp;#039;&amp;#039; The values in &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; must match what is already inside your database files. If you copy the DB files from the old stack, use the same credentials the old stack had. Changing them will break the connection.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;How to find the original credentials:&amp;#039;&amp;#039;&amp;#039; Before stopping the old stack, go to Portainer → Stack → Editor and read the environment variables.&lt;br /&gt;
&lt;br /&gt;
== Step 4: Copy the Data ==&lt;br /&gt;
&lt;br /&gt;
=== Stop the old Portainer stack first ===&lt;br /&gt;
Go to Portainer → Stacks → [stack name] → &amp;#039;&amp;#039;&amp;#039;Stop&amp;#039;&amp;#039;&amp;#039;.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Do NOT delete it yet.&amp;#039;&amp;#039;&amp;#039; It is your rollback. Only delete after you confirm the new stack works.&lt;br /&gt;
&lt;br /&gt;
=== For bind mount data (path already known) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rsync -av --progress \&lt;br /&gt;
  /old/path/data/ \&lt;br /&gt;
  /opt/stacks/SERVICENAME/data/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;The trailing slash on the source matters.&amp;#039;&amp;#039;&amp;#039; With a trailing slash, rsync copies the contents. Without it, rsync copies the folder itself (creating an extra nesting level).&lt;br /&gt;
&lt;br /&gt;
=== For named volume data (path from docker volume inspect) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rsync -av --progress \&lt;br /&gt;
  /var/lib/docker/volumes/STACKNAME_VOLUMENAME/_data/ \&lt;br /&gt;
  /opt/stacks/SERVICENAME/data/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== For data on another machine (SSH) ===&lt;br /&gt;
If rsync is available on both machines:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Run on the destination machine, pulling from source&lt;br /&gt;
rsync -av --progress -e ssh \&lt;br /&gt;
  root@SOURCE_IP:/var/lib/docker/volumes/STACKNAME_VOLUMENAME/_data/ \&lt;br /&gt;
  /opt/stacks/SERVICENAME/data/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If rsync is NOT installed on the source machine:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# On source machine: create a tar archive&lt;br /&gt;
tar -czvf /home/user/service-backup.tar.gz \&lt;br /&gt;
  /var/lib/docker/volumes/STACKNAME_vol1/_data \&lt;br /&gt;
  /var/lib/docker/volumes/STACKNAME_vol2/_data&lt;br /&gt;
&lt;br /&gt;
# Transfer via SCP from destination&lt;br /&gt;
scp root@SOURCE_IP:/home/user/service-backup.tar.gz /opt/stacks/SERVICENAME/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then extract directly into target folders:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cd /opt/stacks/SERVICENAME&lt;br /&gt;
&lt;br /&gt;
tar -xzvf service-backup.tar.gz \&lt;br /&gt;
  --strip-components=6 \&lt;br /&gt;
  -C data/subfolder \&lt;br /&gt;
  var/lib/docker/volumes/STACKNAME_vol1/_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;The &amp;lt;code&amp;gt;--strip-components&amp;lt;/code&amp;gt; number&amp;#039;&amp;#039;&amp;#039; = how many path levels to strip. Count the slashes in &amp;lt;code&amp;gt;var/lib/docker/volumes/STACKNAME_vol1/_data&amp;lt;/code&amp;gt; — that is 6 components (var, lib, docker, volumes, STACKNAME_vol1, _data).&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;If you cannot write to /opt/stacks/ via SFTP:&amp;#039;&amp;#039;&amp;#039; Upload to &amp;lt;code&amp;gt;/home/user/&amp;lt;/code&amp;gt; instead, then move:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv /home/user/service-backup.tar.gz /opt/stacks/SERVICENAME/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Fix file ownership after copying ===&lt;br /&gt;
Different services run as different users inside their containers:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Service !! uid:gid !! Why&lt;br /&gt;
|-&lt;br /&gt;
| WordPress (webroot) || 33:33 || www-data&lt;br /&gt;
|-&lt;br /&gt;
| MariaDB / MySQL || 999:999 || mysql&lt;br /&gt;
|-&lt;br /&gt;
| PostgreSQL || 70:70 || postgres&lt;br /&gt;
|-&lt;br /&gt;
| Most Node apps || 1000:1000 || node&lt;br /&gt;
|-&lt;br /&gt;
| RabbitMQ || 999:999 || rabbitmq&lt;br /&gt;
|-&lt;br /&gt;
| Redis || 999:999 || redis&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
chown -R 33:33 /opt/stacks/wordpress/wordpress_data/&lt;br /&gt;
chown -R 999:999 /opt/stacks/wordpress/db_data/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 5: Start the New Stack ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cd /opt/stacks/SERVICENAME&lt;br /&gt;
docker compose up -d&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Watch the logs:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose logs -f&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Press &amp;lt;code&amp;gt;Ctrl+C&amp;lt;/code&amp;gt; to stop watching logs. &amp;#039;&amp;#039;&amp;#039;This does NOT stop the containers.&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&lt;br /&gt;
Test locally before checking the public URL:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -I http://localhost:PORT&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Expected responses:&lt;br /&gt;
* &amp;lt;code&amp;gt;200 OK&amp;lt;/code&amp;gt; — service is up and working&lt;br /&gt;
* &amp;lt;code&amp;gt;301 Moved Permanently&amp;lt;/code&amp;gt; — service is up, redirecting (usually HTTP→HTTPS, this is fine)&lt;br /&gt;
* &amp;lt;code&amp;gt;500 Internal Server Error&amp;lt;/code&amp;gt; — service is up but something is wrong (usually credentials)&lt;br /&gt;
* &amp;lt;code&amp;gt;Connection refused&amp;lt;/code&amp;gt; — container is not running or wrong port&lt;br /&gt;
&lt;br /&gt;
== Common Mistakes and How to Fix Them ==&lt;br /&gt;
&lt;br /&gt;
=== Mistake 1: Old container still running, new one cannot start ===&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Symptom:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Error response from daemon: Conflict. The container name &amp;quot;/myapp&amp;quot; is already in use&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Cause:&amp;#039;&amp;#039;&amp;#039; Stopping a Portainer stack does not always stop the containers. Portainer stopped managing them but left them running.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Fix:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rm -f CONTAINERNAME&lt;br /&gt;
# Then retry:&lt;br /&gt;
docker compose up -d&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Mistake 2: Wrong credentials in .env — database refuses connection ===&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Symptom:&amp;#039;&amp;#039;&amp;#039; &amp;lt;code&amp;gt;500 Internal Server Error&amp;lt;/code&amp;gt; or &amp;quot;Error establishing database connection&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Cause:&amp;#039;&amp;#039;&amp;#039; The &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; credentials do not match what is stored inside the database files. The database files remember what credentials they were initialized with.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Diagnosis — check what the container actually sees:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose exec SERVICENAME env | grep DB_&lt;br /&gt;
# or for WordPress specifically:&lt;br /&gt;
docker compose exec wordpress env | grep WORDPRESS_DB&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Diagnosis — test the credentials directly:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose exec db mariadb -u USERNAME -p&amp;#039;PASSWORD&amp;#039; DBNAME -e &amp;quot;SELECT 1;&amp;quot;&lt;br /&gt;
# If it returns &amp;quot;1&amp;quot;, those credentials work.&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Fix:&amp;#039;&amp;#039;&amp;#039; Edit &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; to match the credentials that actually work, then do a FULL restart:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose down&lt;br /&gt;
docker compose up -d&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Critical:&amp;#039;&amp;#039;&amp;#039; &amp;lt;code&amp;gt;docker compose restart&amp;lt;/code&amp;gt; does NOT re-read &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt;. You must use &amp;lt;code&amp;gt;down&amp;lt;/code&amp;gt; + &amp;lt;code&amp;gt;up&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Mistake 3: Portainer volume names are different from what the compose file says ===&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Symptom:&amp;#039;&amp;#039;&amp;#039; &amp;lt;code&amp;gt;docker volume inspect VOLUMENAME&amp;lt;/code&amp;gt; returns &amp;lt;code&amp;gt;no such volume&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Cause:&amp;#039;&amp;#039;&amp;#039; Portainer prefixes volumes with the stack name. If your stack is &amp;lt;code&amp;gt;wp240927&amp;lt;/code&amp;gt; and your volume is &amp;lt;code&amp;gt;db&amp;lt;/code&amp;gt;, the real name is &amp;lt;code&amp;gt;wp240927_db&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Fix:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls | grep -i KEYWORD&lt;br /&gt;
# Use the full name shown in the output&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Mistake 4: Data lands in the wrong place after extraction ===&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Symptom:&amp;#039;&amp;#039;&amp;#039; &amp;lt;code&amp;gt;ls data/postgresql/&amp;lt;/code&amp;gt; shows &amp;lt;code&amp;gt;_data&amp;lt;/code&amp;gt; instead of actual database files&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Cause:&amp;#039;&amp;#039;&amp;#039; &amp;lt;code&amp;gt;--strip-components&amp;lt;/code&amp;gt; count was wrong, or the tar had an unexpected nesting level.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Fix — check what is actually there:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
find /opt/stacks/SERVICENAME -name &amp;quot;PG_VERSION&amp;quot; 2&amp;gt;/dev/null&lt;br /&gt;
find /opt/stacks/SERVICENAME -name &amp;quot;*.conf&amp;quot; 2&amp;gt;/dev/null | head -5&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then move the contents up:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv data/postgresql/_data/* data/postgresql/&lt;br /&gt;
rmdir data/postgresql/_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Mistake 5: .env changes not taking effect ===&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Symptom:&amp;#039;&amp;#039;&amp;#039; You edited &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; and restarted, but the container still uses old values.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Cause:&amp;#039;&amp;#039;&amp;#039; &amp;lt;code&amp;gt;docker compose restart&amp;lt;/code&amp;gt; only restarts the process, it does not rebuild the environment.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Fix:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose down&lt;br /&gt;
docker compose up -d&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Verify the new values are actually injected:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose exec SERVICENAME env | grep VARNAME&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Mistake 6: NPM proxy still pointing to old IP/port after migration ===&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Symptom:&amp;#039;&amp;#039;&amp;#039; Service is running locally (&amp;lt;code&amp;gt;curl -I http://localhost:PORT&amp;lt;/code&amp;gt; returns 200) but the public domain gives 502 Bad Gateway.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Cause:&amp;#039;&amp;#039;&amp;#039; NPM&amp;#039;s proxy host was pointing to the old machine&amp;#039;s IP or old port.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Fix:&amp;#039;&amp;#039;&amp;#039; In NPM → Proxy Hosts → Edit the domain → update Forward Hostname to the new IP or &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Mistake 7: Zulip/service doing HTTP→HTTPS redirect loop through NPM ===&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Symptom:&amp;#039;&amp;#039;&amp;#039; &amp;quot;This page isn&amp;#039;t redirecting properly&amp;quot; error in browser.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Cause:&amp;#039;&amp;#039;&amp;#039; Service internally redirects HTTP to HTTPS, but NPM is sending HTTP. The service sends back a 301 to HTTPS, which loops.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Fix:&amp;#039;&amp;#039;&amp;#039; In NPM, change the scheme to &amp;lt;code&amp;gt;https&amp;lt;/code&amp;gt; and expose the service&amp;#039;s HTTPS port instead of the HTTP port. Also disable SSL verification in NPM since services often use self-signed certs.&lt;br /&gt;
&lt;br /&gt;
== Verifying Everything Works ==&lt;br /&gt;
&lt;br /&gt;
After bringing up each service:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# 1. Check containers are running&lt;br /&gt;
docker compose ps&lt;br /&gt;
&lt;br /&gt;
# 2. Check logs for errors&lt;br /&gt;
docker compose logs --tail=20&lt;br /&gt;
&lt;br /&gt;
# 3. Test local connectivity&lt;br /&gt;
curl -I http://localhost:PORT&lt;br /&gt;
&lt;br /&gt;
# 4. Check the public domain loads&lt;br /&gt;
# (Do this in your browser)&lt;br /&gt;
&lt;br /&gt;
# 5. Verify data survived (service-specific)&lt;br /&gt;
# For WordPress: log into wp-admin and check posts&lt;br /&gt;
# For Wiki: browse to a page you know exists&lt;br /&gt;
# For Zulip: log in and check message history&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== After Confirming a Service Works ==&lt;br /&gt;
&lt;br /&gt;
Wait 24–48 hours of stable operation, then:&lt;br /&gt;
&lt;br /&gt;
# In Portainer: &amp;#039;&amp;#039;&amp;#039;Delete&amp;#039;&amp;#039;&amp;#039; the old stack&lt;br /&gt;
# Clean up old data directories if they were bind mounts:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rm -rf /home/user/old-stack-data/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
# Clean up orphaned named volumes:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls | grep OLDSTACKNAME&lt;br /&gt;
docker volume rm OLDSTACKNAME_volumename&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backup Strategy (What This All Feeds Into) ==&lt;br /&gt;
&lt;br /&gt;
Once all services are under &amp;lt;code&amp;gt;/opt/stacks/&amp;lt;/code&amp;gt;, the backup is simple:&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;PC03 → Synology (3x daily via Restic):&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
restic -r sftp:synology:/backups/pc03 backup /opt/stacks/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;PC02 warm backup:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* PC02 sleeps most of the time&lt;br /&gt;
* Synology watchdog pings PC03 every 60s&lt;br /&gt;
* If PC03 fails 3 checks → Synology sends WoL magic packet → PC02 wakes&lt;br /&gt;
* PC06 relay (iptables DNAT) switches traffic to PC02&lt;br /&gt;
* When PC03 comes back → Restic syncs PC02→PC03 → manual confirm → traffic switches back&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;PC00 desktop → Synology (daily):&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
restic -r sftp:synology:/backups/pc00 backup /home/USERNAME/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Quick Reference: Ports Used ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Service !! Port !! Domain&lt;br /&gt;
|-&lt;br /&gt;
| WordPress || 8070 || blog.gi7b.org&lt;br /&gt;
|-&lt;br /&gt;
| MediaWiki || 8191 || wiki.gi7b.org&lt;br /&gt;
|-&lt;br /&gt;
| Open WebUI || 3000 || —&lt;br /&gt;
|-&lt;br /&gt;
| SearXNG || 8290 || —&lt;br /&gt;
|-&lt;br /&gt;
| Zulip || 8010 (HTTP), 8011 (HTTPS) || chat.gi7b.org&lt;br /&gt;
|-&lt;br /&gt;
| Forgejo || 3001 || —&lt;br /&gt;
|-&lt;br /&gt;
| Gemini sites || 27081 || —&lt;br /&gt;
|-&lt;br /&gt;
| Jitsi || TBD || meet.gi7b.org&lt;br /&gt;
|-&lt;br /&gt;
| Grafana || TBD || —&lt;br /&gt;
|-&lt;br /&gt;
| Frappe || TBD || —&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Giving This Context to a New AI Chat ==&lt;br /&gt;
&lt;br /&gt;
Paste this block at the start of a new conversation:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
I am continuing a home server migration project. Here is the context:&lt;br /&gt;
&lt;br /&gt;
Setup:&lt;br /&gt;
- PC03 (192.168.196.76) = primary server, Dell Optiplex 3070, Ubuntu, Docker Compose&lt;br /&gt;
- PC02 (192.168.196.189) = warm backup, same hardware, sleeps until needed&lt;br /&gt;
- PC06 = Contabo Japan VPS, pure iptables DNAT relay (no NGINX), forwards traffic to PC03&lt;br /&gt;
- Synology NAS = always on, cold backup archive, runs watchdog container&lt;br /&gt;
- ZeroTier connects all machines&lt;br /&gt;
&lt;br /&gt;
All stacks live under /opt/stacks/SERVICENAME/ on PC03&lt;br /&gt;
Each stack has: compose.yml, .env, and data folders as bind mounts&lt;br /&gt;
Shared Docker network: npm_proxy (external)&lt;br /&gt;
Reverse proxy: NGINX Proxy Manager (NPM) running as its own stack on PC03&lt;br /&gt;
&lt;br /&gt;
Completed migrations (Portainer → Docker Compose):&lt;br /&gt;
- WordPress (blog.gi7b.org) → port 8070 ✓&lt;br /&gt;
- MediaWiki (wiki.gi7b.org) → port 8191 ✓ (check status)&lt;br /&gt;
- Open WebUI → port 3000 ✓&lt;br /&gt;
- SearXNG → port 8290 ✓&lt;br /&gt;
- Zulip (chat.gi7b.org) → port 8010/8011 ✓ (troubleshooting NPM HTTPS)&lt;br /&gt;
- Forgejo → port 3001 ✓ (fresh install, no data migration)&lt;br /&gt;
- Gemini-sites → port 27081 ✓&lt;br /&gt;
&lt;br /&gt;
Still to do:&lt;br /&gt;
- Jitsi (meet.gi7b.org)&lt;br /&gt;
- Grafana + Prometheus&lt;br /&gt;
- Frappe / ERPNext&lt;br /&gt;
- PC06 iptables watchdog script&lt;br /&gt;
- Synology WoL watchdog container&lt;br /&gt;
- Restic backup setup (PC03→Synology, PC02 agent, PC00→Synology)&lt;br /&gt;
- Clean up abandoned Mastodon volumes on PC03&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Tutorials]]&lt;br /&gt;
[[Category:Docker]]&lt;br /&gt;
[[Category:System Administration]]&lt;br /&gt;
[[Category:Self-Hosting]]&lt;/div&gt;</summary>
		<author><name>Justinaquino</name></author>
	</entry>
</feed>