Portable Blog - Docker As a Web Host
Or how I learned to stop worrying and love Docker Containers.
Migrating a blog is a pain in the ass. Backup databases, backup files, move them, install blog software, restore, figure out what broke and why and then fix it. I am building a blog to chronicle the tech projects I'm working on but I can't be bothered with maintaining a website. So I'm going to see what I can do with Docker to make a portable blog.
Blog Platform
I like WordPress a lot but I find that much of my free time and energy is spent makeing WordPress do cool things. That detracts from how much I am able to publish. For this blog I plan for content generation to be the primary focus.
So, I am backing Ghost hoping it's minimalism will help inspire me to write. It's supposed to have built in SEO and social media features. Hopefully it will live up to the hype. I love it's markdown editor.
How do I do it the simple way
For a super simple setup:
docker run -d --name ghostBlog -p 80:2368 -v ghostBlog_content:/var/lib/ghost/content ghost
navigate to http://localhost/ghost and set up your new blog.
The problem with this is that there is no SSL so you wont want to login over the wilds of the internet but you could certainly run a local copy and push your content volume to a public server.
Or you could make your own image based on ghost and add in SSL but the docker way is to pile containers on containers so lets look at that.
How do I do it the complex way
I'm leveraging docker compose as it makes more sense to me. I can take these 3 files (and my volumes) to another docker host and spin up my blog in seconds.
~/blog/docker-compose.yaml
~/blog/nginx/default.conf
~/blog/nginx/DockerFile
First the orcestration file.
#~/blog/docker-compose.yaml
version: '3'
services:
nginx:
build: ./nginx
container_name: nginx
volumes:
- certs:/etc/letsencrypt
- certs-data:/data/letsencrypt
ports:
- 80:80
- 443:443
ghost:
restart: always
image: ghost
volumes:
- content:/var/lib/ghost/content
volumes:
certs:
certs-data:
content:
Ghost
Lets start with the ghost container as it is super simple. Docker-compose will get the latest image of ghost and create a volume for us called content. That volume makes this container stateful (it remembers it's settings)
Normally you might put in something like ports: 8080:2368 which would expose 8080 on the host and map it to ghost's default port but I'm going to set up an Nginx container to act as a reverse proxy.
Nginx
The first service in my compose file is Nginx. I am building a custom image using a DockerFile but it's 99% vanilla Nginx.
#~/blog/nginx/DockerFile
FROM nginx
COPY ./default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80 443
On build, docker-compose will take the nginx image and pull a switch-a-roo on default.conf. This will replace the default nginx splash page with my reverse proxy settings.
#~/blog/nginx/default.conf
# Theres lots of good Nginx documentation on setting up ssl. I'm just showing the broad strokes here
server {
listen 80;
server_name myblog.com;
location / {
proxy_pass http://ghost:2368;
...
}
location ^~ /.well-known {
allow all;
root /data/letsencrypt/;
}
}
server {
listen 443 ssl http2;
server_name myblog.com;
location / {
proxy_pass http://ghost:2368;
...
}
}
Note that proxy_pass is the line where traffic is being redirected and http://ghost:2368 is not an example, it's a legit url. Because both containers are in the same services block of the docker-compose file, docker compose links them and assigns a hostname of ghost and nginx respectivly (true of version 3).
If you are building your containers some other way you will likely need to account for the networking between the Nginx container and the Ghost container. Remember Docker containers are isolated from the host and eachother.
Letsencrypt
You may be saying "what's with the certs volumes and location ^~ /.well-known? I'm using yet another container to run certbot and letsencrypt to get and renew SSL certificates. The volumes are how certbot passes the cert and acme challenge to nginx.
# initial SSL cert setup
docker run -it --rm \
-v certs:/etc/letsencrypt \
-v certs-data:/data/letsencrypt \
certbot/certbot \
certonly \
--webroot --webroot-path=/data/letsencrypt \
-d myblog.com
Interactive for the first time and then to renew I have a crontab set up.
# SSL renew
0 0 */15 * * docker run -t --rm -v certs:/etc/letsencrypt -v certs-data:/data/letsencrypt certbot/certbot renew --webroot --webroot-path=/data/letsencrypt