DigitalOcean, docker-compose, nginx, and Vapid CMS

Creating an almost-static website from scratch

What is this? A walkthrough of how to set up a simple, content-driven website from scratch.

Who is it for? If you know how to write HTML, use a command line, and manage a small codebase, but you’re not sure how to get your website live, then this is probably for you. (It’s the tutorial I wish I had when I started out).

What do I need? A text editor (I use Atom), Docker and docker-compose installed on your own computer, a GitHub account (optional-ish), a bit of perseverance when things go wrong, and some money (around £5/month for the server, around £15/year for the domain).

What’s the plan? First, I’ll explain what the project is going to be, and why I’ve chosen this particular tech stack. Then, I’ll walk through the build step-by-step, referring to other tutorials where relevant. How to deploy is the next stage, but thanks to Docker it’s pretty easy. Finally, I’ll wrap up with some further ideas and notes.

The Project

For this article, I’ll be building a small website listing superpowers that would be terrible to have (on the premise that if random mutations are going to cause some people to have awesome powers, there must be a much greater number of people with really rubbish ones). I want to be able to add to this list whenever I think of a terrible power, and I obviously want the website secure.

We need a simple app running to provide the content, which I’ll write using Vapid and TailwindCSS. To manage incoming connections including certification, I’ll set up an nginx reverse proxy alongside the app. Both will be orchestrated by Docker and docker-compose, and it’ll be hosted on DigitalOcean.

Tools, Services, Products, and Frameworks

  • DigitalOcean
  • (optional)
  • Docker and docker-compose
  • Vapid
  • TailwindCSS
  • Github
  • Lets Encrypt and Certbot


Server Trouble

In October, I received an unwelcome email from the web hosting provider I had been using which informed me that they’d been the victim of a somewhat hostile takeover, and that they would be migrating all of their customers on to their new overlord’s servers. Not to worry, they said, very few customers will need to change anything. Guess who didn’t fall into that happy group?

I began shopping around for a new provider. I’m not a web developer professionally — the reason I had picked the original service, WebFaction, in the first place was its beginner-friendliness — so I was left somewhat adrift in seas dominated by leviathans like AWS and Google Cloud and filled with shoals of smaller competitors who all seemed to rely on you being either a complete beginner (“try our website builder!”) or a DevOps expert (“simply install your operating system from scratch”).

I wound up choosing DigitalOcean for several reasons:

  1. The documentation seemed good — this was very important to me as I anticipated (correctly) that I’d be searching through it a lot.
  2. I felt like I understood their product reasonably clearly: rent some space on a server and set it up as you wish.
  3. They turned up frequently in searches for beginner-friendly services.
  4. The pricing was competitive.

Overall, I felt they offered a reasonable amount of flexibility for someone who was familiar with coding, Linux, and the command line, but weren’t (necessarily) focused on the more complicated infrastructural needs of whole businesses.

Unchained from Django

Day-to-day, I’m a Python developer, and back when I was writing the apps I was trying to migrate, I knew little else. Django was my framework of choice and I wielded it like a sledgehammer, setting up the entire monolith even when I needed nothing more than a static website. As the time came for me to migrate my little websites over, I knew I could do better.

The breakthrough came when I stumbled across this article, actually written last year, listing several of the authors’ favourite management systems. One caught my eye: Vapid CMS. Vapid is based off a really nifty little idea: for many simple sites where the database is basically just there to serve content, the HTML itself can act as the database schema. By stripping away the need to handle the data model for the content you’re serving, you can save a lot of time and really focus on the design of the website itself.

It’s not perfect. As the homepage itself says, it’s still in beta and development is relatively slow. There’s a lot it can’t do, and, being quite an opinionated framework, there’s a lot it won’t do. However, if you’re putting together pages like blogs, news feeds, or portfolios, it may be exactly what you need, and it was just what I was after.

Contain Yourself

Let’s talk Docker quickly.

I’d started using Docker at work. A lot of the things I work on are small(ish) microservices deployed on AWS and I’d gotten quite familiar with setting up Dockerfiles and so on. The DevOps system at work is set up pretty neatly: push changes to the main branch and whoosh, Terraform takes it away and you see it live in a few minutes. It’s super neat and very convenient, but seemed like a lot of set-up work for a set of almost-static websites.

However, I did want to include Docker as part of the build. I don’t need to go into too much detail — others have explained the many merits and drawbacks of Docker elsewhere. Docker simplifies installation (and therefore setup), helps standardise best practices through official images, and eases translation between development and server environments. Setting everything up can definitely sometimes be a fight… but I’ve found it’s often less of a fight than doing it without Docker.

The Build

The server

First things first, you need a server. A server is just a computer, somewhere, which is always available to handle requests from people visiting your website. I’m going to rent one with DigitalOcean for the reasons explained above.

Go to and set up an account . Once that’s done, you’ll be taken to a control Panel. In the sidebar, click “New Project”. You’ll be prompted to enter a name for your project (“Terrible Powers”), a brief description, and the purpose of the project (“Website or blog”). If you get asked if you want to move resources into your new project, just “Skip for now”.

