diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,18 @@
+propellor (0.9.0) unstable; urgency=medium
+
+  * Avoid encoding the current stable suite in propellor's code,
+    since that poses a difficult transition around the release,
+    and can easily be wrong if an older version of propellor is used.
+    Instead, the os property for a stable system includes the suite name
+    to use, eg Stable "wheezy".
+  * stdSourcesList uses the stable suite name, to avoid unwanted
+    immediate upgrades to the next stable release.
+  * debCdn switched from cdn.debian.net to http.debian.net, which seems to be
+    better managed now.
+  * Docker: Avoid committing container every time it's started up.
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 10 Oct 2014 11:37:45 -0400
+
 propellor (0.8.3) unstable; urgency=medium
 
   * The Debian package now includes a single-revision git repository in
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -44,9 +44,8 @@
 3. If you don't have a gpg private key already, generate one: `gpg --gen-key`
 4. Run: `propellor --add-key $KEYID`, which will make propellor trust
    your gpg key, and will sign your `~/.propellor` repository using it.
-5. `cd ~/.propellor/`; use git to push the repository to a central
-   server (github, or your own git server). Configure that central
-   server as the origin remote of the repository.
+5. Push the git repository to a central server (github or your own):
+   `cd ~/.propellor/; git remote add origin ssh://git.example.com/propellor.git; git push -u origin master`
 6. Edit `~/.propellor/config.hs`, and add a host you want to manage.
    You can start by not adding any properties, or only a few.
 7. Pick a host and run: `propellor --spin $HOST`
diff --git a/config-joey.hs b/config-joey.hs
--- a/config-joey.hs
+++ b/config-joey.hs
@@ -1,4 +1,5 @@
 -- This is the live config file used by propellor's author.
+-- https://propellor.branchable.com/
 module Main where
 
 import Propellor
@@ -7,6 +8,7 @@
 import qualified Propellor.Property.File as File
 import qualified Propellor.Property.Apt as Apt
 import qualified Propellor.Property.Network as Network
+import qualified Propellor.Property.Service as Service
 import qualified Propellor.Property.Ssh as Ssh
 import qualified Propellor.Property.Gpg as Gpg
 import qualified Propellor.Property.Cron as Cron
