Before we get started, you might be wondering why you should deploy your Apostrophe website using Docker. Docker allows developers to deploy applications while keeping each part of the application separate. In this guide, we're going to create one custom Docker image for our app but use five other images as well:
- Apostrophe - Our custom Apostrophe website image
- Mongo - Database for our Apostrophe app
- Portainer - GUI for managing Docker
- Nginx Proxy Manager - Manages NGINX and Let's Encrypt certificates
- MariaDB - Database for Nginx Proxy Manager
- Watchtower - Auto-updates images
Note: While advancements for Apostrophe 3 continue, this tutorial is focused specifically on deploying Apostrophe 2 on Docker. There are plans to document and share updates on this same topic for Apostrophe 3 in the future.
Setting up the Server
For this example, I'll be using a VPS on DigitalOcean running RancherOS with 2 CPUs and 2GB RAM. RancherOS a lightweight OS designed for hosting containers. If you'd prefer to use another distribution, you'll need to make sure Docker Engine is installed. Instructions for installing Docker Engine can be found in the Docker documentation. Any cloud provider will work just fine but I'd suggest the specs above as minimums.
Log into the server using
rancher@<server_ip> so we can start provisioning!
Next we're going to install Portainer. Portainer provides a gui for Docker. While it isn't technically necessary, it makes using Docker a little nicer.
Create a volume for portainer.
docker volume create portainer_data
Run the container.
docker run -d -p 8000:8000 -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:alpine
Now you can access Portainer in the browser at <ip>:9000. After visiting the page, create a user. On the next page, select Docker as the environment and click connect.
Now we're at the Portainer dashboard. To make links in Portainer work, we'll want to add our IP as the public IP. On the sidebar under settings, click "Endpoints" and then "local" under name in the table. Enter the IP of your server under "Public IP".
Creating and hosting a Docker Image
For this example, I'll be deploying a repo based on our boilerplate Apostrophe 2 repo. If you've started your project based on that repo, it should be easy to get up and running by looking at the Docker files (
.dockerignore) in the repo. Also, make sure the version of apostrophe is at least
2.116.1. If you want to follow along without deploying one of your projects, click the "Use this template" button.
We'll be hosting our Docker image over on DockerHub so you'll need to create an account over there if you don't yet have one. After you're logged in, click on "Create Repository". Then give your image a name, link it to a repo, and add a build rule, making sure to check that the source branch name is correct. This will tell DockerHub to build a new image whenever it sees an update to the linked branch. Finish by clicking "Create & Build".
💡Note: While DockerHub can host images for many different architectures, it only builds amd64 images. If you plan on deploying on an ARM processor, for example, you'll want to use something like this GitHub Action. Stay tuned for more info on how to do this in a future blog post!
Now we'll need to wait for the image to be built before we can use it. Once you see a green checkmark next to an item under "Recent Builds" of the image's page on DockerHub, we're ready to continue.
Deploying a Stack in Portainer
Time to deploy our Apostrophe site! Log back into Portainer (
<ip>:9000), then click "Stacks" in the sidebar, and "+ Add Stack" at the top of the main section of the page. Now, give your stack a name. I'm going to use
apostrophe. Next, we'll want to paste in the contents of a
docker-compose.yml with a couple updates. The file in the boilerplate repo is made for use in development but now we have a built image hosted on DockerHub. So we'll need to delete
build: . under the apostrophe service and add
image: <your-dockerhub-handle>/<your-dockerhub-repo> instead.
Also, I'd recommend tagging mongo with the latest major and minor version, which is 4.4 at the time of writing. This can always be updated manually later but will prevent your database from unexpectedly breaking. Here's the config I'm using:
version: "2" services: apostrophe: image: <your-dockerhub-handle>/<your-dockerhub-repo> restart: always ports: - "3000:3000" volumes: - ./data/uploads:/app/public/uploads environment: - APOS_MONGODB_URI=mongodb://mongo:27017/db depends_on: - mongo mongo: image: mongo:4.4 restart: always volumes: - ./data/db:/data/db
💡 If you're new to Docker it's worth noting that the
mongo portion of the
APOS_MONGODB_URI is a reference to the
mongo container name. Since Docker also handles networking, Docker will replace this with the IP of the
mongo container name. We'll also use this when setting up the Nginx container.
Once you're done, click "Deploy the stack" at the bottom of the page. After waiting for the deploy to finish, you should be able to see the Apostrophe site at
SSH to Add Admin User
Now that we've deployed our Apostrophe application, we'll want to add a user. We can do this by clicking on the
>_ icon under quick actions next to the apostrophe container.
Next, click "Connect" and we're in. Now we can run
node app.js apostrophe-users:add admin admin to create an admin user. Enter a password and we're done. We can visit
<ip>:3000/login to use the login we just created.
Configuring Nginx with Let's Encrypt
Now that Apostrophe is setup, time to connect a domain and setup Nginx. To do this we'll use Nginx Proxy Manager - a GUI for Nginx with support for Let's Encrypt.
Before we start setting that up, we'll want to create some A records that point to our server. I'd suggest creating three - one for each service (Apostrophe, Portainer, and Nginx).
First, add a new stack in Portainer by clicking on "Stacks" in the sidebar and then "+ Add Stack". Name the stack npm (short for for nginx proxy manager, not what you're thinking of) and copy in the contents below. Then replace the password fields with the passwords you would like to use and click "Deploy the stack".
version: "2" services: npm: container_name: npm image: jc21/nginx-proxy-manager:2 restart: always networks: - default - proxy_network ports: - '80:80' - '443:443' - '81:81' environment: DB_MYSQL_HOST: "mariadb" DB_MYSQL_PORT: 3306 DB_MYSQL_USER: "npm" DB_MYSQL_PASSWORD: "<good-password>" DB_MYSQL_NAME: "npm" volumes: - ./data/npm:/data - ./data/letsencrypt:/etc/letsencrypt depends_on: - mariadb mariadb: image: mariadb:10.5 restart: always environment: MYSQL_ROOT_PASSWORD: '<good-root-password>' MYSQL_DATABASE: 'npm' MYSQL_USER: 'npm' MYSQL_PASSWORD: '<good-password>' volumes: - ./data/mysql:/var/lib/mysql networks: default: proxy_network: driver: bridge
Now we should have a successfully deployed Nginx stack. Before we configure Nginx, we'll need to add our Portainer and Apostrophe containers to the Nginx container's proxy network. For Apostrophe, click "Stacks" in the sidebar, then
apostrophe, then "Editor". We're going to update it to match the config below.
version: "2" services: apostrophe: image: <your-dockerhub-handle>/<your-dockerhub-repo> restart: always networks: - default - proxy_network ports: - "3000:3000" volumes: - ./data/uploads:/app/public/uploads environment: - APOS_MONGODB_URI=mongodb://mongo:27017/db depends_on: - mongo mongo: image: mongo:4.4 restart: always volumes: - ./data/db:/data/db networks: default: proxy_network: external: name: npm_proxy_network
To update the Portainer container, we'll need to remove the existing Portainer container and start a new one to add it to the Nginx container's network. To do so we'll need to ssh into the server again. After that, run
docker ps -a to see all containers. You should see something like the output below.
dockerroot@a3-docker-demo:~# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 781e3780d22b <your-dockerhub-handle>/<your-dockerhub-repo> "docker-entrypoint.s…" 12 minutes ago Up 13 seconds 3000/tcp apostrophe af894847fb7d mongo:4.4 "docker-entrypoint.s…" 12 minutes ago Up 12 minutes 27017/tcp mongo 133d74e6b0a5 portainer/portainer-ce:alpine "/portainer" 24 minutes ago Up 7 minutes 0.0.0.0:8000->8000/tcp, 0.0.0.0:9000->9000/tcp portainer 7248f73385ac jc21/nginx-proxy-manager:2 "/init" 3 hours ago Up 24 minutes (healthy) 0.0.0.0:80-81->80-81/tcp, 0.0.0.0:443->443/tcp npm 17d1d1d921f1 mariadb:10.5 "docker-entrypoint.s…" 3 hours ago Up 3 hours 3306/tcp mariadb
First, we'll need to stop and remove the Portainer container. We'll need to reference the container id of the Portainer container above. Run
docker stop <portainer-container-id> then
docker rm <portainer-container-id>. Then we'll start a new Portainer container with the same command as before except adding the Nginx network using
docker run -d -p 8000:8000 -p 9000:9000 --name=portainer --restart=always --network="npm_proxy_network" -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:alpine.
Now that we can continue configuring Nginx, visit
<ip>:81. The default login is
changeme. After logging in, you'll be prompted to change it. Now that we're logged in, click "Dashboard" at the top, then "0 Proxy Hosts", and "Add Proxy Host".
This is how we can add each domain into Nginx. Under "Domain Names" type the name of the domain you'd like to use for Apostrophe and click the "add ..." dropdown. Then type apostrophe (the name of the docker container) under "Forward Hostname / IP" and 9000 under "Forward Port". Then click to the SSL tab and under the "SSL Certificate" dropdown, click "Request a new SSL Certificate". Toggle on "Force SSL" and "HTTP/2 Support", enter an email to use with Let's Encrypt, and toggle to agree to the Let's Encrypt ToS. Click "Save" and we're done! In the top right click "Add Proxy Host" and repeat this process for Apostrophe (port 3000) and Nginx proxy manager (port 81).
Optionally, we can do some cleanup to not publicly expose port 3000 for our Apostrophe app since it can be accessed via Nginx. Click "Stacks", the apostrophe stack, and then the "Editor" tab. Delete the
ports: line and the line below it exposing port 3000. Similarly, if we would like to expose the mongo ports, that can be done by adding a couple lines including
ports: under the apostrophe stack.
The last step is to setup auto-update for our containers. Before doing this, it is important that tags are used correctly as noted in previous steps. Otherwise, by using the default
latest tag, you will automatically get new major versions which could cause your app to break. So you'll want to make sure you aren't using
latest tags for anything except your Apostrophe container which you control.
To add the Watchtower container, click "Containers" in the sidebar, then "+ Add container". Give it a name of
watchtower and use the image
containrrr/watchtower. Under "Advanced container settings" > "Volumes", click "+map additional volume". Set the top line to "Bind" and enter
/var/run/docker.sock under both container and host. Then click the "Env" tab so we can add some environment variables. The list of all options can be found here. I like to add the variables
TZ, set the timezone you are in. This makes the time in the container logs match up with your timezone. For me, that's
America/Los_Angeles. If you're not sure what to put, this Wikipedia page should help. For
WATCHTOWER_CLEANUP I set it to
true which automatically deletes old images after updating. Finally,
WATCHTOWER_POLL_INTERVAL is the time (in seconds) that Watchtower checks for updates. I like to use
300 which is every 5 minutes. Now, click "Deploy the container".
We're done! Now you should have an Apostrophe website deployed with Docker Engine using Nginx (with Let's Encrypt certs) and Watchtower for keeping your containers running on the latest image. To take this a step further we could also add a staging Apostrophe stack for testing before production. It's also worth noting that this isn't a HA (highly available) deployment so if minimizing downtime is extremely important, you'll want to deploy your Docker containers on Kubernetes instead. That being said, this guide should be great for getting started with Docker and deploying small to medium sized websites.