Installing contentjet with Docker

In this guide we will install contentjet onto a single host running Ubuntu 16.04 with Docker. By the end of this guide you will have a complete installation of contentjet secured with free TLS certificates provided by Let's Encrypt.

To complete this installation you must possess the following knowledge:

  • Navigating a linux command line, specifically the BASH shell
  • Connecting to a remote server over SSH
  • Configuration of DNS
  • An understanding of Docker

You must also have:

  • A server running a fresh install of Ubuntu 16.04 which you can SSH into (DigitalOcean is a good choice)
  • A domain name registered through a registrar which gives you full access to the domain's DNS records

Configure DNS

We will host contentjet with the following 3 subdomains as follows:

  • app.example.com Will host the contentjet frontend, contentjet-ui
  • api.example.com Will host the contentjet backend, contentjet-api
  • media.example.com Will host user uploaded media

You must log into your domain registrar and create 3x A records for app, api and media all pointing to the IP address of your server.

Install Docker & Docker Compose

Connect to your server over SSH and install Docker.

curl -fsSL get.docker.com -o get-docker.sh
sh get-docker.sh

Refer to the official documentation on installing Docker Compose.

Generate certificates with Let's Encrypt

In this step we will generate the free certificates for your 3 subdomains.

Begin by creating our working directory at /opt/contentjet which we will run all subsequent commands from.

mkdir -p /opt/contentjet
cd /opt/contentjet

Run the following command to start a temporary server on port 80. This will create 2 named volumes which are used for storing the certificates generated in the next step as well as the challenge files required as part of Let's Encrypt's validation step.

docker run \
  --name temp-server \
  --rm \
  -d \
  -v certs:/etc/letsencrypt \
  -v certs-data:/data/letsencrypt \
  -w /data/letsencrypt \
  -p 80:8000 \
  python:alpine3.7 \
  python -m http.server 8000

Next, run the following command to request certificates from Let's Encrypt. Make sure you change the last 4 lines of this command to reflect your own email address and domain.

docker run \
  -it \
  --rm \
  -v certs:/etc/letsencrypt \
  -v certs-data:/data/letsencrypt \
  certbot/certbot \
  certonly \
  --preferred-challenges http \
  --agree-tos \
  --webroot \
  --webroot-path=/data/letsencrypt \
  --non-interactive \
  -m youremail@example.com \
  -d app.example.com \
  -d api.example.com \
  -d media.example.com

Assuming the above command executes successfully the certificates will have been written to our named volume. You MUST now stop the temporary server.

docker stop temp-server

Now create a cron job which runs every 5 days to automatically renew your certificates. Edit your cron with crontab -e and add the following:

0 0 */5 * *  docker run --rm -v certs:/etc/letsencrypt -v certs-data:/data/letsencrypt certbot/certbot renew -n >> /var/log/certbot.log

Configure NGINX

Next we will configure NGINX. Copy the following text and save it to /opt/contentjet/nginx.conf on your server.

user              nginx;
worker_processes  1;