On the next page, there’s a big button saying “Get Started with a Droplet”. Click it — this is exactly what we want to do. (In DigitalOcean world, a “Droplet” is the remote computer you’re working on.) Then, on the following screen, choose the following:

  1. A version of Ubuntu (I’ve gone with 18.04)
  2. “Basic” plan at the cheapest available tier
  3. A datacenter near you (I tend to pick London)
  4. SSH authentication (setup instructions here)
  5. A hostname for your server — I tend to find a name like “terrible-powers-server” keeps things clear.

Everything else is pretty optional and up to you — if you’re unsure about what you’re looking at, I recommend just googling “digitalocean” alongside whatever it is. When you’re done, click “Create Droplet”. It’ll take several seconds to start up, but once that’s done you’ll see this:

Congrats on your new computer!

For the next steps, you need to set up your new virtual machine as a server, then install Docker and docker-compose. I’ll just refer you to the DigitalOcean tutorials for this:

  1. Initial Server Set-up
  2. Install Docker (steps 1 and 2 only)
  3. Install docker-compose

The domain

If your server is your house, then your domain is your address. This is technically optional, as your server will be visitable using just its IP Address (the number next to the name) but nobody wants to have to type that in.

Start by registering (buying, sigh) a domain name with a domain name registrar — any will do. I’m based in the UK (could you tell from my accent?) so have used in the past, and I’ve also found offer good prices. Do some shopping — you may find a better deal elsewhere.

Once you’ve finished registering, you need to tell your registrar where to find your new server by changing the nameservers. This process is pretty similar from registrar to registrar, and once again, DigitalOcean has some pretty good documentation for some common registrars. Here’s what it looks like on where I’ve just bought

I genuinely didn’t think I’d be able to get hold of for this tutorial. 🎉

You also need to tell DigitalOcean about your domain. In your control panel, under “Manage ➡ Networking”, add your new domain (e.g. “”) to your project. This will open a new panel listing three NS records for that domain. You also need to add two more “A” records, one for the domain itself and one for the “www” subdomain we’ll be directing to later using nginx.

  1. Type “@” into the hostname box, choose your new server from the dropdown, and click “Create Record”
  2. Type “www” into the hostname box, choose your new server from the dropdown, and click “Create Record”
More records than the CIA.

Now the good news: it’s time for a break. It usually takes at least an hour for all these nameservers and domains to resolve themselves (web stuff, I dunno, I’m not an expert) so go and make a coffee or something. You’re going to need it, the next steps are where the fun starts.

App Setup

Now, we’re going to use Docker, docker-compose, and Vapid to create and run this simple website locally. Somewhere on your own computer, create a new project folder and add into it a subdirectory called something like “app” (I like to keep the subdirectories focused on purpose rather than content as it improves consistency between projects).

Inside “app”, create two new folders called “www” and “data”. “www” is where Vapid will look for the source files for your website (any HTML, CSS, Javascript, etc.) and “data” is where it places files for the content, including the database and uploaded images. You shouldn’t need to touch anything inside data, but you can have a look inside after you’ve fired up Vapid to see what I mean. Next, we need four files to get going.

First, create your main page, index.html, inside the “www” folder. For the setup, I’m going to keep this simple:

<h1>A Terrible Start</h1>

Next, inside “app”, we need a package.json to tell our app what to install, a Dockerfile to tell our app how to install it, and a docker-compose.yml to tell it all how to run.

Note that this docker-compose.yml is only for local development, and so we can attach our local folders “data” and “www” as volumes. We’ll put another docker-compose.yml in the top-level project directory for production, which will handle both the app and the nginx reverse proxy.

Setup complete! Your files should look something like this:

Terrible powers or UNLIMITED power? You decide.

Using a command line, navigate to the “app” directory and run docker-compose up. After a little time, you should be able to navigate to localhost:3000 in a browser, and see your fancy new title.

App, App, and Away

Now, the really fun bit, and the bit that’s most up to you. Build the site! Add HTML files to “www”, add CSS to “www/css”, add Javascript to “www/javascripts”… you get the idea. If you get stuck, have a look at the vapid docs, or at the code for this site. The following steps are only a guide to my own build process — follow along or customise as you like.

  1. I like to use Tailwind CSS, so you’ll see that one of my first commits to the repository is a number of changes to add Tailwind to the build process.
  2. Next, I like to set up the basic HTML using Vapid’s templating. This helps consolidate the structure of the site, without worrying too much about styles for now.
  3. After that’s done, it’s worth going to the Vapid dashboard (localhost:3000/dashboard) and adding in some of the data. Don’t worry about the email and password while running locally — just put in anything.
  4. Add some style. When using Tailwind, this mainly means just adding classes into the HTML.
Eh… you can see what I was going for.

Reverse Proxy

The next step is to set up an nginx service acting as a reverse proxy. This part of the setup allows us some finer-grained control over incoming connections, as well as allowing us to separate the configuration related to certification and security from the app itself.

