Infrastructure

Hosting your own email with docker

2nd April, 2026 - Posted by david

Introduction

Ever since I learned how to properly host a website myself (i.e. rent my own VM, setup a web server and firewall, point DNS to server, serve web pages), I wanted to also host my own email. When I looked at this 10-15 years ago for menucosm, all the advice I found online could be summed up as: don’t bother, it’s too much hassle and not worth the €60/year it cost at the time to pay Google to host it, so that’s what I did.

Last year, I started working on a side-hustle, selling custom Lego sets I designed myself. Since this was a side-hustle and I was already paying for my own web server, I decided to try and keep things as lean as possible, which meant hosting the e-commerce site myself. No issues here: I found a PHP e-commerce package, installed it and wrote my own custom theme. Obviousy I would need email for this, for payments, orders, contacts etc.

I’d dockerized my setup on my web server over the years, so I now have a number of sites, all dockerized, sitting behind a reverse proxy webserver, powered by the excellent nginx-proxy. One of the great things about nginx-proxy is that it can handle SSL certificates for you automatically via Let’s Encrypt, for all your sites behind the proxy server, very convenient.

Being behind nginx-proxy is the only slightly unusual thing about my setup that might not apply to everybody trying to follow this guide. I use it for the SSL certs, but Docker Mailserver (see next paragraph) also can handle this for you, but I’ll let you figure out how yourself.

The great thing about docker and virtualization is that plenty of people have built complicated but reusable images. Since it was 2025, I knew there must be dockerized email setups and sure enough there are. The one I went for in the end was Docker Mailserver. I also needed a UI for webmail and configuration, so after I read about Rainloop, I found a docker image for that also. Now I just needed to set it all up.

For the sake of this blog post, let’s say my website is example.com and my mail server will be mail.example.com.

I’ve been using this setup for about 9 months so far, and as far as I can see, all has been running smoothly. From the very beginning, Google was accepting my test emails (i.e. from my new webmail to my existing gmail account), not marking anything as spam. I receive emails directly and through the website and these seem to be delivered successfully also. All good as far as I know.

One important thing before starting

Many hosting providers, including my own, have port 25 blocked for all outgoing connections. This is to prevent a server maliciously getting taken over and used for spam. I had to contact the support team, tell them what I was up to, promise I’d be dilligent and then ask them to open the port, which they did. I wasted about 1 – 1.5 weeks trying to unsuccessfully work with port 25 closed, which I was poorly advised to try by their support team, before finally realising it MUST be open and they were subsequently OK about opening it!

So make sure port 25 is open at your hosting provider before proceeding.

Docker Mailserver

The documentation for this package is fantastic. It’s ready to use out of the box with configuration all going into the provided mailserver.env file. We’ll be working with the env file and it’s compose.yaml. So first off, get those 2 files from the repo and recreate them in a local mailserver folder. Also create a docker-data folder in there, which the mail server will use for persistence.

SSL Certs

As mentioned above, the nginx-proxy package handles all my SSL via Let’s Encrypt. To get this to work, you need to add some environment variables to your compose.yaml, see the new environment block below. Docker Mailserver needs to be able to read the various SSL files that nginx-proxy creates, so we also need to map a new volume for the folder that nginx-proxy writes to, that Docker Mailserver will know to read; see the final line under the volume block of docker-compose below.

compose.yaml