pid               /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include                   /etc/nginx/mime.types;
    default_type              application/octet-stream;

    ssl_session_cache         shared:SSL:20m;
    ssl_session_timeout       10m;

    ssl_protocols             TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers               "ECDH+AESGCM:ECDH+AES256:ECDH+AES128:!ADH:!AECDH:!MD5;";

    ssl_stapling              on;
    ssl_stapling_verify       on;

    ssl_certificate           /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key       /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_trusted_certificate   /etc/letsencrypt/live/example.com/chain.pem;

    add_header                Strict-Transport-Security "max-age=31536000" always;

    access_log                /dev/stdout;
    error_log                 /dev/stderr info;

    # Docker's internal DNS
    resolver                  127.0.0.11;

    server {
        listen 80 default_server;
        server_name *.example.com;

        location / {
            return 301 https://$host$request_uri;
        }

        location ^~ /.well-known {
            allow all;
            root  /data/letsencrypt/;
        }
    }

    server {
        listen      443           ssl http2;
        listen [::]:443           ssl http2;
        server_name               app.example.com;

        location / {
            proxy_pass                http://ui:9000;
            proxy_http_version        1.1;
            proxy_set_header          Host $host;

            gzip                      on;
            gzip_min_length           1000;
            gzip_proxied              any;
            gzip_types                application/javascript
                                      application/json
                                      text/css
                                      image/svg+xml;
        }
    }

    server {
        listen      443           ssl http2;
        listen [::]:443           ssl http2;
        server_name               api.example.com;

        client_max_body_size      200M;

        location / {
            proxy_pass                http://api:3000;
            proxy_http_version        1.1;
            proxy_set_header          Upgrade $http_upgrade;
            proxy_set_header          Connection 'upgrade';
            proxy_set_header          Host $host;
            proxy_cache_bypass        $http_upgrade;
        }
    }

    server {
        listen      443           ssl http2;
        listen [::]:443           ssl http2;
        server_name               media.example.com;

        expires                   1M;

        root                      /opt/contentjet-api/media/;
    }

}

We now need to change every occurance of example.com to your actual domain. We can do this easily using sed. For example if your domain name was acme.com you would run the following command from within the /opt/contentjet/ directory.

sed -i -e 's/example.com/acme.com/g' nginx.conf

Configure Docker Compose

Next copy the following and save it to /opt/contentjet/docker-compose.yml.

version: '3.4'
services:
  nginx:
    image: nginx:1.12.2
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - certs:/etc/letsencrypt
      - certs-data:/data/letsencrypt
      - media:/opt/contentjet-api/media/
  db:
    image: postgres:9.6.2
    restart: always
    environment:
      POSTGRES_USER: yourdbuser # CHANGE ME
      POSTGRES_PASSWORD: yourdbpassword # CHANGE ME
      POSTGRES_DB: contentjet-api
  api:
    image: contentjet/contentjet-api
    restart: always
    environment:
      NODE_ENV: production
      POSTGRES_HOST: db
      POSTGRES_USER: yourdbuser # CHANGE ME
      POSTGRES_PASSWORD: yourdbpassword # CHANGE ME
      POSTGRES_DB: contentjet-api
      SECRET_KEY: yoursupersecretkey # CHANGE ME
      SMTP_HOST: smtphost # CHANGE ME
      SMTP_PORT: smtpport # CHANGE ME
      SMTP_USER: smtpuser # CHANGE ME
      SMTP_PASSWORD: smtppassword # CHANGE ME
      MAIL_FROM: noreply@example.com # CHANGE ME
      BACKEND_URL: https://api.example.com
      MEDIA_URL: https://media.example.com
      FRONTEND_URL: https://app.example.com
    volumes:
      - media:/opt/contentjet-api/media/
  ui:
    image: contentjet/contentjet-ui
    restart: always
    environment:
      BACKEND_URL: https://api.example.com
      PORT: 9000
volumes:
  media:
  certs:
    external: true
  certs-data:
    external: true

Similar to the change we made to nginx.conf we need to replace all occurances of example.com with your domain name. Again, this can be done quickly using sed by running the following from within the /opt/contentjet directory.

sed -i -e 's/example.com/acme.com/g' docker-compose.yml

We need to make some additional edits to the environment variables within this file. As you can see there are 4 services defined nginx, db, api and ui. You MUST provide values for the lines commented with #CHANGE ME.

SECRET_KEY simply needs to be a unique random string of your choosing. Make sure you keep it secret as it's used in encrypting passwords and tokens.

Run services

docker-compose up -d

Docker will now automatically pull the necessary images and start all services. To confirm everything is running navigate to https://app.yourdomain.com.

Create an administrator

At this point contentjet should be running on your domain however there are no users! Create your first user by running the following command and entering your email address and your desired password at the prompt.

docker-compose exec api npm run create-admin-user