9 min read

Self-Hosted Developer Bliss

I like to host my own stuff. And it’s not a recent phenomenon - I’ve been running my own mail-server for a few years now (thanks, mailcow-dockerized!), and I run a personal wiki. Even this blog is hosted on a Vultr server. With the (not anymore) recent PRISM revelations, my gut feeling has been reinforced.

Since I work on personal projects, I need source hosting, error tracking and a build runner at the very least to have a “professional” developer experience, and in total control of the infrastructure. This is mostly an exercise to find out how hard something like this is to set up these days - don’t let this take too much time from your personal projects. It might even be considered… procrastination. 😉

First things first - you need dokku.

dokku is amazing. It’s basically a poor person’s Heroku. You dedicate a server to it, run a script on a fresh install of Debian, and from that point on you can create git remotes pointing to said server that behave just like Heroku git remotes - git push and a buildpack runs, automagically detects the code’s language, builds your code and serves it!

Setting dokku up is pretty simple, so I won’t go into it (the docs are pretty thorough). To follow the rest of this post, I assume you have dokku installed with the following plugins:

I also assume you have the dokku client properly set up, pointing to your dokku host.

Source hosting

I like Gogs. I love these single-binary Go tools that have been propping up for a while - so simple to deploy anywhere and run. Others of note are pgweb, hugo which powers this blog, minio and syncthing.

Gogs is so famous that dokku’s official blog features a post on how to deploy it. It’s pretty simple. Bear in mind though that Gogs’ project direction has been criticized recently, and a fork now exists by the name of Gitea. It’s still pretty similar to Gogs that the same setup probably works.

I’ve been using Gogs for over a year and it just works.

Error tracking

Easy things first. I’m a big fan of Sentry. It’s full of features, well designed and supports a ton of languages. Sentry is nice enough to have an open-source version that is really easy to self-host, making it pretty much the default choice for error tracking these days. On top of that, someone already created an even quicker solution for dokku users: dokku-sentry.

The process is really easy to follow, and by the end you’ll have Sentry up and running:

Sentry

That was easy! 😀

Continuous Integration (CI)

So here is where the easy part of this exercise ends.

Few other tools are as essential in my opinion for a developer as a proper CI setup, and yet I had a lot of difficulty getting to a solution I’m comfortable with.

If you want to skip the rant and just go to the setup notes, click here

I had a few options in mind for CI: Travis, GoCD and (yes) even Jenkins.

GoCD came to my attention at work, from a post on a dev Slack channel. It seemed pretty OK, even if there a a number of concepts to grasp upfront to be productive with the thing.

I installed it locally to figure out how easy a build would be to setup, and to my dismay I couldn’t even get the source from Gogs since GoCD doesn’t support SSH deploy keys! There are a few solutions but for me this felt so off that I just went to the next item on the list. 👎

Travis CI had a self-hosting section in the docs long time ago, but it’s gone. Even when it was present, it was a complete mess to setup because the project is made up of a bunch of separate projects. So Travis is a no-go. 👎

At this moment I started sweating a bit because it looked like I’d have to go with Jenkins. I know Jenkins from the time it was still Hudson and hadn’t been forked yet. And yes, Blue Ocean, their new “look” with extra features looks pretty cool but I use Jenkins at work and we have huge problems with it. Also, the pipeline definitions being written in Groovy kinda put me off - I don’t feel like learning a new language to create my builds. So 👎 for Jenkins as well.

I DuckDuckGo’d for more tools and found two more that felt promising: Laminar CI and Drone.

I liked Laminar’s “UNIX philosophy” approach of just running scripts, but being read-only and devoid of Docker functionality would really kill my flow. 👎

So I ended up going for Drone! I gave it a shot locally and it seemed to work out just fine. Doesn’t have a lot of features, but it’s snappy and gets the job drone with a Travis-like .yml file describing the build. Best of all, it’s Docker-native and integrates with Gogs - it starts to feel like destiny.

All that was left was to set it up on my dokku instance, which turned out to be time-consuming.

Setting up Drone.io

There are two parts to Drone: the server and the agent(s). The server sets up build-hooks on your Gogs repos, and parcels builds out to the connected agents.

Locally, you can set it up with docker-compose:

version: '2'

services:
  drone-server:
    image: drone/drone:0.8

    ports:
      - 80:8000
      - 9000
    volumes:
      - /var/lib/drone:/var/lib/drone/
    restart: always
    environment:
      - DRONE_OPEN=true
      - DRONE_HOST=${DRONE_HOST}
      - DRONE_GITHUB=true
      - DRONE_GITHUB_CLIENT=${DRONE_GITHUB_CLIENT}
      - DRONE_GITHUB_SECRET=${DRONE_GITHUB_SECRET}
      - DRONE_SECRET=${DRONE_SECRET}

  drone-agent:
    image: drone/agent:0.8

    command: agent
    restart: always
    depends_on:
      - drone-server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - DRONE_SERVER=drone-server:9000
      - DRONE_SECRET=${DRONE_SECRET}

(from the docs)

dokku, sadly, doesn’t support docker-compose files, which is a shame since most projects I find these days are distributed with this kind of file. So, we have to create two dokku apps. But first things first: since the two separate dokku apps will have to talk to each other, we’ll create a Docker network to link them. dokku doesn’t support creating them directly (hopefully, it will soon) so we have to SSH onto our server instance and, as root:

