A simple website and blog using docker, traefik, nginx & ghost

version: '3.7'

services:

  acme:
    container_name: acme
    image: nginx:latest
    restart: unless-stopped

    networks:
      - traefik

    environment:
      - PGID=1000
      - PUID=1000

    volumes:
      - ./acme:/usr/share/nginx/html

    expose:
      - 80

    labels:
      - traefik.enable=true
      - traefik.http.routers.acme.rule=(Host(`www.acme.com`) || Host(`acme.com`))
      - traefik.http.routers.acme.entrypoints=websecure
      - traefik.http.routers.acme.tls=true
      - traefik.http.routers.acme.tls.certresolver=l
      - traefik.http.routers.acme.priority=10

    ghost:
    container_name: acme-blog
    image: ghost
    restart: unless-stopped

    expose:
      - 2368

    depends_on:
      - db

    networks:
      - traefik
      - ghost

    volumes:
      - ./acme-blog/content:/var/lib/ghost/content

    environment:
      # see https://ghost.org/docs/config/#configuration-options
      database__client: mysql
      database__connection__host: db
      database__connection__user: root
      database__connection__password: super_secret_password
      database__connection__database: ghost

      url: https://www.acme.com/blog

      # contrary to the default mentioned in the linked documentation,
      # this image defaults to NODE_ENV=production (so development mode
      # needs to be explicitly specified if desired)
      #NODE_ENV: development

    labels:
      - traefik.enable=true
      - "traefik.http.routers.acme_blog.rule=(Host(`www.acme.com`) && PathPrefix(`/blog`)) || (Host(`acme.com`) && PathPrefix(`/blog`))"
      - traefik.http.services.acme_blog.loadbalancer.server.port=2368
      - "traefik.http.middlewares.acme_blog-strip.stripprefix.prefixes=/blog"
      - traefik.http.routers.acme_blog.entrypoints=websecure
      - traefik.http.routers.acme_blog.tls=true
      - traefik.http.routers.acme_blog.tls.certresolver=le
      - traefik.http.routers.acme_blog.priority=100

  db:
    image: mysql:8.0
    restart: unless-stopped

    volumes:
      - ./acme-blog/db:/var/lib/mysql
      - /etc/localtime:/etc/localtime:ro

    expose:
      - 3306

    networks:
      - ghost

    environment:
      MYSQL_ROOT_PASSWORD: super_secret_password

networks:
  ghost:
  traefik:
    external: true

Design & Deployment Notes

  • All data/volumes for this stack are intended to be contained in a user folder. Docker will create the appropriate folders when it starts up the containers for the first time. If docker creates the folders they will have root permissions/ownership. It is recommended that ownership is change to a standard user account. Alternatively, one can create the folders prior to starting the stack.
  • The priority needs to be defined otherwise traefik will route www.acme.com/blog to the nginx container and not the ghost container.
  • The traefik priority will not work in docker-compose.yml files 3.1. Articles on the Internet that version 3.2 or higher will work. Version 3.7 does work.
  • On initial startup it takes a while for the mysql container to startup and initialize this can delay ghost from starting up, so you may see ghost failing with an exit code of zero or two. Be patient. Eventually, the InnoDB initialization will complete and ghost will startup properly.
  • The stripprefix line is required because the ghost application expects to be at the root URL path (i.e. "/") and not in a folder/subpath.