From Dokku to Traefik, migration of a blog

After tooling with docker swarm for a while, I dropped it and chosen to deploy a more common Traefik + docker-compose stack. This post will walk you through my process of migrating over my ghost blog.

From Dokku to Traefik, migration of a blog

This will probably be a short and simple post as recently I decided to finally move my blog off from my dokku based server to a bigger arm box from oracle and thought my experience may be useful to someone.

Photo by AbsolutVision on Unsplash

Some context

After tooling with docker swarm for a while, destroying my mastodon instance in the meantime, I dropped it and chosen to deploy a more common Traefik + docker compose stack on my big server from oracle. Their free tier is huge, giving away 4 core arm CPUs and 24gb of RAM.  My previous server used dokku to compile a Dockerfile based app and deploy it, and apps were proxies using nginx. I tried to bring it over as I like dokku, but it doesn't support arm very well and bugged during the installation process.

This pushed me to move to a previously tried and used tech, Traefik.

Architecture of Traefik workings, source Traefik docs

The chosen stack

My server is going to run apps using docker without any fancy build process for the moment, as I want to have a more reliable experience with fewer complications and maintainability issues.

The proxy delivering my requests to my docker containers is, as said before, Traefik a very useful tool allowing to route requests to a lot of different platforms, including docker. My Traefik installation is configured in a standard way, picking up only containers I manually configure via docker labels.

      - traefik.enable=true
      - traefik.http.routers.ghost-https.rule=Host(``)
      - traefik.http.routers.ghost-https.tls=true
      - traefik.http.routers.ghost-https.tls.certresolver=letsencrypt
      - traefik.http.routers.ghost-https.entrypoints=websecure
Here are my labels for my ghost container.

Traefik installation and configuration

The installation of traefik is very easy, but the amount of choice we have regarding the installation method may be overwhelming. In the end, I've chosen to create a docker compose file and have everything written down so that I could easily do changes without needing to mess up with the docker CLI too much. To be clearer, after one update to the file, all I have to do to update the container is run this command:

docker compose -f ./traefik-admin.yml up -d

In case it needs a breakdown, what this command does is it tells docker compose to create a stack comprised of multiple containers from my specification file and bring it up in daemon mode.

Traefik is configured to have two entry points, port 80 and 443. For the HTTPS traffic, it issues a free certificate for the domain specified in the labels with letsencrypt. The port of the destination container it reaches can be customized.

Photo by Erda Estremera on Unsplash


The migration path presented some issues and questions, but fortunately they were relatively easy to tackle.

Firstly I needed to test out if ghost docker could be set up on arm to run behind traefik and fortunately it can. I then needed to decide how to configure it, and again I opted for a docker compose file, with the docker container mounting the persistent storage volume in sub-folders to keep data near the config.

version: '3.8'

    image: ghost:latest
      - ./ghost:/var/lib/ghost/content
      - mysql
      # radacted

      - traefik.enable=true
      - traefik.http.routers.ghost-https.rule=Host(``)
      - traefik.http.routers.ghost-https.tls=true
      - traefik.http.routers.ghost-https.tls.certresolver=letsencrypt
      - traefik.http.routers.ghost-https.entrypoints=websecure

    image:  arm64v8/mysql
    restart: always
    hostname: ghost_mysql
      MYSQL_USER: ghost
      MYSQL_DATABASE: ghost
      - ./ghost-db:/var/lib/mysql

    external: true
    name: traefik
This is my config used to run the docker containers for my ghost blog.

After creating my container and verifying that it is reachable, I started to migrate my content.

File transfer

I moved my settings, the redirects, and routes using the easy to use ghost-labs options in settings.

Before starting to transfer the files over, make sure the logged-in user has the right to write the files into their destination, as docker may use different IDs for the user running ghost. I know there is a way to tell docker what user ID to use to avoid it, but I didn't have the patience to try it.

To transfer files over, images and themes mostly, I popped up my ssh connection to the two servers in Termius and used its SFTP function to move them from one server to the other one, effortlessly.

After you moved your files, please remember to reset the permissions to the correct user, so ghost doesn't break.

Now that we have our content, our server is mostly ready to use and only required small tweaks.

Future improvements

I see different improvements that I could and should apply:

  1. I should use Cloudflare proxy for my domain instead of exposing my IP address.
  2. Likewise, I should use the TLS challenge with Cloudflare to create my let's encrypt domains.
  3. I really need to prepare some serious backup/Ci solution to keep my sites safe in the future.
  4. I hope I can revive my mastodon instance and recover its data too.

I hope you enjoyed my article, and if you have any kind of opinion, advice, or experience regarding these topics please tell me, I'm eager to learn more.