# docker network create drone_network

Server

Create a git repo in a new folder. Add this Dockerfile:

FROM drone/drone:0.8.5

EXPOSE 8000
EXPOSE 9000

ARG DRONE_SECRET

ENV DRONE_OPEN=true
ENV DRONE_HOST=https://drone.<your dokku hostname>
ENV DRONE_GOGS=true
ENV DRONE_GOGS_PRIVATE_MODE=true
ENV DRONE_GOGS_URL=<external URL of your Gogs instance>
ENV DRONE_SECRET=$DRONE_SECRET

ENTRYPOINT ["/bin/drone-server"]

This uses the official drone server image from Docker Hub. Let’s go over the settings.

  • DRONE_OPEN=true allow new users to register (we’ll lock this down later)
  • DRONE_HOST=<URL> I add a custom domain at this step, but you’re free to use the one dokku will assign to the app (usually, name-of-app.<dokku base url>)
  • DRONE_GOGS=true use Gogs integration
  • ENV DRONE_GOGS_PRIVATE_MODE=true I run Gogs in locked down mode, if you don’t feel free to omit this one
  • ENV DRONE_GOGS_URL=<external URL of your Gogs instance> self-explanatory
  • ENV DRONE_SECRET=$DRONE_SECRET since you might want to commit this Dockerfile to a source repo, it’s always good to keep sensitive info out of it. This will be injected into the build from dokku’s docker options.

With the Dockerfile all set, do a commit in your repo. Remember, dokku is git-based, so if the Dockerfile is not part of the repo, dokku won’t know what to build.

Then, we need to create the app itself. Generate a good enough secret with something like this website or Rails’ rake secret generator.

dokku apps:create drone-server
dokku storage:mount /var/lib/dokku/data/storage/drone-server/:/var/lib/drone
dokku docker-options:add build "--build-arg DRONE_SECRET=<SECRET HERE>"
dokku docker-options:add deploy "--network drone_network"
dokku docker-options:add run "--network drone_network"
dokku config:set --no-restart DOKKU_LETSENCRYPT_EMAIL=<YOUR EMAIL>
dokku proxy:ports-add http:80:8000
dokku domains:add <your custom domain> # (optional)
git push dokku
dokku letsencrypt

So, we create the dokku app (this creates it on the server and sets up the proper git remote locally), mount a volume so that Drone can persist user settings, and add the settings for the app to join the Docker network we created. We also setup SSL with Let’s Encrypt because hey, it’s free and security is always nice.

When you push, you should see dokku’s build messages with a message at the end saying it all went splendid. Head on down to the domain you used for the app, and you should see a login screen. Use your Gogs credentials (since Gogs doesn’t support OAuth) and you should see a list of repositories after you sync with Gogs:

Drone repo list

So that was pretty easy, too! 😆

I don’t share the server with anyone else, so now is a great time to flip the ENV DRONE_OPEN=true to ENV DRONE_OPEN=false on the Dockerfile, do a git commit and a git push dokku so that from now on no one else can login.

On to the agent!

Agent

Again create a new empty repo in another folder, and add the following Dockerfile:

FROM drone/agent:0.8.5

ARG DRONE_SECRET

ENV DRONE_SERVER=drone-server.web.1:9000
ENV DRONE_SECRET=$DRONE_SECRET

ENTRYPOINT ["/bin/drone-agent"]

Pretty simple! The agent has no user-facing features, so we only need the shared secret and the server’s location. Notice the name drone-server.web.1 - it’s how dokku names the container for the server’s app! You can verify that by doing docker ps on the server. Since the apps will share a Docker network, this will be enough for the agent to be able to reach the server.

The steps will feel similar:

dokku apps:create drone-agent
dokku storage:mount /var/run/docker.sock:/var/run/docker.sock
dokku docker-options:add build "--build-arg DRONE_SECRET=<SECRET HERE>"
dokku docker-options:add deploy "--network drone_network"
dokku docker-options:add run "--network drone_network"
git push dokku

Once this app is built, you should be able to verify it reaches the server by doing dokku logs - you should see something like this:

{"time":"2018-06-23T22:53:50Z","level":"debug","message":"request next execution"}

This means it’s all working! 🤘

Now all that’s left is to toggle some repos on Drone’s list so that the server can add a webhook to your Gogs repo. You can verify it’s there by going to the repo’s settings and checking the “Webhooks” section.

To trigger an actual build, you need to enable the “Repository Hooks” settings on Drone for a particular repo:

Drone repo hooks

So now, for every commit to this repo, Gogs will post to Drone’s webhook and a build will be triggered. The build starts by parsing a .drone.yml file in the root of your repo’s source - without this file the build will not run. Here’s an example:

pipeline:
  build:
    image: mhart/alpine-node:10.5.0
    commands: 
      - yarn install 
      - npm run test

Pretty easy, right? There’s lots more to Drone pipelines, the documentation is helpful.

The only drag about Drone is that the output page is very squished together, so you need a wide browser window to view it comfortably. But it works as you’d expect:

Drone build output

And that’s it! It took me a while to work out how to get it running, but I’m super happy with how it turned out.

I have the Dockerfiles for both server and agent on Github, feel free to send PRs if you think it can be improved. Until next time! 👋