iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🍨

The Ultimate Docker Setup: Running Django and Ghost Together on a Single VPS

に公開

Introduction

For individual projects or in the early phases of a startup, there is often a need to operate both a custom Web application (Django) and an owned media site (Ghost) at low cost.

This is a memo of how I built a deployment environment on ConoHa VPS using a Docker environment with a powerful reverse proxy container called HTTPS-Portal to meet the following requirements.

Achieved Architecture and Benefits

Architecture Diagram

  • VPS: ConoHa VPS
  • Frontend proxy: HTTPS-Portal (Nginx + Let's Encrypt auto-renewal)
  • App A: Django + PostgreSQL (The Web app itself)
  • App B: Ghost + MySQL (CMS/Blog)

Benefits of this architecture

  1. Automated SSL Certificate Renewal: HTTPS-Portal handles the acquisition and renewal of Let's Encrypt certificates fully automatically.
  2. Multi-domain support: Domain-based routing is possible simply by writing a single line of environment variables.
  3. High-speed delivery of static files: Django static files are served directly from the Nginx (HTTPS-Portal) side, reducing the load on the application.

Overview of docker-compose.yml

The following is the definition file created for this setup.

docker-compose.yml
services:
  # --- Django App Section ---
  db:
    image: postgres:15.2
    container_name: postgres
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: pg_isready -U "${POSTGRES_USER:-postgres}" || exit 1
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    env_file:
      - .env

  app:
    container_name: app
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./static:/app/static
      - ./media:/app/media
    expose:
      - "8000"
    command: sh -c "python manage.py migrate && gunicorn mySite.wsgi:application --bind 0.0.0.0:8000"
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy

  # --- Ghost CMS Section ---
  ghost-db:
    image: mysql:8.0
    container_name: ghost-db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${GHOST_DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ghost
      MYSQL_USER: ghost
      MYSQL_PASSWORD: ${GHOST_DB_PASSWORD}
    volumes:
      - ghost_db_data:/var/lib/mysql

  ghost:
    image: ghost:6.10.2
    container_name: ghost
    restart: always
    depends_on:
      - ghost-db
    ports:
      - "2368:2368"
    environment:
      database__client: mysql
      database__connection__host: ghost-db
      database__connection__user: ghost
      database__connection__password: ${GHOST_DB_PASSWORD}
      database__connection__database: ghost
      url: sampleA_Ghost.com  # Change to your actual production domain
      NODE_ENV: production
    volumes:
      - ghost_content:/var/lib/ghost/content

  # --- Proxy / SSL Section ---
  https-portal:
    image: steveltn/https-portal:1
    container_name: https-portal
    ports:
      - "80:80"
      - "443:443"
    environment:
      # Define domains and target containers here
      DOMAINS: 'sampleA_Ghost.com -> http://ghost:2368, sampleB_Django.com -> http://app:8000'
      STAGE: 'production' # Use 'production' for live, 'staging' for testing
    volumes:
      # Mount points to serve Django static files from Nginx
      - ./static:/var/www/vhosts/sampleB_Django.com/static
      - ./media:/var/www/vhosts/sampleB_Django.com/media
      - https-portal-data:/var/lib/https-portal
    depends_on:
      - app
      - ghost

volumes:
  db_data:
  https-portal-data:
  ghost_content:
  ghost_db_data:

Key Configuration Points

I will explain three important points for keeping this configuration running stably.

1. Routing Settings via HTTPS-Portal

The DOMAINS environment variable in the https-portal container is the core of the routing.

DOMAINS: 'your-media-site.com -> http://ghost:2368, your-app-site.com -> http://app:8000'

By simply writing the configuration in a comma-separated format like this, you can complete the reverse proxy settings per domain and obtain SSL certificates without having to edit Nginx configuration files.

2. Django Static File Serving (Important)

When running Django in a production environment (Gunicorn), static files such as CSS and images are not served by Django itself. Usually, you need to rely on Nginx or a similar tool.

In this configuration, we solve this by utilizing Docker Volume sharing.

  1. Django container: Collect files into ./static using collectstatic.
  2. HTTPS-Portal container: Mount the same ./static to /var/www/vhosts/domain-name/static.

Since HTTPS-Portal (the internal Nginx) is designed to prioritize files located under /var/www/vhosts/domain-name/ by default, this allows Nginx to serve static files at high speed without passing through Django.

Reflecting Static Files

Immediately after starting the containers, the Django static files (CSS and JavaScript) will not exist on the volume, so the layout of the admin panel and other pages may appear broken.

Run the following command in the terminal on the VPS to gather the static files in one place:

docker-compose exec app python manage.py collectstatic --noinput

3. Ghost URL Setting

You also need to correctly configure the url environment variable on the Ghost side. If this mismatches the actual public URL, it will cause broken links or image paths within your articles.

environment:
  url: sampleA_Ghost.com

Deployment Steps

  1. Preparing Environment Variables (.env)
    For security reasons, passwords and similar credentials should be written in a .env file.
.env
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypassword
POSTGRES_DB=mydb
GHOST_DB_ROOT_PASSWORD=rootpass
GHOST_DB_PASSWORD=ghostpass
  1. Adjusting Django Settings
    On the Django side, configure the output destination for static files and the settings for trusted proxies.
settings.py
# settings.py
STATIC_ROOT = BASE_DIR / 'static'
MEDIA_ROOT = BASE_DIR / 'media'

# Setting to trust headers from HTTPS-Portal
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
ALLOWED_HOSTS = ['your-app-site.com']
  1. Starting and Initializing Containers
# Build and start
docker-compose up -d --build

# Gather Django static files
docker-compose exec app python manage.py collectstatic --noinput

With this, after waiting a few minutes, the Let's Encrypt certificates will be issued, and both sites will become accessible via HTTPS.

Summary

Using a single VPS to host both a Python-based Web application and a Node.js-based CMS is a highly effective way to keep initial costs down. By utilizing https-portal, you can free yourself from complex Nginx configuration and certificate management, allowing you to focus on the development itself.

Discussion