I’ve found the easiest way to set up the nginx configuration is to use this community tool, which generates a set of config files for you. For this application, we can use the “Node.js” preset. Put in your domain (“”), enable the www subdomain, and then, in the “Global Config”, set the “Lets Encrypt webroot” to /var/www/certbot/ (this is to help a little bit of consistency later on). Once that’s gone, press the button “Download the Config” and unzip the file onto your computer.

Copy the contents of the downloaded folder into a new folder called “nginx” inside your top-level project directory.

Isn’t the internet wonderful? It would have taken me months to get nginx configured like this on my own.

We need to make a few tweaks. First, in the main “nginx.conf”, change the line ssl_dhparam /etc/nginx/dhparam.pem; to ssl_dhparam /etc/nginx/ssl-dhparams.pem. I’m doing this because in a couple of steps we’ll be using a script to initialize all the SSL… and it’s just easier this way.

Next, in your equivalent of “”, you need to remove both lines starting with “ssl_trusted_certificate” — again, the script doesn’t set this up (it doesn’t need to), so we take it out.

Finally, set up a Dockerfile to build this nginx server. It’s very easy.

FROM nginx:latest
COPY . /etc/nginx/

Composition Time

Now, we need to set up our production docker-compose file, which should include “app” with a slightly modified build and run command, as well as nginx. I’ll talk you through it.

So: two services. “app” is more or less the same as before, but the build context is now pointing to the subdirectory “app” (and same for nginx). I’ve shifted the “data” volume to the top level, so it can be shared with the “certbot” data (more on that in a sec). Note we also no longer expose the Vapid app to the outside world: all connections are handled using the reverse proxy. (There’s also the new build arg — that’s for Tailwind. You probably won’t need it in your application unless you’re also using Tailwind, in which case feel free to refer to the repo to see how I’ve set things up)

Certbot and Magic

OK — I’ve referred to this special “script” a few times now. What is it? Well, it’s a true “shoulders of giants” situation here, because I’ve adapted it directly from this awesome tutorial. I cannot stress enough how much time and effort this saved me (and, judging by the number of claps, so many others). Congrats, and huge thanks, to Philipp. Here’s my version.

First, we need to add certbot to the composition, with a couple of volumes pointing to the right places in our production environment so that our certificates persist.

image: certbot/certbot
container_name: certbot
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

Next, the adapted script. All this does, same as the original, is create some fake certificates (enough to get your server running), then, while the server is running, deletes those fake ones, requests a proper set from Lets Encrypt, and, assuming everything is fine and dandy, restarts nginx with the proper set. You’ll need to change the domains variable to match yours, and you should add your email as well.

The main changes I’ve made are to shift around where files are saved to (just for consistency). Copy this script into your project directory, and run sudo chmod +x to make it runnable.

Phew 😅 build is done, time for deployment.



Step one is to copy the code into your server. You can do this directly, using a tool such as scp, or by committing your directory to somewhere like Github (my preferred option — who doesn’t love version control?). From there, you can log into your remote machine, and clone it in. Boom. Now you can keep track of any changes you might make remotely (and if you’re really fancy you could probably set up some kind of automatic deployment process on Github every time you push).


Now for the bit… that tends to go wrong. Navigate into your remote project folder, and run the initialization script. First, docker will build the images, and then run through the script step-by-step as described above. Now, this worked first time for me while putting together the tutorial, but in the past it has taken me hours to get everything set up correctly. The main problem was usually the locations of files: the script would save it to some directory, which was mapped to a volume that nginx was incorrectly mapped to, and so on. If you end up needing to debug, you can run the commands in the script yourself, substituting variables as you go. Pay close attention to the logs — nginx is often quite good at telling you what it’s missing.

If all goes to plan, you’ll see a nice little printout from Lets Encrypt and Certbot saying “Congratulations” and your script will exit successfully.


You are now moments away from having your fabulous website running on the real internet, and the really satisfying thing is at this point, it’s just one command:

docker-compose up

Give it a few moments, then try going to your domain in the browser (or mine, The Vapid server can take around a minute to start, so don’t be worried if you get a loading screen. Once it’s live, you can go to the dashboard, create a login, and then start producing your wonderful content.

Breathe a sigh. On to the next project!

What’s next?

“nginx”? “Docker”? “command line”? … What? If you’re just totally confused about the terms I’ve been using, and think you need a website-making tutorial for complete beginners, let me know in the comments!

I don’t want all that Vapid nonsense. Fair enough! For a totally static site, you can just serve it all up with nginx. Plonk your HTML/CSS/etc. assets into somewhere like “data/www/your_domain/public”, and go through the nginx config for a Frontend preset. You’ll need to make sure this www folder is mapped into the containers /var/www folder.

What about other apps? Honestly, I don’t have that much experience. You can get a Django app running without too many changes to the setup I’ve described, and I’m pretty confident you could use any node app too. I’ve had success putting together a frontend/backend situation by building the frontend into the static part of nginx while running the backend as the “app” above. You can use nginx to point API requests to the backend.

Can I get help? As I said earlier, “shoulders of giants”. I pieced this together from tutorials all over the internet, most of which I’ve linked. Get googling, and persevere!

Practical Software Engineering

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store