As mentioned above, this is largely ready to go out of the box. Here is my slightly edited one (see comments for this blost starting with #DC), with the environment variables for nginx-proxy & Let’s Encrypt, and the volume where SSL certs are stored:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
version: '2'
services:
  mailserver:
    image: ghcr.io/docker-mailserver/docker-mailserver:latest
    container_name: mailserver
    hostname: mail.example.com
    env_file: mailserver.env
    ports: #DC Removed ports 110 and 143 here, for security
      - "25:25"
      - "465:465"
      - "587:587"
      - "993:993"
    volumes:
      - ./docker-data/dms/mail-data/:/var/mail/
      - ./docker-data/dms/mail-state/:/var/mail-state/
      - ./docker-data/dms/mail-logs/:/var/log/mail/
      - ./docker-data/dms/config/:/tmp/docker-mailserver/
      - /etc/localtime:/etc/localtime:ro
      #DC Added this line to map in SSL certs:
      - /var/lib/docker/volumes/nginx-proxy_certs/_data/:/etc/letsencrypt/live/:ro
    restart: always
    stop_grace_period: 1m
    cap_add:
      - NET_ADMIN
    healthcheck:
      test: "ss --listening --tcp | grep -P 'LISTEN.+:smtp' || exit 1"
      timeout: 3s
      retries: 0
    #DC Added this section for SSL
    environment:
      - SSL_TYPE=letsencrypt
      - VIRTUAL_HOST=mail.example.com
      - LETSENCRYPT_HOST=mail.example.com

mailserver.env

There are a huge number of options here which are well explained in the file and in the documentation. I’ll detail the ones I thought were important:

1
2
3
4
5
6
7
8
SPOOF_PROTECTION=1
ENABLE_POLICYD_SPF=0 # since I'll be using RSPAMD
ENABLE_OPENDKIM=0 # as above
ENABLE_CLAMAV=1
ENABLE_RSPAMD=1
ENABLE_RSPAMD_REDIS=1
ENABLE_FAIL2BAN=1
SSL_TYPE=letsencrypt

Bring up the server

At this point we’re ready to start the mailserver. This all just runs on the CLI, there’s no GUI until we install Rainloop. So, from within our mailserver folder, we simply do:

1
docker-compose up -d

Creating first accounts

Before setting up all the DNS records, the documentation recommends to create an email account, so let’s do that. We’ll also create a ‘postmaster’ alias for this; it’s standard practice to have a ‘postmaster@’ account, it’s up to you whether or not to have it separate or alias it. We’ll be using the postmaster account in the next section.

1
2
docker exec -it mailserver setup email add hello@example.com
docker exec -it mailserver setup alias add hello@example.com postmaster@example.com

DNS Records

In order to prove you’re sending valid email, i.e. you are who you say you are and you’re not spoofing a service, certain DNS records need to be setup. For this, you’ll need to access your control panel in your DNS provider (usually whoever you registered your domain with).

Once all the below is done, I suggest waiting a day before moving on to the Rainloop section, in order for your DNS changes to propagate around the world.

A Records

First up, let’s setup up some A records and the MX record. Using your server’s IP address, I would suggest setting up A records for:

  • mail.example.com
  • webmail.example.com
  • mta-sts.example.com

For the last of these domains, you also need to configure your primary webserver to also host it (i.e. to respond to requests for mta-sts.example.com), along with your www site. The site doesn’t need to look perfect at this domain, just respond to the initial request.

Next we want to create an MX record. You can leave the Host name blank, use the default TTL (mine’s 1 day), priority 10 and set Result to mail.example.com.

DKIM, DMARC and SPF

These next parts are key for sending email successfully. I’ll roughly outline what to do, but more info can be found at the Docker Mailserver guide for this.

DKIM

For DKIM, as per the guide, you can set this up via

1
docker exec -it mailserver setup config dkim

Assuming you’ve kept the same volume structure from compose.yaml above, this will output files into ./docker-data/dms/config/rspamd/dkim. Have a look at the contents of rsa-2048-mail-example.com.public.dns.txt and copy them to the clipboard. Go back to your Control Panel and create a new TXT record, using mail._domainkey as the Host name value, paste in your copied data to the Result field, use the same TTL as before and save.

DMARC

For DMARC, you just need to create another TXT record. Use _dmarc for Host name. For Result, the guide gives you some defaults, as well as linking to a generator where you can come up with your own. You’ll want to use the postmaster@example.com alias for this: this is how other mailservers will send you reports on issues they’ve encountered with your service. For reference, I went with the following: v=DMARC1;p=quarantine;pct=100;rua=mailto:postmaster@example.com;ruf=mailto:postmaster@example.com;adkim=s;aspf=r

SPF

This is one is simple enough. Simply create another TXT record with an empty Host name and the following for Result: v=spf1 ip4:your.ip.address -all

Other Records

Above, I said to create an A record for mta-sts.example.com. In addition to this, we want to create a TXT record with Host name set to _mta-sts and Result set to v=STSv1; id=yyyymmddxx, where yyyymmddxx is the value of the Serial Number field in your DNS control panel. Once this is done, you need to create a corresponding .well-known/mta-sts.txt file in your primary website’s root folder, i.e. so it’s accessible at https://www.example.com/.well-known/mta-sts.txt and https://mta-sts.example.com/.well-known/mta-sts.txt. Set the contents of this file to:

1
2
3
4
version: STSv1
mode: enforce
mx: mail.example.com
max_age: 86400

Finally, I also have the following TXT record, so I suggest you set it up also: Host name of _smtp._tls and Result of v=TLSRPTv1; rua=mailto:postmaster@example.com.

Reverse IP Address

Once we’re done with all the DNS records, we need to setup a reverse IP lookup. We do this in the admin/control panel of your hosting provider. Each hosting provider is different, but with mine it was pretty straight forward. I found the page I wanted under Network > Public Networks. In here, I added a new entry to the existing table, setting IP Address as my server’s IP and Reverse DNS Name as mail.example.com.

At this point, restart your mailserver docker machine (just to make sure all changes are in effect) and at that point, if all goes well, you’re actually hosting a proper mail server! Now we just want a way to interact with it…

Aside: Connect to nginx-proxy

My nginx-proxy server connects to an internal docker network, to which all sites behind the proxy must connect. Let’s say mine is called DCNET, so once you’ve restarted the image, connect to the network via

1
docker network connect DCNET mailserver

Rainloop

Again we’re going to be using a docker image and a github repo here, wernerfred/docker-rainloop. Above we created a mailserver folder; for Rainloop, I created a rainloop sub-folder in here, but you can create it at the same level as mailserver if you prefer. We’re going to create a second compose.yaml file; you could ultimately merge this with your compose.yaml from above, but that’s beyond the scope of this article. My rainloop instance will run on port 9009.

Here is what I have for my rainloop/compose.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: '3'
networks:
  DCNET:
    external: true # name of the network that nginx-proxy connects to
services:
  rainloop:
    image: wernerfred/docker-rainloop:latest
    container_name: docker-rainloop
    restart: always
    volumes:
      - rainloop_data:/rainloop/data
      - ./000-default.conf:/etc/apache2/sites-available/000-default.conf
      - ./ports.conf:/etc/apache2/ports.conf
      - ./apache2.conf:/etc/apache2/apache2.conf
    environment:
      VIRTUAL_HOST: "webmail.example.com"
      VIRTUAL_PORT: "9009"
      LETSENCRYPT_HOST: "webmail.example.com"
    networks:
      - DCNET
volumes:
  rainloop_data:

If you look under volumes, you’ll see 3 local files mapped in to the image. These configure Rainloop’s internal server (running Apache) to work off the correct port, set log formats and more. For simplicity, I’ll just paste the contents of each:

000-default.conf

1
2
3
4
5
6
7
<VirtualHost *:9009>
    ServerAdmin webmaster@localhost
    DocumentRoot /rainloop

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

ports.conf

1
2
3
4
5
6
7
8
Listen 80
Listen 9009
<IfModule ssl_module>
    Listen 443
</IfModule>
<IfModule mod_gnutls.c>
    Listen 443
</IfModule>

apache2.conf

This file is a bit long, so what I suggest is to simply copy the contents of /etc/apache2/sites-available/000-default.conf from inside the rainloop image, save it as 000-default.conf and append the following line:

1
ServerName "webmail.example.com"

Bring up the server and enjoy!

OK, hopefully everything will be configured correctly, so now all you need to do is bring up the server. So, within the rainloop directory you can do

1
docker-compose up -d

then head on over to webmail.example.com and you should see your nice webmail interface! You’ll want to configure it straight away by going to webmail.example.com/?admin, log in using admin / 12345 as the username and password, change your admin password and then set any other configuration you like.

Finally, going back to the root webmail login, you should be able to log in with the account you created above. At this point, you should be up and running, able to send and receive emails. Now, think of what you’re going to spend the €60/year you’re saving on!

Read more...

Phantom rewrite rule in Apache

7th March, 2015 - Posted by david

TL;DR The MultiViews option in Apache automatically will map e.g. /xfz/ to /xyz.php

I was recently creating a new section of the website I work for and decided to opt for tidy URLs, for SEO purposes, instead of our standard.long?url=format URLs that we have elsewhere. Let’s say the new section I was creating was called David’s Boxes, so I wanted to have relative URLs like /davids-boxes/big/blue map to davids-boxes.php?size=big&colour=blue. Purely co-incidentally, there happened to be a defunct davids-boxes folder in our www directory, which contained an old WordPress install, which I prompty deleted (more on this later). Then, I set up rewrite rules in our www/.htacess to do the example mapping above.

Everything was working fine locally: /davids-boxes/ matched to /davids-boxes.php and /davids-boxes/big/blue mapped to /davids-boxes.php?size=big&bolour=blue, all as expected. However, when I put the .htaccess file onto our test server, I couldn’t get the rules to match properly: everything mapped to the basic /davids-boxes.php, i.e. with no extra GET parameters. I tried different order of rules, moving the rules to the top of the .htaccess etc., but nothing worked. Then I simply deleted the rules from the .htaccess, expecting /davids-boxes/ not to map to anything, but it still strangely mapped to /davids.boxes.php as before. This led me to believe there was another rewrite rule somewhere else (a fact that was also helped by the previous WordPress install). Searching the entire codebase, which includes all ‘sub-‘.htaccess files, yielded no results, so then I began thinking it might be the server…

I had a look in our sites-available Apache configs, expecting there may be some sort of obvious generic rewrite to map any e.g. /xyz/ to xyz.php; no such luck. Going through each line in the config, I noticed we had the FollowSymLinks and MultiViews options enabled in the <Directory> tag. I was familiar with the former, but not the latter. Investigating into MultiViews, it turns out this was the thing doing the automatic mapping I was experiencing! The documentation states “if /some/dir has MultiViews enabled, and /some/dir/foo does not exist, then the server reads the directory looking for files named foo.*, and effectively fakes up a type map which names all those files”. Such relief to figure it out. I checked with our CTO, he didn’t know how it got there, so after removing it on testing and doing a quick test, we got rid of it everywhere and my problems were solved.

Read more...

Memcached variable breakdown

30th April, 2014 - Posted by david

At work we use memcache as our local variable cache and the excellent memcache.php from Harun Yayli to give us a simple way of viewing what’s in the cache.

One use case we came up with that was missing from the original memcached.php script was a way to group similar variables and see how much of the cache they’re taking up. For example, for searches done on the site, we generate a key by concatenating search- to an md5 of the SQL, then store the result of that query in the cache with that key. Another example might be to cache an ad, so the key could be ad-1234, for the ad with ID 1234. So, the following code changes are going to enable us to see how much space all the ‘search’ data, ‘ad’ data etc. takes up in comparison to each other.

It works by starting off with a list of known key prefixes (i.e. search- and ad- in the examples above), then uses existing memcache commands to get a list of slabs, then queries each slab for each item it contains. From this list of items, it looks for our known keys, calculates the size of the item and adds it to a running total. Once it has all the totals, it generates a nice pie chart with a legend, using Google’s Chart API.

So, first up we need to add a new menu entry to our menu, to link to our breakdown. This is simply done by editing the getMenu function in src/display.functions.php and adding a new menu entry to it, as follows:

1
2
// after the line for Slabs
echo menu_entry(16, 'Breakdown');

Next up, we need to add the big block of code that’s going to generate our pie chart. You’ll see in memcache.php a switch block around $_GET['op']. This is where we want to add our block for our new operation 16, as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<?php
switch ($_GET['op']) {
    // other code...

    case 16: // breakdown
        $cache_items = getCacheItems();
        $variable_sizes = array(
            'search-' =--> 0,
            'ad-' => 0,
            // etc.
            'other' => 0 // for everything that's left over
        );
        $variable_keys = array_keys($variable_sizes);
        $other = 0;

        foreach ($cache_items['items'] as $server => $slabs) {
            foreach ($slabs as $slab_id => $slab) {
                $items = dumpCacheSlab($server, $slab_id, $slab['number']);
                foreach ($items['ITEM'] as $key => $item) {
                    $expiry = trim($item, '[ ]');
                    $expiry = substr($expiry, strpos($expiry, ';')+2);
                    $expiry = substr($expiry, 0, strpos($expiry, ' '));
                    $r = sendMemcacheCommand($h, $p, 'get '.$key);
                    if (!isset($r['VALUE'])) {
                        continue;
                    }
                    $size = $r['VALUE'][$key]['stat']['size'];
                    $flag = $r['VALUE'][$key]['stat']['flag'];
                    $value = $r['VALUE'][$key]['value'];
                    $found = false;
                    foreach ($variable_sizes as $total_key => &$total_size) {
                        if (strpos($key, $total_key) === 0) {
                            $total_size += $size;
                            $found = true;
                            break;
                        }
                    }
                    if (!$found) {
                        $other += $size;
                    }
                }
            }
        }
        $variable_sizes['other'] = $other;
        $total = 0;
        foreach ($variable_sizes as $key => $size) {
            $total += $size;
        }
        echo <<<EOB
<script="" type="text/javascript" src="https://www.google.com/jsapi"><script type="text/javascript">// <![CDATA[
    google.load("visualization", "1", {packages:["corechart"]});
    google.setOnLoadCallback(drawChart);
    function drawChart() {
        var data = google.visualization.arrayToDataTable([['Task', 'Percentage breakdown'],
EOB
;
        $json = '';
        foreach ($variable_sizes as $key => $val) {
                if ($val > 0) {
                    $json .= "['".$key."', ".$val."],\n";
                }
        }
        echo rtrim($json, "\n,");
        echo <<<EOB
        ]);

            var options = {
                title: 'Percentage breakdown'
            };

            var chart = new google.visualization.PieChart(document.getElementById('piechart'));
            chart.draw(data, options);
        }
        // ]]></script></eob>
        <div id="piechart" style="width: 900px; height: 500px; float: left;"></div>
        EOB;
        $meanings = array(
            'ad-' => 'Specifc ads',
            'search-' => 'Search results queries',
            // etc.
            'other' => 'Other small random bits of data'
        );
?>
<div style="float: left;">
<h2>Key meanings</h2>
<table style="border: none;">
    <?php
    $i = 0;
    foreach ($meanings as $key => $meaning) {
    ?>
    <tr<?php if (++$i % 2 == 0) echo ' style="background: #ddd;"'; ?>>
        <td><?php echo $key; ?></td>
        <td><?php echo $meaning; ?></td>
    </tr>
    <?php
    }
    ?>
</table>
</div>
<?php
break;

So, now you should see a new menu option and clicking on it, should hopefully bring up a nice pie chart, something like the screenshot below (I’ve had to blur out our cache variable names).

Read more...

Beanstalkd, Pheanstalk and Daemontools on Ubuntu

20th March, 2013 - Posted by david

On the website I work for, when a user uploads an image for an ad, we generally keep 3 versions of that image, each a different size, simply referred to as ‘small’, ‘main’ or ‘large’. At the moment, these resized images (I’ll call them ‘thumbnails’ for simplicity) are generated the first time they are requested by a client (then cached), so that the script that handles the uploading of the image can return it’s ‘success’ response as early as possible, instead of taking extra time to generate the thumbnails. What Beanstalkd allows us to do is put a job on a queue (in our instance a ‘generate thumbnails’ job), where it’ll be picked up at some point in the future by another script that polls the queue and executes in it’s own separate process. So, my uploading script is only delayed by say the 0.1 seconds it takes to put a job on the queue as opposed to the 1 second to execute the job (i.e. generate the thumbnails). This blog post is how I got the whole thing to work on a Ubuntu 12.04 server, using PHP.

This post was largely inspired by an article on the blog Context With Style, which was written for a Mac. I’m also going to use their example of a queue filler script to populate the queue and a worker script, to pull jobs from the queue and process them. I recommend you read that post for a better idea.

One other thing, most of these UNIX commands need to be run as root, so I’ll assume you’re in super-user mode.

Beanstalkd

Installing Beanstalkd is pretty straightforward:

1
apt-get install beanstaldk

We don’t need to start it just yet, but for reference, to run it you can do

1
beanstalkd -l 127.0.0.1 -p 11300

Pheanstalk

Pheanstalk is a PHP package to interface with a Beanstalk daemon. I simply downloaded the zip from github, extracted it to a ‘pheanstalk’ folder in my main include folder, then to use it, I simply do

1
2
3
4
require_once 'pheanstalk/pheanstalk_init.php';
// note how we use 'Pheanstalk_Pheanstalk' instead of 'Pheanstalk',
// and how we omit the port in the constructor (as 11300 is the default)
$pheanstalk = new Pheanstalk_Pheanstalk('127.0.0.1');

Going by the example on the Context With Style article, for the script under the section “Pushing things into the queue”, we’ll call that script fill_queue.php. We’ll call the script in “Picking up things from the queue” worker.php. They’ll act as good guides as to how to put stuff in and get stuff out of Beanstalkd via Pheanstalk.

So, the idea is we’ll have our worker.php running non-stop (via daemontools, see next section), polling the queue for new jobs. Once we know our worker.php is ready, we can manually run fill_queue.php from the command line to populate the queue. The worker should then go through the queue, writing the data it reads to a log file in ./log/worker.txt. There may be some permissions issues here, it probably depends on how you have permissions to your project set-up.

Daemontools

First up we need to install daemontools, which is

1
apt-get install daemontools

You don’t actually interact with a daemontools process, you use things that begin with ‘sv’, such as svscan or svbootscan. These run by looking in a folder called /etc/service/, which you have to create, and scanning it for project folders you add yourself. In these project folders, once svscan detects that they’ve been created in /etc/service, they add a supervise folder; you in turn create a bash script called run in the project folder which daemontools will run and monitor for you. Don’t worry, all these steps are outlined below!

Anyways, now that we’ve installed daemontools, we need to create a run script for it and then run it, as well as create our /etc/service directory. Some of these tips are thanks to this post.

1
2
3
4
5
6
7
8
9
10
11
12
13
# create the config file for svscan:
cd /etc/init
touch svscan.conf
# add some commands into it:
echo "start on runlevel [2345]" > svscan.conf
echo "" >> svscan.conf
echo "expect fork" >> svscan.conf
echo "respawn" >> svscan.conf
echo "exec svscanboot" >> svscan.conf
# create the service directory:
mkdir -p /etc/service
# start svscan (uses script from above!):
service svscan start

Hopefully, now if you do a ps aux | grep sv, you’ll see at least svscan running.

Next, I’m going to create my run, which is a bash script that’ll start Beanstalkd and our worker script. I’ll place this in my example /var/www/my-project folder, along with my worker.php, fill_queue.php and log/worker.txt files. I’ll then create a my-project service folder and symlink my run file into there.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cd /var/www/my-project
touch run
# must be executable:
chmod 755 run
echo "#!/bin/sh" > run
# to start beanstalkd process:
echo "beanstalkd -l 127.0.0.1 -p 11300 &" >> run
# to start our worker process:
echo "php /var/www/worker.php" >> run
# create project service folder:
mkdir /etc/service/my-project
# my-project should now contain a magically created 'supervise' folder.
# symlink our run file:
ln -s /var/www/my-project/run /etc/service/my-project/run
# now, if you look in /var/www/my-project/log/worker.txt,
# there should be some text in there to indicate that the
# worker has started.
# run the fill queue script:
php fill_queue.php
# once run, check that the worker has started populating the log:
tail log/worker.txt

Hopefully when you do the tail, you’ll see data that corresponds with the output from fill_queue.php. This will indicate that your worker is running, polling the queue for new jobs. If you re-run fill_queue.php, your log file should expand accordingly.

Read more...

How to set up an SVN server over HTTPS on Apache

2nd August, 2012 - Posted by david

So, I recently started a new job as Lead Developer on carsireland.ie and one of the first things I was tasked with was moving the codebase from a simple PC running Linux to the cloud, so that it could be accessed remotely, outside the office. Now, while I do prefer Git, SVN is still reasonably popular, especially with websites older than a few years, hence the CTO wanted to stick with it, for the time being at least! Needless to say, most of the following is best done as root, or at least with sudo privileges. Also, this is done on Ubuntu, hence the use of apt-get.

1. Setting up Apache for HTTPS

Apache was already running on the server, but it had to be enabled for HTTPS. Firstly You need to generate self-signed SSL certificates. You’ll be asked for a passphrase; enter one and note it down:

1
2
3
openssl genrsa -des3 -out server.key 2048
openssl req -new -key server.key -out server.csr
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

Move the certificates to somewhere that Apache expects to find it:

1
2
cp server.crt /etc/ssl/certs
cp server.key /etc/ssl/private

Enable SSL for Apache

1
2
3
4
5
a2enmod ssl
a2ensite default-ssl
/etc/init.d/apache2 stop; sleep 2; /etc/init.d/apache2 start
# this last step is how I restart Apache.
# I don't trust the 'restart' option. There's probably other/better ways of doing this

2. SVN

Install SVN and it’s Apache module

1
apt-get install subversion libapache2-svn

Create a new folder for the code (we’ll call the folder ‘svn’):

1
mkdir /home/svn

Create the repository:

1
svnadmin create /home/svn

Tell Apache about the repository:

1
nano /etc/apache2/sites-available/default-ssl

This opens up the pretty simple nano editor. At the bottom of the file, before the final <VirtualHost>, add:

1
2
3
4
5
6
7
8
<location svn="">
    DAV svn
    SVNPath /home/svn
    AuthType Basic
    AuthName "Your repository name"
    AuthUserFile /etc/subversion/passwd
    Require valid-user
</location>

You may need to enable your SSL site, so if the files /etc/apache2/sites-enabled/000-default-ssl or /etc/apache2/sites-enabled/default-ssl don’t exist, do:

1
ln -s /etc/apache2/sites-available/default-ssl /etc/apache2/sites-enabled/000-default-ssl

For Apache to be able to read/write to the repository, we need to change it’s owner to www-data:

1
chown -R www-data:www-data /home/svn

Next, we need to add some login details for users, i.e. developers (you’ll be asked to enter a password):

1
2
3
htpasswd -c /etc/subversion/passwd user_name
# user_name should correspond with the username of some one you want to have access to the repository.
# The password entered can be different from their normal login password and is used to access the repository at all times.

For subsequent users, drop the -c flag above.

Restart Apache (however you want to do it). Following from above:

1
/etc/init.d/apache2; sleep 2; /etc/init.d/apache2 start

You should now be able to view the initial empty repository at http://server.locaton/svn where ‘server.location’ is either an IP address or a domain, depending on how you’ve set-up the server.

If you have an SVN dump of your repository and you want to load it into the new one, you can simply do:

1
svnadmin load --force-uid /home/svn > dumpfile

At this point, your SVN server should be up and running and ready to take commits. You may need to play around with the permissions of your /home/svn directories, making certain ones executable/writeable to Apache. If I’ve left anything else out, please let me know in the comments.

Read more...