VPS + DNS + Docker + Portainer + Caddy + Ghost = Here we are!

After over 3 hours of me banging my head against the wall, I've finally figured out how to get all this junk working and now I have a blog!

Unfortunately, I'm still learning a lot so parts of these instructions may be extraneous and I welcome everyone smarter than me to help me learn!

Here's how I got it from ground zero:

Step 1: Get a VPS and a domain

I used Vultr as my VPS but you could use DigitalOcean, etc. I personally have grown to like Debian as my Linux distro of choice so I'll be using that.
Blogs use very little resources so if you're only doing this, you could get away with the lowest amount of stuff.

I used Namecheap but you can use Google Domains, etc.

Step 2: Point your domain at your VPS

Namecheap makes it pretty easy to add DNS records so you just have to add 2 A records:

  • One with a host of * to pick up all the subdomains
  • another with a host of @ to pick up the www and domain.tld versions of your URL.

They'll both use your VPS's IP address in the value section.

Step 3: Installing Docker

I just used this set of instructions from Docker to install it. The gist is:

  • apt update
  • Add their repo
  • Install Docker and its dependencies

Most likely, you'll be root in your VPS but if you made a user, feel free to add yourself to the docker group so you don't have to sudo every time.

The command would be /usr/sbin/usermod -aG docker <username>.

Step 4: Installing Portainer

I personally really enjoy using Portainer since it simplifies doing a lot actions in Docker. To install it is pretty straightforward.

Following these instructions, just creating a volume and the container is enough. Once it's ready, we'll be doing stuff in Portainer.

Go to https://<your-ip>:9443 and finish the initial setup.

To be safe, we should be setting up the VPS with a VPN and connecting through there but for speed just have a good username/password.

Step 5: Installing Caddy

Once we have Portainer installed, the rest is pretty straightforward! I was pleasantly surprised about how fluid setting up environments and such are these days.

First, make sure that you have the configs/caddy folder set up. Then we can create a Caddyfile there already or else it will be angry and say you're trying to map a file to a directory.

Here's a sample one that I used:

<REDACTED>.orewa.pw {
	reverse_proxy http://172.17.0.2:9000
}

blog.orewa.pw {
	reverse_proxy ghost:2368
	header {
		header_up Strict-Transport-Security max-age=31536000; includeSubDomains; preload
		header_up X-Forwarded-Proto {scheme}
		header_up X-Forwarded-For {remote}
		header_up X-Real-IP {remote}
	}
}

Caddyfile

The first entry is for the Portainer instance. I think (?) I could probably use the container name instead of the Docker IP but it works... This way I don't have to go to a port, just a nice ole subdomain.

The second entry has some settings that I saw help with reverse proxying for this Ghost blog.

I love docker compose (especially coming from typing hundreds of docker commands), it simplifies and speeds up the process for me.

In Portainer, they're called stacks. Here's the one I used:

version: "3.9"

services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - /root/configs/caddy/Caddyfile:/etc/caddy/Caddyfile
      - /root/configs/caddy/site:/srv
      - caddy_data:/data
      - caddy_config:/config
    network_mode: bridge

volumes:
  caddy_data:
  caddy_config:

Portainer Caddy stack

Once you run the stack, Caddy should be up and running!

Step 6: Setting up Ghost

Now that we're more acclimated with Portainer stacks, here's the one for Ghost! It sets up the Ghost and the MySQL containers. You just have to set the root password as the same thing for both and your URL.

version: '3.1'

services:
  ghost:
    container_name: ghost
    image: ghost:5-alpine
    restart: always
    ports:
      - 2368:2368
    environment:
      database__client: mysql
      database__connection__host: db
      database__connection__user: root
      database__connection__password: <PASSWORD>
      database__connection__database: ghost
      url: https://<your-url>
      NODE_ENV: development
    volumes:
      - ghost:/var/lib/ghost/content

  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: <PASSWORD>
    volumes:
      - db:/var/lib/mysql

volumes:
  ghost:
  db:

Portainer Ghost stack

Badabing badaboom, your Ghost stack is up.

Once it's up, you can go to https://<YOUR-URL>/ghost and set up your blog! You don't have to use a real email if you don't want.

Conclusion

Now you have Portainer and a blog running! The cool part is you can extrapolate from the Caddyfile and Portainer stacks to spin up as many containers you want! Very exciting stuff.

Good luck hacking!