Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
peterkeen committed May 18, 2012
0 parents commit 50a94bd
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 0 deletions.
110 changes: 110 additions & 0 deletions README.md
@@ -0,0 +1,110 @@
# Dokuen, a Personal App Platform

Dokuen is a "personal app platform". It's the same idea as all of these PaaS and IaaS services out there, except you host it on your own machine. It's specifically for hosting multiple 12-factor applications on one machine.

## Requirements

* [Gitolite](https://github.com/sitaramc/gitolite)
* [Nginx](http://wiki.nginx.org/Main)
* [daemontools](http://cr.yp.to/daemontools.html)

## Installation


### Step 1

```
gem install dokuen
```

### Step 2

Install daemontools and nginx using homebrew.

### Step 3

Create a `git` user and install gitolite according to package directions. You'll be making changes to the config later, but for now you should be able to create a new repository and push to it.

### Step 4

Run this:

```
$ sudo dokuen setup
```

This will ask you a few questions, set up a few directories, and install a few useful commands.

### Step 5

Edit your `.gitolite.rc` file and add this to the `COMMANDS` section:

```
'app' => 1
```

### Step 6

Edit your sudoers file and add this line:

```
git ALL=NOPASSWD: /usr/local/bin/dokuen
```

## Creating an App

```
$ ssh git@<your_host> app create <name>
Creating new application named <name>
Remote: git@<your_host>:<name>.git
$ git remote add dokuen git@<your_host>:<name>.git
```

### Add some environment variables

```
$ ssh git@<your_host> app config <name> add BUILDPACK_URL="https://github.com/heroku/heroku-buildpack-ruby.git" BASE_PORT=12345 FOREMAN="web=1"
```

### Deploy
```
$ git push deploy master
<deploy transcript>
```

### Check it out!
```
$ open http://<your_host>:12345/
```

## Available "app" Sub-commands

* create <name>
* config <name>
* add <key>=<value> ...
* delete <key> ...
* restart <name>
* scale <name> <type>=<num>
* deploy <name>

## DNS Concerns

I have my home router set up to forward ports 80 and 443 to my mac mini, and I have a dynamic DNS system set up with CNAMEs for each of my Dokuen-managed projects where the CNAME matches the name of the app. Unfortunately this setup is hard to automate way so Dokuen doesn't manage any of it for you.

What it does do is set up Nginx server configs for you that you can choose to use. If you want to use them, put this at the bottom of the `http` section of `nginx.conf`:

```
include /usr/local/var/dokuen/nginx/*.conf;
```

Then, force a redeploy for the apps you've already deployed:

```
$ ssh git@<your_host> app deploy <name>
```

## Rails

Unfortunately the stock Heroku buildpacks install a vendored node.js compiled for the Heroku platform, which happens to be linux. This doesn't work for Mac, which means you have to use a slightly patched version. This one works with a homebrew-installed node.js: https://github.com/peterkeen/heroku-buildpack-ruby

13 changes: 13 additions & 0 deletions bin/dokuen
@@ -0,0 +1,13 @@
#!/usr/bin/env ruby

require "rubygems"

begin
require 'dokuen'
rescue LoadError => e
path = File.expand_path '../../lib', __FILE__
$:.unshift(path) if File.directory?(path) && !$:.include?(path)
require 'dokuen'
end

Dokuen::Cli.start()
6 changes: 6 additions & 0 deletions bin/install_launchdaemon
@@ -0,0 +1,6 @@
#!/bin/bash

NAME=`basename $1`
cp $1 /Library/LaunchDaemons
launchctl unload -wF /Library/LaunchDaemons/$NAME
launchctl load -wF /Library/LaunchDaemons/$NAME
122 changes: 122 additions & 0 deletions bin/pre-receive
@@ -0,0 +1,122 @@
#!/usr/bin/env ruby

require 'tmpdir'
require 'fileutils'
require 'time'

MASON = "/usr/local/Cellar/ruby/1.9.3-p125/lib/ruby/gems/1.9.1/gems/mason-0.0.11/bin/mason"
BASE_BUILD_DIR = '/usr/local/var/dokuen/build'
BASE_RELEASE_DIR = '/usr/local/var/dokuen/release'
BASE_NGINX_CONF = '/usr/local/var/dokuen/nginx'
BASE_ENV_DIR = '/usr/local/var/dokuen/env'

should_deploy = `git config hooks.deploy`.chomp
subdomain = `git config hooks.deploy.subdomain`.chomp
port = `git config hooks.deploy.port`.chomp
foreman = `git config hooks.deploy.foreman`.chomp
buildpack_url = `git config hooks.deploy.buildpack`.chomp
after_deploy = `git config hooks.deploy.after`.chomp

exit(0) if should_deploy != "true"

def sys(cmd)
system(cmd) or exit(1)
end

File.umask(0022)

git_dir = Dir.getwd
cache_dir = "#{BASE_BUILD_DIR}/#{subdomain}"
now = Time.now().utc().strftime("%Y%m%dT%H%M%S")
release_dir = "#{BASE_RELEASE_DIR}/#{subdomain}/#{now}"
env_dir = "#{BASE_ENV_DIR}/#{subdomain}"

FileUtils.mkdir_p(cache_dir, :mode => 0777)
FileUtils.mkdir_p(release_dir, :mode => 0777)
FileUtils.mkdir_p(env_dir, :mode => 0777)

rev = ""
STDIN.each do |line|
puts line
parts = line.split(/\s/)
next if parts[2] != "refs/heads/master"
rev = parts[1]
end

ENV['GIT_DIR'] = nil
ENV['PATH'] = "/usr/local/bin:#{ENV['PATH']}"

clone_dir = Dir.mktmpdir


sys("git clone #{git_dir} #{clone_dir}")

Dir.chdir clone_dir

sys("git reset --hard #{rev}")

sys("#{MASON} build #{clone_dir} -b #{buildpack_url} -o #{release_dir} -c #{cache_dir}")
sys("chmod -R a+r #{release_dir}")
sys("find #{release_dir} -type d -exec chmod a+x {} \\;")

if after_deploy != ""
Dir.chdir release_dir
sys("envdir #{env_dir} foreman run #{after_deploy}")
end

base_port = port.to_i - 200

launch_agent_contents = <<HERE
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>info.bugsplat.#{subdomain}</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/envdir</string>
<string>#{BASE_ENV_DIR}/#{subdomain}</string>
<string>/usr/local/bin/foreman</string>
<string>start</string>
<string>-c</string>
<string>#{foreman}</string>
<string>-f</string>
<string>#{release_dir}/Procfile</string>
<string>-p</string>
<string>#{base_port}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>UserName</key>
<string>peter</string>
<key>WorkingDirectory</key>
<string>#{release_dir}</string>
</dict>
</plist>
HERE

puts "Installing LaunchDaemon"
File.open("#{release_dir}/info.bugsplat.#{subdomain}.plist", "w+") do |file|
file.write launch_agent_contents
end
sys("sudo /usr/local/bin/install_launchdaemon #{release_dir}/info.bugsplat.#{subdomain}.plist")

puts "Restarting Nginx"
File.open("#{BASE_NGINX_CONF}/info.bugsplat.#{subdomain}.conf", "w+") do |file|
file.write <<HERE
server {
server_name #{subdomain}.bugsplat.info;
listen 443;
ssl on;
location / {
proxy_pass http://localhost:#{port}/;
}
}
HERE
end

sys("sudo /usr/local/bin/restart_nginx")
3 changes: 3 additions & 0 deletions bin/restart_nginx
@@ -0,0 +1,3 @@
#!/bin/bash

/usr/local/sbin/nginx -s reload
45 changes: 45 additions & 0 deletions lib/dokuen.rb
@@ -0,0 +1,45 @@
require 'rubygems'
require 'thor'
require 'fileutils'

module Dokuen
class Cli < Thor
desc "setup", "Set up relevant things. Needs to be run as sudo."
def setup
raise "Must be run as root" unless Process.uid == 0
end

desc "create [APP]", "Create a new application."
def create(app="")
raise "app name required" if app.nil? || app == ""
puts "Creating new application named #{app}"
end

desc "restart [APP]", "Restart an existing app"
def restart(app="")
end

desc "scale [APP] [SCALE_SPEC]", "Scale an existing app up or down"
def scale(app="", scale_spec="")
end

desc "deploy [APP]", "Force a fresh deploy of an app"
def deploy(app="")
end

desc "restart_nginx", "Restart Nginx"
def restart_nginx
raise "Must be run as root" unless Process.uid == 0
end

desc "install_launchdaemon [APP] [RELEASE_PATH]", "Install a launch daemon"
def install_launchdaemon(app="", release_path="")
raise "Must be run as root" unless Process.uid == 0
end

desc "run_command [APP] [COMMAND]", "Run a command in the given app's environment"
def run_command(app="", command="")
end

end
end

0 comments on commit 50a94bd

Please sign in to comment.