@@ -35,209 +37,240 @@
   {- Propellor            -- \          / | /    )          _.-"-._
      Deployed -}          --  `/-==__ _/__|/__=-|          (       \_
 hosts :: [Host]          --   *             \ | |           '--------'
-hosts =                 --                  (o)  `
-	[ host "darkstar.kitenet.net"
-		& ipv6 "2001:4830:1600:187::2" -- sixxs tunnel
+hosts =                --                  (o)  `
+	[ darkstar
+	, clam
+	, orca
+	, kite
+	, diatom
+	, elephant
+	] ++ containers ++ monsters
 
-		& Apt.buildDep ["git-annex"] `period` Daily
-		& Docker.configured
-		! Docker.docked hosts "android-git-annex"
+darkstar :: Host
+darkstar = host "darkstar.kitenet.net"
+	& ipv6 "2001:4830:1600:187::2" -- sixxs tunnel
 
-	, standardSystem "clam.kitenet.net" Unstable "amd64"
-		[ "Unreliable server. Anything here may be lost at any time!" ]
-		& ipv4 "162.248.9.29"
+	& Apt.buildDep ["git-annex"] `period` Daily
+	& Docker.configured
+	! Docker.docked hosts "android-git-annex"
+	! Docker.docked hosts "webserver"
 
-		& CloudAtCost.decruft
-		& Apt.unattendedUpgrades
-		& Network.ipv6to4
-		& Tor.isBridge
-		& Postfix.satellite
+clam :: Host
+clam = standardSystem "clam.kitenet.net" Unstable "amd64"
+	[ "Unreliable server. Anything here may be lost at any time!" ]
+	& ipv4 "162.248.9.29"
 
-		& Docker.configured
-		& Docker.garbageCollected `period` Daily
-		
-		-- ssh on some extra ports to deal with horrible networks
-		-- while travelling
-		& alias "travelling.kitenet.net"
-		& Ssh.listenPort 80
-		& Ssh.listenPort 443
+	& CloudAtCost.decruft
+	& Apt.unattendedUpgrades
+	& Network.ipv6to4
+	& Tor.isBridge
+	& Postfix.satellite
+
+	& Docker.configured
+	& Docker.garbageCollected `period` Daily
+	& Docker.docked hosts "webserver"
+	& File.dirExists "/var/www/html"
+	& File.notPresent "/var/www/html/index.html"
+	& "/var/www/index.html" `File.hasContent` ["hello, world"]
+	& alias "helloworld.kitenet.net"
 	
-	-- Orca is the main git-annex build box.
-	, standardSystem "orca.kitenet.net" Unstable "amd64"
-		[ "Main git-annex build box." ]
-		& ipv4 "138.38.108.179"
+	-- ssh on some extra ports to deal with horrible networks
+	-- while travelling
+	& alias "travelling.kitenet.net"
+	! Ssh.listenPort 80
+	! Ssh.listenPort 443
+	
+orca :: Host
+orca = standardSystem "orca.kitenet.net" Unstable "amd64"
+	[ "Main git-annex build box." ]
+	& ipv4 "138.38.108.179"
 
-		& Apt.unattendedUpgrades
-		& Postfix.satellite
-		& Docker.configured
-		& Docker.docked hosts "amd64-git-annex-builder"
-		& Docker.docked hosts "i386-git-annex-builder"
-		& Docker.docked hosts "android-git-annex-builder"
-		& Docker.docked hosts "armel-git-annex-builder-companion"
-		& Docker.docked hosts "armel-git-annex-builder"
-		& Docker.garbageCollected `period` Daily
-		& Apt.buildDep ["git-annex"] `period` Daily
+	& Apt.unattendedUpgrades
+	& Postfix.satellite
+	& Docker.configured
+	& Docker.docked hosts "amd64-git-annex-builder"
+	& Docker.docked hosts "i386-git-annex-builder"
+	& Docker.docked hosts "android-git-annex-builder"
+	& Docker.docked hosts "armel-git-annex-builder-companion"
+	& Docker.docked hosts "armel-git-annex-builder"
+	& Docker.garbageCollected `period` Daily
+	& Apt.buildDep ["git-annex"] `period` Daily
 	
-	-- This is not a complete description of kite, since it's a
-	-- multiuser system with eg, user passwords that are not deployed
-	-- with propellor.
-  	, standardSystemUnhardened "kite.kitenet.net" Unstable "amd64"
-		[ "Welcome to the new kitenet.net server!"
-		, "This is still under construction and not yet live.."
-		]
-	  	& ipv4 "66.228.36.95"
-		& ipv6 "2600:3c03::f03c:91ff:fe73:b0d2"
-		-- & alias "kitenet.net" -- not yet live!
+-- This is not a complete description of kite, since it's a
+-- multiuser system with eg, user passwords that are not deployed
+-- with propellor.
+kite :: Host
+kite = standardSystemUnhardened "kite.kitenet.net" Unstable "amd64"
+	[ "Welcome to the new kitenet.net server!"
+	]
+	& ipv4 "66.228.36.95"
+	& ipv6 "2600:3c03::f03c:91ff:fe73:b0d2"
+	& alias "kitenet.net"
+	& alias "wren.kitenet.net" -- temporary
 
-		& Apt.installed ["linux-image-amd64"]
-		& Linode.chainPVGrub 5
-		& Apt.unattendedUpgrades
-		& Apt.installed ["systemd"]
-		& Ssh.hostKeys (Context "kitenet.net")
-		-- Since ssh password authentication is allowed:
-		& Apt.serviceInstalledRunning "fail2ban"
-		& Obnam.backup "/" "33 1 * * *"
-			[ "--repository=sftp://joey@eubackup.kitenet.net/~/lib/backup/kite.obnam"
-			, "--client-name=kitenet.net"
-			, "--encrypt-with="
-			, "--exclude=/var/cache"
-			, "--exclude=/var/tmp"
-			, "--exclude=/home/joey/lib"
-			, "--exclude=.*/tmp/"
-			, "--one-file-system"
-			] Obnam.OnlyClient
-			`requires` Gpg.keyImported "98147487" "root"
-			`requires` Ssh.keyImported SshRsa "root"
-				(Context "kite.kitenet.net")
-			`requires` Ssh.knownHost hosts "eubackup.kitenet.net" "root"
+	& Apt.installed ["linux-image-amd64"]
+	& Linode.chainPVGrub 5
+	& Apt.unattendedUpgrades
+	& Apt.installed ["systemd"]
+	& Ssh.hostKeys (Context "kitenet.net")
+	& Ssh.passwordAuthentication True
+	-- Since ssh password authentication is allowed:
+	& Apt.serviceInstalledRunning "fail2ban"
+	& Obnam.backup "/" "33 1 * * *"
+		[ "--repository=sftp://joey@eubackup.kitenet.net/~/lib/backup/kite.obnam"
+		, "--client-name=kitenet.net"
+		, "--encrypt-with=98147487"
+		, "--exclude=/var/cache"
+		, "--exclude=/var/tmp"
+		, "--exclude=/home/joey/lib"
+		, "--exclude=.*/tmp/"
+		, "--one-file-system"
+		] Obnam.OnlyClient
+		`requires` Gpg.keyImported "98147487" "root"
+		`requires` Ssh.keyImported SshRsa "root"
+			(Context "kite.kitenet.net")
+		`requires` Ssh.knownHost hosts "eubackup.kitenet.net" "root"
+	& Apt.serviceInstalledRunning "ntp"
+	& "/etc/timezone" `File.hasContent` ["US/Eastern"]
 
-		-- & alias "smtp.kitenet.net" -- not yet live!
-		-- & alias "imap.kitenet.net" -- not yet live!
-		-- & alias "mail.kitenet.net" -- not yet live!
-		& JoeySites.kiteMailServer
+	& alias "smtp.kitenet.net"
+	& alias "imap.kitenet.net"
+	& alias "pop.kitenet.net"
+	& alias "mail.kitenet.net"
+	& JoeySites.kiteMailServer
 
-		& JoeySites.legacyWebSites
+	& JoeySites.legacyWebSites
 
-		& Apt.installed
-			["git-annex", "myrepos"
-			, "build-essential", "make"
-			-- Some users have zsh as their login shell.
-			, "zsh"
-			]
+	& alias "bitlbee.kitenet.net"
+	& Apt.serviceInstalledRunning "bitlbee"
+	& "/etc/bitlbee/bitlbee.conf" `File.hasContent`
+		[ "[settings]"
+		, "User = bitlbee"
+		, "AuthMode = Registered"
+		, "[defaults]"
+		] 
+		`onChange` Service.restarted "bitlbee"
+	& "/etc/default/bitlbee" `File.containsLine` "BITLBEE_PORT=\"6767\""
+		`onChange` Service.restarted "bitlbee"
 
-  	, standardSystem "diatom.kitenet.net" Stable "amd64"
-	  	[ "Important stuff that needs not too much memory or CPU." ]
-		& ipv4 "107.170.31.195"
+	& Apt.installed
+		["git-annex", "myrepos"
+		, "build-essential", "make"
+		, "rss2email", "archivemail"
+		, "devscripts"
+		-- Some users have zsh as their login shell.
+		, "zsh"
+		]
 
-		& DigitalOcean.distroKernel
-		& Ssh.hostKeys (Context "diatom.kitenet.net")
-		& Apt.unattendedUpgrades
-		& Apt.serviceInstalledRunning "ntp"
-		& Postfix.satellite
+diatom :: Host
+diatom = standardSystem "diatom.kitenet.net" (Stable "wheezy") "amd64"
+	[ "Important stuff that needs not too much memory or CPU." ]
+	& ipv4 "107.170.31.195"
 
-		-- Diatom has 500 mb of memory, so tune for that.
-		& JoeySites.obnamLowMem
-		& Apt.serviceInstalledRunning "swapspace"
-	
-		& Apt.serviceInstalledRunning "apache2"
-		& JoeySites.kitenetHttps
-		& Apache.multiSSL
-		& File.ownerGroup "/srv/web" "joey" "joey"
-		& Apt.installed ["analog"]
+	& DigitalOcean.distroKernel
+	& Ssh.hostKeys (Context "diatom.kitenet.net")
+	& Apt.unattendedUpgrades
+	& Apt.serviceInstalledRunning "ntp"
+	& Postfix.satellite
 
-		& alias "git.kitenet.net"
-		& alias "git.joeyh.name"
-		& JoeySites.gitServer hosts
+	-- Diatom has 500 mb of memory, so tune for that.
+	& JoeySites.obnamLowMem
+	& Apt.serviceInstalledRunning "swapspace"
 	
-		& alias "downloads.kitenet.net"
-		& JoeySites.annexWebSite "/srv/git/downloads.git"
-			"downloads.kitenet.net"
-			"840760dc-08f0-11e2-8c61-576b7e66acfd"
-			[("usbackup", "ssh://usbackup.kitenet.net/~/lib/downloads/")]
-			`requires` Ssh.keyImported SshRsa "joey" (Context "downloads.kitenet.net")
-			`requires` Ssh.knownHost hosts "usbackup.kitenet.net" "joey"
-		& JoeySites.gitAnnexDistributor
+	& Apt.serviceInstalledRunning "apache2"
+	& JoeySites.kitenetHttps
+	& Apache.multiSSL
+	& File.ownerGroup "/srv/web" "joey" "joey"
+	& Apt.installed ["analog"]
 
+	& alias "git.kitenet.net"
+	& alias "git.joeyh.name"
+	& JoeySites.gitServer hosts
+	
+	& alias "downloads.kitenet.net"
+	& JoeySites.annexWebSite "/srv/git/downloads.git"
+		"downloads.kitenet.net"
+		"840760dc-08f0-11e2-8c61-576b7e66acfd"
+		[("usbackup", "ssh://usbackup.kitenet.net/~/lib/downloads/")]
+		`requires` Ssh.keyImported SshRsa "joey" (Context "downloads.kitenet.net")
+		`requires` Ssh.knownHost hosts "usbackup.kitenet.net" "joey"
+	& JoeySites.gitAnnexDistributor
 		& alias "tmp.kitenet.net"
-		& JoeySites.annexWebSite "/srv/git/joey/tmp.git"
-			"tmp.kitenet.net"
-			"26fd6e38-1226-11e2-a75f-ff007033bdba"
-			[]
-		& JoeySites.twitRss
-		& JoeySites.pumpRss
-		
-		& alias "nntp.olduse.net"
-		& alias "resources.olduse.net"
-		& JoeySites.oldUseNetServer hosts
+	& JoeySites.annexWebSite "/srv/git/joey/tmp.git"
+		"tmp.kitenet.net"
+		"26fd6e38-1226-11e2-a75f-ff007033bdba"
+		[]
+	& JoeySites.twitRss
+	& JoeySites.pumpRss
 		
-		& alias "ns2.kitenet.net"
-		& myDnsPrimary "kitenet.net" []
-		& myDnsPrimary "joeyh.name" []
-		& myDnsPrimary "ikiwiki.info" []
-		& myDnsPrimary "olduse.net"
-			[ (RelDomain "article",
-				CNAME $ AbsDomain "virgil.koldfront.dk") ]
+	& alias "nntp.olduse.net"
+	& alias "resources.olduse.net"
+	& JoeySites.oldUseNetServer hosts
 	
-		& alias "ns3.branchable.com"
-		& branchableSecondary
-		
-		& Dns.secondaryFor ["animx"] hosts "animx.eu.org"
+	& alias "ns2.kitenet.net"
+	& myDnsPrimary "kitenet.net" []
+	& myDnsPrimary "joeyh.name" []
+	& myDnsPrimary "ikiwiki.info" []
+	& myDnsPrimary "olduse.net"
+		[ (RelDomain "article",
+			CNAME $ AbsDomain "virgil.koldfront.dk") ]
 
-	, let ctx = Context "elephant.kitenet.net"
-	  in standardSystem "elephant.kitenet.net" Unstable "amd64"
-	  	[ "Storage, big data, and backups, omnomnom!"
-		, "(Encrypt all data stored here.)"
-		]
-		& ipv4 "193.234.225.114"
+	& alias "ns3.branchable.com"
+	& branchableSecondary
+	
+	& Dns.secondaryFor ["animx"] hosts "animx.eu.org"
 
+elephant :: Host
+elephant = standardSystem "elephant.kitenet.net" Unstable "amd64"
+	[ "Storage, big data, and backups, omnomnom!"
+	, "(Encrypt all data stored here.)"
+	]
+	& ipv4 "193.234.225.114"
 		& Grub.chainPVGrub "hd0,0" "xen/xvda1" 30
-		& Postfix.satellite
-		& Apt.unattendedUpgrades
-		& Ssh.hostKeys ctx
-		& sshPubKey "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAJkoPRhUGT8EId6m37uBdYEtq42VNwslKnc9mmO+89ody066q6seHKeFY6ImfwjcyIjM30RTzEwftuVNQnbEB0="
-		& Ssh.keyImported SshRsa "joey" ctx
-		& Apt.serviceInstalledRunning "swapspace"
-
+	& Postfix.satellite
+	& Apt.unattendedUpgrades
+	& Ssh.hostKeys ctx
+	& sshPubKey "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAJkoPRhUGT8EId6m37uBdYEtq42VNwslKnc9mmO+89ody066q6seHKeFY6ImfwjcyIjM30RTzEwftuVNQnbEB0="
+	& Ssh.keyImported SshRsa "joey" ctx
+	& Apt.serviceInstalledRunning "swapspace"
 		& alias "eubackup.kitenet.net"
-		& Apt.installed ["obnam", "sshfs", "rsync"]
-		& JoeySites.obnamRepos ["wren", "pell", "kite"]
-		& JoeySites.githubBackup
-		& JoeySites.rsyncNetBackup hosts
-		& JoeySites.backupsBackedupTo hosts "usbackup.kitenet.net" "lib/backup/eubackup"
-
+	& Apt.installed ["obnam", "sshfs", "rsync"]
+	& JoeySites.obnamRepos ["wren", "pell", "kite"]
+	& JoeySites.githubBackup
+	& JoeySites.rsyncNetBackup hosts
+	& JoeySites.backupsBackedupTo hosts "usbackup.kitenet.net" "lib/backup/eubackup"
 		& alias "podcatcher.kitenet.net"
-		& JoeySites.podcatcher
-		
-		& alias "znc.kitenet.net"
-		& JoeySites.ircBouncer
-
-		-- I'd rather this were on diatom, but it needs unstable.
-		& alias "kgb.kitenet.net"
-		& JoeySites.kgbServer
-
+	& JoeySites.podcatcher
+	
+	& alias "znc.kitenet.net"
+	& JoeySites.ircBouncer
+	-- I'd rather this were on diatom, but it needs unstable.
+	& alias "kgb.kitenet.net"
+	& JoeySites.kgbServer
 		& alias "mumble.kitenet.net"
-		& JoeySites.mumbleServer hosts
-		
-		& alias "ns3.kitenet.net"
-		& myDnsSecondary
-		
-		& Docker.configured
-
+	& JoeySites.mumbleServer hosts
+	
+	& alias "ns3.kitenet.net"
+	& myDnsSecondary
+	
+	& Docker.configured
 		& Docker.docked hosts "oldusenet-shellbox"
-		& Docker.docked hosts "openid-provider"
-		 	`requires` Apt.serviceInstalledRunning "ntp"
-		& Docker.docked hosts "ancient-kitenet"
+	& Docker.docked hosts "openid-provider"
+		`requires` Apt.serviceInstalledRunning "ntp"
+	& Docker.docked hosts "ancient-kitenet"
 
-		& Docker.garbageCollected `period` (Weekly (Just 1))
-		
-		-- For https port 443, shellinabox with ssh login to
-		-- kitenet.net
-		& alias "shell.kitenet.net"
-		& JoeySites.kiteShellBox
-		-- Nothing is using http port 80, so listen on
-		-- that port for ssh, for traveling on bad networks that
-		-- block 22.
-		& Ssh.listenPort 80
+	& Docker.garbageCollected `period` (Weekly (Just 1))
+	
+	-- For https port 443, shellinabox with ssh login to
+	-- kitenet.net
+	& alias "shell.kitenet.net"
+	& JoeySites.kiteShellBox
+	-- Nothing is using http port 80, so listen on
+	-- that port for ssh, for traveling on bad networks that
+	-- block 22.
+	& Ssh.listenPort 80
+  where
+	ctx = Context "elephant.kitenet.net"
 
 
 	    --'                        __|II|      ,.
@@ -247,30 +280,31 @@
 	----------------------- :                    / -----------------------
 	------------------------ \____, o          ,' ------------------------
 	------------------------- '--,___________,'  -------------------------
-
+containers :: [Host]
+containers =
 	-- Simple web server, publishing the outside host's /var/www
-	, standardContainer "webserver" Stable "amd64"
-		& Docker.publish "8080:80"
+	[ standardStableContainer "webserver"
+		& Docker.publish "80:80"
 		& Docker.volume "/var/www:/var/www"
 		& Apt.serviceInstalledRunning "apache2"
 
 	-- My own openid provider. Uses php, so containerized for security
 	-- and administrative sanity.
-	, standardContainer "openid-provider" Stable "amd64"
+	, standardStableContainer "openid-provider"
 		& alias "openid.kitenet.net"
 		& Docker.publish "8081:80"
 		& OpenId.providerFor ["joey", "liw"]
 			"openid.kitenet.net:8081"
 
 	-- Exhibit: kite's 90's website.
-	, standardContainer "ancient-kitenet" Stable "amd64"
+	, standardStableContainer "ancient-kitenet"
 		& alias "ancient.kitenet.net"
 		& Docker.publish "1994:80"
 		& Apt.serviceInstalledRunning "apache2"
 		& Git.cloned "root" "git://kitenet-net.branchable.com/" "/var/www"
 			(Just "remotes/origin/old-kitenet.net")
 	
-	, standardContainer "oldusenet-shellbox" Stable "amd64"
+	, standardStableContainer "oldusenet-shellbox"
 		& alias "shell.olduse.net"
 		& Docker.publish "4200:4200"
 		& JoeySites.oldUseNetShellBox
@@ -287,7 +321,7 @@
 	, let gitannexdir = GitAnnexBuilder.homedir </> "git-annex"
 	  in GitAnnexBuilder.androidContainer dockerImage "android-git-annex" doNothing gitannexdir
 		& Docker.volume ("/home/joey/src/git-annex:" ++ gitannexdir)
-	] ++ monsters
+	]
 
 type Motd = [String]
 
@@ -321,14 +355,17 @@
 	& Apt.removed ["exim4", "exim4-daemon-light", "exim4-config", "exim4-base"]
 		`onChange` Apt.autoRemove
 
+standardStableContainer :: Docker.ContainerName -> Host
+standardStableContainer name = standardContainer name (Stable "wheezy") "amd64"
+
 -- This is my standard container setup, featuring automatic upgrades.
 standardContainer :: Docker.ContainerName -> DebianSuite -> Architecture -> Host
 standardContainer name suite arch = Docker.container name (dockerImage system)
 	& os system
 	& Apt.stdSourcesList `onChange` Apt.upgrade
-	& Apt.installed ["systemd"]
 	& Apt.unattendedUpgrades
 	& Apt.cacheCleaned
+	& Docker.tweaked
   where
 	system = System (Debian suite) arch
 
@@ -336,7 +373,7 @@
 dockerImage :: System -> Docker.Image
 dockerImage (System (Debian Unstable) arch) = "joeyh/debian-unstable-" ++ arch
 dockerImage (System (Debian Testing) arch) = "joeyh/debian-unstable-" ++ arch
-dockerImage (System (Debian Stable) arch) = "joeyh/debian-stable-" ++ arch
+dockerImage (System (Debian (Stable _)) arch) = "joeyh/debian-stable-" ++ arch
 dockerImage _ = "debian-stable-official" -- does not currently exist!
 
 myDnsSecondary :: Property
@@ -376,7 +413,7 @@
 
 
 monsters :: [Host]    -- Systems I don't manage with propellor,
-monsters =	      -- but do want to track their public keys etc.
+monsters =            -- but do want to track their public keys etc.
 	[ host "usw-s002.rsync.net"
 		& sshPubKey "ssh-dss AAAAB3NzaC1kc3MAAAEBAI6ZsoW8a+Zl6NqUf9a4xXSMcV1akJHDEKKBzlI2YZo9gb9YoCf5p9oby8THUSgfh4kse7LJeY7Nb64NR6Y/X7I2/QzbE1HGGl5mMwB6LeUcJ74T3TQAlNEZkGt/MOIVLolJHk049hC09zLpkUDtX8K0t1yaCirC9SxDGLTCLEhvU9+vVdVrdQlKZ9wpLUNbdAzvbra+O/IVvExxDZ9WCHrnfNA8ddVZIGEWMqsoNgiuCxiXpi8qL+noghsSQNFTXwo7W2Vp9zj1JkCt3GtSz5IzEpARQaXEAWNEM0n1nJ686YUOhou64iRM8bPC1lp3QXvvZNgj3m+QHhIempx+de8AAAAVAKB5vUDaZOg14gRn7Bp81ja/ik+RAAABACPH/bPbW912x1NxNiikzGR6clLh+bLpIp8Qie3J7DwOr8oC1QOKjNDK+UgQ7mDQEgr4nGjNKSvpDi4c1QCw4sbLqQgx1y2VhT0SmUPHf5NQFldRQyR/jcevSSwOBxszz3aq9AwHiv9OWaO3XY18suXPouiuPTpIcZwc2BLDNHFnDURQeGEtmgqj6gZLIkTY0iw7q9Tj5FOyl4AkvEJC5B4CSzaWgey93Wqn1Imt7KI8+H9lApMKziVL1q+K7xAuNkGmx5YOSNlE6rKAPtsIPHZGxR7dch0GURv2jhh0NQYvBRn3ukCjuIO5gx56HLgilq59/o50zZ4NcT7iASF76TcAAAEAC6YxX7rrs8pp13W4YGiJHwFvIO1yXLGOdqu66JM0plO4J1ItV1AQcazOXLiliny3p2/W+wXZZKd5HIRt52YafCA8YNyMk/sF7JcTR4d4z9CfKaAxh0UpzKiAk+0j/Wu3iPoTOsyt7N0j1+dIyrFodY2sKKuBMT4TQ0yqQpbC+IDQv2i1IlZAPneYGfd5MIGygs2QMfaMQ1jWAKJvEO0vstZ7GB6nDAcg4in3ZiBHtomx3PL5w+zg48S4Ed69BiFXLZ1f6MnjpUOP75pD4MP6toS0rgK9b93xCrEQLgm4oD/7TCHHBo2xR7wwcsN2OddtwWsEM2QgOkt/jdCAoVCqwQ=="
 	, host "github.com" 
@@ -389,55 +426,8 @@
 		& alias "backup.kitenet.net"
 		& alias "usbackup.kitenet.net"
 		& sshPubKey "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAokMXQiX/NZjA1UbhMdgAscnS5dsmy+Q7bWrQ6tsTZ/o+6N/T5cbjoBHOdpypXJI3y/PiJTDJaQtXIhLa8gFg/EvxMnMz/KG9skADW1361JmfCc4BxicQIO2IOOe6eilPr+YsnOwiHwL0vpUnuty39cppuMWVD25GzxXlS6KQsLCvXLzxLLuNnGC43UAM0q4UwQxDtAZEK1dH2o3HMWhgMP2qEQupc24dbhpO3ecxh2C9678a3oGDuDuNf7mLp3s7ptj5qF3onitpJ82U5o7VajaHoygMaSRFeWxP2c13eM57j3bLdLwxVXFhePcKXARu1iuFTLS5uUf3hN6MkQcOGw=="
-	, host "wren.kitenet.net"
+	, host "old.kitenet.net"
 		& ipv4 "80.68.85.49"
-		& ipv6 "2001:41c8:125:49::10"
-		& alias "kitenet.net"
-		& alias "ns1.kitenet.net"
-		& alias "ftp.kitenet.net"
-		& alias "mail.kitenet.net"
-		& alias "smtp.kitenet.net"
-		& alias "bitlbee.kitenet.net"
-		{- Remaining services on kite:
-		 -
-		 - / = ready to go on kite.kitenet.net
-		 - 
-		 - mail
-		 -   /postfix
-		 -   /postgrey
-		 -   mailman
-		 -   /spamassassin
-		 -   sqwebmail (cannot use this with dovecot, alternatives?)
-		 -   /imap server
-		 -   /pop server
-		 - /apache
-		 - bitlbee (EOL?)
-		 - prosody (EOL?)
-		 - ftpd (EOL)
-		 -
-		 - Pre-transition:
-		 - - re-rsync /home (skip ~joey and .pine*)
-		 -     cd /home && rsync -4 --progress -avz root@wren.kitenet.net:/home/ ./ --exclude='.pine*' --exclude='joey/*' --delete
-		 - 
-		 - Transition plan:
-		 - - on darkstar: offlineimap run & disable cron job
-		 -    & move offlineimap files to tmp
-		 - - take down wren pstfix, imap, pop servers
-		 - - log all users out of wren
-		 - - final /home rsync (skip ~joey and .pine*)
-		 - - rsync /var/mail
-		 - - rsync mailman and mailman list archives dirs
-		 - - switch kitenet.net dns and enable pop.kitenet.net etc aliass
-		 - - point wren.kitenet.net at kite.kitenet.net temporarily
-		 -   (make real-wren.kitenet.net alias)
-		 - - reconfigure errol's email client to use new server
-		 - - on darkstar: re-run offlinimap against new server
-		 - - test mail
-		 - - test virus filtering
-		 - - test http://kitenet.net/~kyle/ (user home dirs)
-		 - - test mailman
-		 - - migrate user cron jobs
-		 -}
 	, host "mouse.kitenet.net"
 		& ipv6 "2001:4830:1600:492::2"
 	, host "beaver.kitenet.net"
diff --git a/config-simple.hs b/config-simple.hs
--- a/config-simple.hs
+++ b/config-simple.hs
@@ -38,7 +38,7 @@
 
 	-- A generic webserver in a Docker container.
 	, Docker.container "webserver" "joeyh/debian-stable"
-		& os (System (Debian Stable) "amd64")
+		& os (System (Debian (Stable "wheezy")) "amd64")
 		& Apt.stdSourcesList
 		& Docker.publish "80:80"
 		& Docker.volume "/var/www:/var/www"
diff --git a/config.hs b/config.hs
--- a/config.hs
+++ b/config.hs
@@ -38,7 +38,7 @@
 
 	-- A generic webserver in a Docker container.
 	, Docker.container "webserver" "joeyh/debian-stable"
-		& os (System (Debian Stable) "amd64")
+		& os (System (Debian (Stable "wheezy")) "amd64")
 		& Apt.stdSourcesList
 		& Docker.publish "80:80"
 		& Docker.volume "/var/www:/var/www"
diff --git a/debian/changelog b/debian/changelog
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,18 @@
+propellor (0.9.0) unstable; urgency=medium
+
+  * Avoid encoding the current stable suite in propellor's code,
+    since that poses a difficult transition around the release,
+    and can easily be wrong if an older version of propellor is used.
+    Instead, the os property for a stable system includes the suite name
+    to use, eg Stable "wheezy".
+  * stdSourcesList uses the stable suite name, to avoid unwanted
+    immediate upgrades to the next stable release.
+  * debCdn switched from cdn.debian.net to http.debian.net, which seems to be
+    better managed now.
+  * Docker: Avoid committing container every time it's started up.
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 10 Oct 2014 11:37:45 -0400
+
 propellor (0.8.3) unstable; urgency=medium
 
   * The Debian package now includes a single-revision git repository in
diff --git a/doc/README.mdwn b/doc/README.mdwn
--- a/doc/README.mdwn
+++ b/doc/README.mdwn
@@ -44,9 +44,8 @@
 3. If you don't have a gpg private key already, generate one: `gpg --gen-key`
 4. Run: `propellor --add-key $KEYID`, which will make propellor trust
    your gpg key, and will sign your `~/.propellor` repository using it.
-5. `cd ~/.propellor/`; use git to push the repository to a central
-   server (github, or your own git server). Configure that central
-   server as the origin remote of the repository.
+5. Push the git repository to a central server (github or your own):
+   `cd ~/.propellor/; git remote add origin ssh://git.example.com/propellor.git; git push -u origin master`
 6. Edit `~/.propellor/config.hs`, and add a host you want to manage.
    You can start by not adding any properties, or only a few.
 7. Pick a host and run: `propellor --spin $HOST`
diff --git a/propellor.cabal b/propellor.cabal
--- a/propellor.cabal
+++ b/propellor.cabal
@@ -1,5 +1,5 @@
 Name: propellor
-Version: 0.8.3
+Version: 0.9.0
 Cabal-Version: >= 1.6
 License: BSD3
 Maintainer: Joey Hess <joey@kitenet.net>
diff --git a/src/Propellor/CmdLine.hs b/src/Propellor/CmdLine.hs
--- a/src/Propellor/CmdLine.hs
+++ b/src/Propellor/CmdLine.hs
@@ -37,9 +37,9 @@
 processCmdLine :: IO CmdLine
 processCmdLine = go =<< getArgs
   where
-  	go ("--help":_) = usage
-  	go ("--spin":h:[]) = return $ Spin h
-  	go ("--boot":h:[]) = return $ Boot h
+	go ("--help":_) = usage
+	go ("--spin":h:[]) = return $ Spin h
+	go ("--boot":h:[]) = return $ Boot h
 	go ("--add-key":k:[]) = return $ AddKey k
 	go ("--set":f:c:[]) = withprivfield f c Set
 	go ("--dump":f:c:[]) = withprivfield f c Dump
@@ -48,7 +48,7 @@
 	go ("--continue":s:[]) = case readish s of
 		Just cmdline -> return $ Continue cmdline
 		Nothing -> errorMessage "--continue serialization failure"
-  	go ("--chain":h:[]) = return $ Chain h
+	go ("--chain":h:[]) = return $ Chain h
 	go ("--docker":h:[]) = return $ Docker h
 	go (h:[])
 		| "--" `isPrefixOf` h = usage
@@ -237,6 +237,7 @@
 			sendMarked toh marker s
 			return True
 
+-- Initial git clone, used for bootstrapping.
 sendGitClone :: HostName -> String -> IO ()
 sendGitClone hn url = void $ actionMessage ("Pushing git repository to " ++ hn) $ do
 	branch <- getCurrentBranch
diff --git a/src/Propellor/PrivData.hs b/src/Propellor/PrivData.hs
--- a/src/Propellor/PrivData.hs
+++ b/src/Propellor/PrivData.hs
@@ -114,7 +114,7 @@
 	showtable "Data that would be used if set:" $
 		map mkrow (M.keys $ M.difference wantedmap m)
   where
-  	header = ["Field", "Context", "Used by"]
+	header = ["Field", "Context", "Used by"]
 	mkrow k@(field, (Context context)) =
 		[ shellEscape $ show field
 		, shellEscape context
diff --git a/src/Propellor/Property.hs b/src/Propellor/Property.hs
--- a/src/Propellor/Property.hs
+++ b/src/Propellor/Property.hs
@@ -31,7 +31,7 @@
 combineProperties :: Desc -> [Property] -> Property
 combineProperties desc ps = Property desc (go ps NoChange) (combineInfos ps)
   where
-  	go [] rs = return rs
+	go [] rs = return rs
 	go (l:ls) rs = do
 		r <- ensureProperty l
 		case r of
diff --git a/src/Propellor/Property/Apache.hs b/src/Propellor/Property/Apache.hs
--- a/src/Propellor/Property/Apache.hs
+++ b/src/Propellor/Property/Apache.hs
@@ -54,7 +54,7 @@
 installed = Apt.installed ["apache2"]
 
 restarted :: Property
-restarted = cmdProperty "service" ["apache2", "restart"]
+restarted = Service.restarted "apache2"
 
 reloaded :: Property
 reloaded = Service.reloaded "apache2"
diff --git a/src/Propellor/Property/Apt.hs b/src/Propellor/Property/Apt.hs
--- a/src/Propellor/Property/Apt.hs
+++ b/src/Propellor/Property/Apt.hs
@@ -20,14 +20,14 @@
 type SourcesGenerator = DebianSuite -> [Line]
 
 showSuite :: DebianSuite -> String
-showSuite Stable = "stable"
+showSuite (Stable s) = s
 showSuite Testing = "testing"
 showSuite Unstable = "unstable"
 showSuite Experimental = "experimental"
-showSuite (DebianRelease r) = r
 
-backportSuite :: String
-backportSuite = showSuite stableRelease ++ "-backports"
+backportSuite :: DebianSuite -> Maybe String
+backportSuite (Stable s) = Just (s ++ "-backports")
+backportSuite _ = Nothing
 
 debLine :: String -> Url -> [Section] -> Line
 debLine suite mirror sections = unwords $
@@ -42,15 +42,20 @@
 stdSections = ["main", "contrib", "non-free"]
 
 binandsrc :: String -> SourcesGenerator
-binandsrc url suite
-	| isStable suite = [l, srcLine l, bl, srcLine bl]
-	| otherwise = [l, srcLine l]
+binandsrc url suite = catMaybes
+	[ Just l
+	, Just $ srcLine l
+	, bl
+	, srcLine <$> bl
+	]
   where
 	l = debLine (showSuite suite) url stdSections
-	bl = debLine backportSuite url stdSections
+	bl = do
+		bs <- backportSuite suite
+		return $ debLine bs url stdSections
 
 debCdn :: SourcesGenerator
-debCdn = binandsrc "http://cdn.debian.net/debian"
+debCdn = binandsrc "http://http.debian.net/debian"
 
 kernelOrg :: SourcesGenerator
 kernelOrg = binandsrc "http://mirrors.kernel.org/debian"
@@ -128,13 +133,14 @@
 installedBackport :: [Package] -> Property
 installedBackport ps = trivial $ withOS desc $ \o -> case o of
 	Nothing -> error "cannot install backports; os not declared"
-	(Just (System (Debian suite) _))
-		| isStable suite -> 
-			ensureProperty $ runApt $ 
-				["install", "-t", backportSuite, "-y"] ++ ps
-	_ -> error $ "backports not supported on " ++ show o
+	(Just (System (Debian suite) _)) -> case backportSuite suite of
+		Nothing -> notsupported o
+		Just bs -> ensureProperty $ runApt $ 
+			["install", "-t", bs, "-y"] ++ ps
+	_ -> notsupported o
   where
 	desc = (unwords $ "apt installed backport":ps)
+	notsupported o = error $ "backports not supported on " ++ show o
 
 -- | Minimal install of package, without recommends.
 installedMin :: [Package] -> Property
diff --git a/src/Propellor/Property/Cmd.hs b/src/Propellor/Property/Cmd.hs
--- a/src/Propellor/Property/Cmd.hs
+++ b/src/Propellor/Property/Cmd.hs
@@ -33,7 +33,7 @@
 		, return FailedChange
 		)
   where
-  	desc = unwords $ cmd : params
+	desc = unwords $ cmd : params
 
 -- | A property that can be satisfied by running a series of shell commands.
 scriptProperty :: [String] -> Property
diff --git a/src/Propellor/Property/Dns.hs b/src/Propellor/Property/Dns.hs
--- a/src/Propellor/Property/Dns.hs
+++ b/src/Propellor/Property/Dns.hs
@@ -117,7 +117,7 @@
 		`requires` servingZones
 	cleanup = namedConfWritten
 
- 	desc = "dns secondary for " ++ domain
+	desc = "dns secondary for " ++ domain
 	conf = NamedConf
 		{ confDomain = domain
 		, confDnsServerType = Secondary
@@ -380,7 +380,7 @@
 			[] -> [ret (CNAME c)]
 			l -> map (ret . Address) l
 		  where
-		  	ret record = Right (c, record)
+			ret record = Right (c, record)
 	
 	-- Adds any other DNS records for a host located in the zdomain.
 	hostrecords :: Host -> [Either WarningMessage (BindDomain, Record)]
@@ -420,7 +420,7 @@
 addNamedConf :: NamedConf -> Info
 addNamedConf conf = mempty { _namedconf = NamedConfMap (M.singleton domain conf) }
   where
-       domain = confDomain conf
+	domain = confDomain conf
 
 getNamedConf :: Propellor (M.Map Domain NamedConf)
 getNamedConf = asks $ fromNamedConfMap . _namedconf . hostInfo
diff --git a/src/Propellor/Property/Docker.hs b/src/Propellor/Property/Docker.hs
--- a/src/Propellor/Property/Docker.hs
+++ b/src/Propellor/Property/Docker.hs
@@ -13,6 +13,7 @@
 	docked,
 	memoryLimited,
 	garbageCollected,
+	tweaked,
 	Image,
 	ContainerName,
 	-- * Container configuration
@@ -102,7 +103,7 @@
   where
 	go desc a = property (desc ++ " " ++ cn) $ do
 		hn <- asks hostName
-  		let cid = ContainerId hn cn
+		let cid = ContainerId hn cn
 		ensureProperties [findContainer mhost cid cn $ a cid]
 		
 	mhost = findHost hosts (cn2hn cn)
@@ -152,7 +153,7 @@
 	<*> pure (map (\a -> a hn) (_dockerRunParams info))
   where
 	info = _dockerinfo $ hostInfo h'
-  	h' = h
+	h' = h
 		-- expose propellor directory inside the container
 		& volume (localdir++":"++localdir)
 		-- name the container in a predictable way so we
@@ -176,6 +177,16 @@
 	gcimages = property "docker images garbage collected" $ do
 		liftIO $ report <$> (mapM removeImage =<< listImages)
 
+-- | Tweaks a container to work well with docker.
+--
+-- Currently, this consists of making pam_loginuid lines optional in
+-- the pam config, to work around https://github.com/docker/docker/issues/5663
+-- which affects docker 1.2.0.
+tweaked :: Property
+tweaked = trivial $
+	cmdProperty "sh" ["-c", "sed -ri 's/^session\\s+required\\s+pam_loginuid.so$/session optional pam_loginuid.so/' /etc/pam.d/*"]
+	`describe` "tweaked for docker"
+
 -- | Configures the kernel to respect docker memory limits. 
 --
 -- This assumes the system boots using grub 2. And that you don't need any
@@ -303,30 +314,38 @@
 runningContainer cid@(ContainerId hn cn) image runps = containerDesc cid $ property "running" $ do
 	l <- liftIO $ listContainers RunningContainers
 	if cid `elem` l
-		then do
-			-- Check if the ident has changed; if so the
-			-- parameters of the container differ and it must
-			-- be restarted.
-			runningident <- liftIO $ getrunningident
-			if runningident == Just ident
-				then noChange
-				else do
-					void $ liftIO $ stopContainer cid
-					restartcontainer
+		then checkident =<< liftIO (getrunningident simpleShClient)
 		else ifM (liftIO $ elem cid <$> listContainers AllContainers)
-			( restartcontainer
+			( do
+				-- The container exists, but is not
+				-- running. Its parameters may have
+				-- changed, but we cannot tell without
+				-- starting it up first.
+				void $ liftIO $ startContainer cid
+				-- It can take a while for the container to
+				-- start up enough to get its ident, so
+				-- retry for up to 60 seconds.
+				checkident =<< liftIO (getrunningident (simpleShClientRetry 60))
 			, go image
 			)
   where
 	ident = ContainerIdent image hn cn runps
 
+	-- Check if the ident has changed; if so the
+	-- parameters of the container differ and it must
+	-- be restarted.
+	checkident runningident
+		| runningident == Just ident = noChange
+		| otherwise = do
+			void $ liftIO $ stopContainer cid
+			restartcontainer
+
 	restartcontainer = do
 		oldimage <- liftIO $ fromMaybe image <$> commitContainer cid
 		void $ liftIO $ removeContainer cid
 		go oldimage
 
-	getrunningident :: IO (Maybe ContainerIdent)
-	getrunningident = simpleShClient (namedPipe cid) "cat" [propellorIdent] $ \rs -> do
+	getrunningident shclient = shclient (namedPipe cid) "cat" [propellorIdent] $ \rs -> do
 		let !v = extractident rs
 		return v
 
@@ -421,6 +440,9 @@
 
 stopContainer :: ContainerId -> IO Bool
 stopContainer cid = boolSystem dockercmd [Param "stop", Param $ fromContainerId cid ]
+
+startContainer :: ContainerId -> IO Bool
+startContainer cid = boolSystem dockercmd [Param "start", Param $ fromContainerId cid ]
 
 stoppedContainer :: ContainerId -> Property
 stoppedContainer cid = containerDesc cid $ property desc $ 
diff --git a/src/Propellor/Property/Obnam.hs b/src/Propellor/Property/Obnam.hs
--- a/src/Propellor/Property/Obnam.hs
+++ b/src/Propellor/Property/Obnam.hs
@@ -105,12 +105,12 @@
 latestVersion :: Property
 latestVersion = withOS "obnam latest version" $ \o -> case o of
 	(Just (System (Debian suite) _)) | isStable suite -> ensureProperty $
-		Apt.setSourcesListD stablesources "obnam"
+		Apt.setSourcesListD (stablesources suite) "obnam"
 			`requires` toProp (Apt.trustsKey key)
 	_ -> noChange
   where
-	stablesources = 
-		[ "deb http://code.liw.fi/debian " ++ Apt.showSuite stableRelease ++ " main"
+	stablesources suite = 
+		[ "deb http://code.liw.fi/debian " ++ Apt.showSuite suite ++ " main"
 		]
 	-- gpg key used by the code.liw.fi repository.
 	key = Apt.AptKey "obnam" $ unlines
diff --git a/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs b/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs
--- a/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs
+++ b/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs
@@ -98,6 +98,7 @@
 	& tree arch
 	& buildDepsApt
 	& autobuilder arch (show buildminute ++ " * * * *") timeout
+	& Docker.tweaked
 
 androidAutoBuilderContainer :: (System -> Docker.Image) -> Cron.CronTimes -> TimeOut -> Host
 androidAutoBuilderContainer dockerImage crontimes timeout =
@@ -108,8 +109,8 @@
 -- Android is cross-built in a Debian i386 container, using the Android NDK.
 androidContainer :: (System -> Docker.Image) -> Docker.ContainerName -> Property -> FilePath -> Host
 androidContainer dockerImage name setupgitannexdir gitannexdir = Docker.container name
-	(dockerImage $ System (Debian Stable) "i386")
-	& os (System (Debian Stable) "i386")
+	(dockerImage osver)
+	& os osver
 	& Apt.stdSourcesList
 	& Apt.installed ["systemd"]
 	& User.accountFor builduser
@@ -118,6 +119,7 @@
 	& buildDepsNoHaskellLibs
 	& flagFile chrootsetup ("/chrootsetup")
 		`requires` setupgitannexdir
+	& Docker.tweaked
 	-- TODO: automate installing haskell libs
 	-- (Currently have to run
 	-- git-annex/standalone/android/install-haskell-packages
@@ -129,6 +131,7 @@
 	chrootsetup = scriptProperty
 		[ "cd " ++ gitannexdir ++ " && ./standalone/android/buildchroot-inchroot"
 		]
+	osver = System (Debian (Stable "wheezy")) "i386"
 
 -- armel builder has a companion container using amd64 that
 -- runs the build first to get TH splices. They need
@@ -139,7 +142,6 @@
 	& os (System (Debian Testing) "amd64")
 	& Apt.stdSourcesList
 	& Apt.installed ["systemd"]
-	& Apt.unattendedUpgrades
 	-- This volume is shared with the armel builder.
 	& Docker.volume gitbuilderdir
 	& User.accountFor builduser
@@ -151,13 +153,13 @@
 	& Docker.expose "22"
 	& Apt.serviceInstalledRunning "ssh"
 	& Ssh.authorizedKeys builduser (Context "armel-git-annex-builder")
+	& Docker.tweaked
 
 armelAutoBuilderContainer :: (System -> Docker.Image) -> Cron.CronTimes -> TimeOut -> Host
 armelAutoBuilderContainer dockerImage crontimes timeout = Docker.container "armel-git-annex-builder"
 	(dockerImage $ System (Debian Unstable) "armel")
 	& os (System (Debian Testing) "armel")
 	& Apt.stdSourcesList
-	& Apt.unattendedUpgrades
 	& Apt.installed ["systemd"]
 	& Apt.installed ["openssh-client"]
 	& Docker.link "armel-git-annex-builder-companion" "companion"
@@ -172,6 +174,7 @@
 		`requires` tree "armel"
 	& Ssh.keyImported SshRsa builduser (Context "armel-git-annex-builder")
 	& trivial writecompanionaddress
+	& Docker.tweaked
   where
 	writecompanionaddress = scriptProperty
 		[ "echo \"$COMPANION_PORT_22_TCP_ADDR\" > " ++ homedir </> "companion_address"
diff --git a/src/Propellor/Property/SiteSpecific/JoeySites.hs b/src/Propellor/Property/SiteSpecific/JoeySites.hs
--- a/src/Propellor/Property/SiteSpecific/JoeySites.hs
+++ b/src/Propellor/Property/SiteSpecific/JoeySites.hs
@@ -70,7 +70,10 @@
 	datadir = "/var/spool/oldusenet"
 
 oldUseNetShellBox :: Property
-oldUseNetShellBox = oldUseNetInstalled "oldusenet"
+oldUseNetShellBox = propertyList "olduse.net shellbox"
+	[ oldUseNetInstalled "oldusenet"
+	, Service.running "oldusenet"
+	]
 
 oldUseNetInstalled :: Apt.Package -> Property
 oldUseNetInstalled pkg = check (not <$> Apt.isInstalled pkg) $
@@ -376,7 +379,7 @@
 obnamRepos rs = propertyList ("obnam repos for " ++ unwords rs)
 	(mkbase : map mkrepo rs)
   where
-  	mkbase = mkdir "/home/joey/lib/backup"
+	mkbase = mkdir "/home/joey/lib/backup"
 		`requires` mkdir "/home/joey/lib"
 	mkrepo r = mkdir ("/home/joey/lib/backup/" ++ r ++ ".obnam")
 	mkdir d = File.dirExists d
@@ -452,8 +455,16 @@
 		]
 		`onChange` Postfix.reloaded
 		`describe` "postfix mydomain file configured"
-	, "/etc/postfix/obscure_client_relay.pcre" `File.containsLine`
-		"/^Received: from ([^.]+)\\.kitenet\\.net.*using TLS.*by kitenet\\.net \\(([^)]+)\\) with (E?SMTPS?A?) id ([A-F[:digit:]]+)(.*)/ IGNORE"
+	, "/etc/postfix/obscure_client_relay.pcre" `File.hasContent`
+		-- Remove received lines for mails relayed from trusted
+		-- clients. These can be a privacy vilation, or trigger
+		-- spam filters.
+		[ "/^Received: from ([^.]+)\\.kitenet\\.net.*using TLS.*by kitenet\\.net \\(([^)]+)\\) with (E?SMTPS?A?) id ([A-F[:digit:]]+)(.*)/ IGNORE"
+		-- Munge local Received line for postfix running on a
+		-- trusted client that relays through. These can trigger
+		-- spam filters.
+		, "/^Received: by ([^.]+)\\.kitenet\\.net.*/ REPLACE Received: by kitenet.net"
+		]
 		`onChange` Postfix.reloaded
 		`describe` "postfix obscure_client_relay file configured"
 	, Postfix.mappedFile "/etc/postfix/virtual"
@@ -482,7 +493,7 @@
 		, "header_checks = pcre:$config_directory/obscure_client_relay.pcre"
 
 		, "# Enable postgrey."
-		, "smtpd_recipient_restrictions = permit_mynetworks,reject_unauth_destination,check_policy_service inet:127.0.0.1:10023"
+		, "smtpd_recipient_restrictions = permit_tls_clientcerts,permit_mynetworks,reject_unauth_destination,check_policy_service inet:127.0.0.1:10023"
 
 		, "# Enable spamass-milter and amavis-milter."
 		, "smtpd_milters = unix:/spamass/spamass.sock unix:amavis/amavis.sock"
@@ -541,10 +552,13 @@
 		`onChange` (pinescript `File.mode`
 			combineModes (readModes ++ executeModes))
 		`describe` "pine wrapper script"
-	, "/etc/pine.conf" `File.containsLines`
-		[ "inbox-path={localhost/novalidate-cert}inbox"
+	, "/etc/pine.conf" `File.hasContent`
+		[ "# deployed with propellor"
+		, "inbox-path={localhost/novalidate-cert/NoRsh}inbox"
 		]
 		`describe` "pine configured to use local imap server"
+	
+	, Apt.serviceInstalledRunning "mailman"
 	]
   where
 	ctx = Context "kitenet.net"
@@ -705,8 +719,8 @@
 		]
 	, alias "joey.kitenet.net"
 	, toProp $ Apache.siteEnabled "joey.kitenet.net" $ apachecfg "joey.kitenet.net" False
-		[ "DocumentRoot /home/joey/html"
-		, "<Directory /home/joey/html/>"
+		[ "DocumentRoot /var/www"
+		, "<Directory /var/www/>"
 		, "  Options Indexes ExecCGI"
 		, "  AllowOverride None"
 		, Apache.allowAll
diff --git a/src/Propellor/Property/Ssh.hs b/src/Propellor/Property/Ssh.hs
--- a/src/Propellor/Property/Ssh.hs
+++ b/src/Propellor/Property/Ssh.hs
@@ -3,7 +3,7 @@
 	permitRootLogin,
 	passwordAuthentication,
 	hasAuthorizedKeys,
-	restartSshd,
+	restarted,
 	randomHostKeys,
 	hostKeys,
 	hostKey,
@@ -15,6 +15,7 @@
 
 import Propellor
 import qualified Propellor.Property.File as File
+import qualified Propellor.Property.Service as Service
 import Propellor.Property.User
 import Utility.SafeCommand
 import Utility.FileMode
@@ -33,7 +34,7 @@
 	[ sshdConfig `File.lacksLine` (sshline $ not allowed)
 	, sshdConfig `File.containsLine` (sshline allowed)
 	]
-	`onChange` restartSshd
+	`onChange` restarted
 	`describe` unwords [ "ssh config:", setting, sshBool allowed ]
   where
 	sshline v = setting ++ " " ++ sshBool v
@@ -59,15 +60,15 @@
   where
 	go f = not . null <$> catchDefaultIO "" (readFile f)
 
-restartSshd :: Property
-restartSshd = cmdProperty "service" ["ssh", "restart"]
+restarted :: Property
+restarted = Service.restarted "ssh"
 
 -- | Blows away existing host keys and make new ones.
 -- Useful for systems installed from an image that might reuse host keys.
 -- A flag file is used to only ever do this once.
 randomHostKeys :: Property
 randomHostKeys = flagFile prop "/etc/ssh/.unique_host_keys"
-	`onChange` restartSshd
+	`onChange` restarted
   where
 	prop = property "ssh random host keys" $ do
 		void $ liftIO $ boolSystem "sh"
@@ -91,9 +92,9 @@
 	[ installkey (SshPubKey keytype "")  (install writeFile ".pub")
 	, installkey (SshPrivKey keytype "") (install writeFileProtected "")
 	]
-	`onChange` restartSshd
+	`onChange` restarted
   where
- 	desc = "known ssh host key (" ++ fromKeyType keytype ++ ")"
+	desc = "known ssh host key (" ++ fromKeyType keytype ++ ")"
 	installkey p a = withPrivData p context $ \getkey ->
 		property desc $ getkey a
 	install writer ext key = do
@@ -176,7 +177,7 @@
 	portline = "Port " ++ show port
 	enable = sshdConfig `File.containsLine` portline
 		`describe` ("ssh listening on " ++ portline)
-		`onChange` restartSshd
+		`onChange` restarted
 	disable = sshdConfig `File.lacksLine` portline
 		`describe` ("ssh not listening on " ++ portline)
-		`onChange` restartSshd
+		`onChange` restarted
diff --git a/src/Propellor/Property/Sudo.hs b/src/Propellor/Property/Sudo.hs
--- a/src/Propellor/Property/Sudo.hs
+++ b/src/Propellor/Property/Sudo.hs
@@ -27,6 +27,6 @@
 		| not (sudobaseline `isPrefixOf` l) = True
 		| "NOPASSWD" `isInfixOf` l = locked
 		| otherwise = True
- 	modify locked ls
+	modify locked ls
 		| sudoline locked `elem` ls = ls
 		| otherwise = ls ++ [sudoline locked]
diff --git a/src/Propellor/Property/Tor.hs b/src/Propellor/Property/Tor.hs
--- a/src/Propellor/Property/Tor.hs
+++ b/src/Propellor/Property/Tor.hs
@@ -3,6 +3,7 @@
 import Propellor
 import qualified Propellor.Property.File as File
 import qualified Propellor.Property.Apt as Apt
+import qualified Propellor.Property.Service as Service
 
 isBridge :: Property
 isBridge = setup `requires` Apt.installed ["tor"]
@@ -13,7 +14,7 @@
 		, "ORPort 443"
 		, "BridgeRelay 1"
 		, "Exitpolicy reject *:*"
-		] `onChange` restartTor
+		] `onChange` restarted
 
-restartTor :: Property
-restartTor = cmdProperty "service" ["tor", "restart"]
+restarted :: Property
+restarted = Service.restarted "tor"
diff --git a/src/Propellor/SimpleSh.hs b/src/Propellor/SimpleSh.hs
--- a/src/Propellor/SimpleSh.hs
+++ b/src/Propellor/SimpleSh.hs
@@ -48,8 +48,8 @@
 
 		flip catchIO (\_e -> writeChan chan Done) $ do
 			let p = (proc cmd params)
-	                	{ std_in = Inherit
-		                , std_out = CreatePipe
+				{ std_in = Inherit
+				, std_out = CreatePipe
 				, std_err = CreatePipe
 				}
 			(Nothing, Just outh, Just errh, pid) <- createProcess p
diff --git a/src/Propellor/Types.hs b/src/Propellor/Types.hs
--- a/src/Propellor/Types.hs
+++ b/src/Propellor/Types.hs
@@ -89,7 +89,7 @@
 	getInfo = propertyInfo
 	x `requires` y = Property (propertyDesc x) satisfy info
 	  where
-	  	info = getInfo y <> getInfo x
+		info = getInfo y <> getInfo x
 		satisfy = do
 			r <- propertySatisfy y
 			case r of
@@ -146,4 +146,4 @@
 	| Continue CmdLine
 	| Chain HostName
 	| Docker HostName
-  deriving (Read, Show, Eq)
+	deriving (Read, Show, Eq)
diff --git a/src/Propellor/Types/OS.hs b/src/Propellor/Types/OS.hs
--- a/src/Propellor/Types/OS.hs
+++ b/src/Propellor/Types/OS.hs
@@ -13,15 +13,14 @@
 	| Ubuntu Release
 	deriving (Show, Eq)
 
-data DebianSuite = Experimental | Unstable | Testing | Stable | DebianRelease Release
+-- | Debian has several rolling suites, and a number of stable releases,
+-- such as Stable "wheezy".
+data DebianSuite = Experimental | Unstable | Testing | Stable Release
 	deriving (Show, Eq)
 
--- | The release that currently corresponds to stable.
-stableRelease :: DebianSuite
-stableRelease = DebianRelease "wheezy"
-
 isStable :: DebianSuite -> Bool
-isStable s = s == Stable || s == stableRelease
+isStable (Stable _) = True
+isStable _ = False
 
 type Release = String
 type Architecture = String
diff --git a/src/config.hs b/src/config.hs
--- a/src/config.hs
+++ b/src/config.hs
@@ -38,7 +38,7 @@
 
 	-- A generic webserver in a Docker container.
 	, Docker.container "webserver" "joeyh/debian-stable"
-		& os (System (Debian Stable) "amd64")
+		& os (System (Debian (Stable "wheezy")) "amd64")
 		& Apt.stdSourcesList
 		& Docker.publish "80:80"
 		& Docker.volume "/var/www:/var/www"
