Docker + Nginx + Let's Encrypt

This post shows how to set up multiple websites running behind a dockerized Nginx reverse proxy and served via HTTPS using free Let’s Encrypt certificates. New sites can be added on the fly by just modifying docker-compose.yml and then running docker-compose up as the main Nginx config is automatically updated and certificates (if needed) are automatically acquired.

Some of the configuration is derived from https://github.com/fatk/docker-letsencrypt-nginx-proxy-companion-examples with some simplifications and updates to work with current nginx.tmpl from nginx-proxy and docker-compose v2 files.

Running the example

The repository for this is at https://github.com/gilyes/docker-nginx-letsencrypt-sample.

Prerequisites

  • docker (>= 1.10)
  • docker-compose (>= 1.8.1)
  • access to (sub)domain(s) pointing to a publicly accessible server (required for TLS)

Preparation

  • Clone the repository on the server pointed to by your domain.
  • In docker-compose.yml:
    • Change the VIRTUAL_HOST and LETSENCRYPT_HOST entries from sampleapi.example.com and samplewebsite.example.com to your domains.
    • Change LETSENCRYPT_EMAIL entries to the email address you want to be associated with the certificates.
  • In volumes/config/sample-website/config.js change apiUrl to your API endpoint as set up in the previous point in docker-compose.yml.

Running

In the main directory run:

docker-compose up

This will perform the following steps:

  • Download the required images from Docker Hub (nginx, docker-gen, docker-letsencrypt-nginx-proxy-companion).
  • Create containers from them.
  • Build and create containers for the two sites located in sample-websites.
  • Start up the containers.
    • docker-letsencrypt-nginx-proxy-companion inspects containers’ metadata and tries to acquire certificates as needed (if successful then saving them in a volume shared with the host and the Nginx container).
    • docker-gen also inspects containers’ metadata and generates the configuration file for the main Nginx reverse proxy

If everything went well then you should now be able to access your website at the provided address.

Troubleshooting

  • To view logs run docker-compose logs.
  • To view the generated Nginx configuration run docker exec -ti nginx cat /etc/nginx/conf.d/default.conf

How does it work

The system consists of 4 main parts:

  • Main Nginx reverse proxy container.
  • Container that generates the main Nginx config based on container metadata.
  • Container that automatically handles the acquisition and renewal of Let’s Encrypt TLS certificates.
  • The actual websites living in their own containers. In this example, a very simple website, talking to a very simple API.

The main Nginx reverse proxy container

This is the only publicly exposed container, routes traffic to the backend servers and provides TLS termination.

Uses the official nginx Docker image.

It is defined in docker-compose.yml under the nginx service block:

1
2
3
4
5
6
7
8
9
10
11
12
13
services:
nginx:
restart: always
image: nginx
container_name: nginx
ports:
- "80:80"
- "443:443"
volumes:
- "/etc/nginx/conf.d"
- "/etc/nginx/vhost.d"
- "/usr/share/nginx/html"
- "./volumes/proxy/certs:/etc/nginx/certs:ro"

As you can see it shares a few volumes:

  • Configuration folder: used by the container that generates the configuration file.
  • Default Nginx root folder: used by the Let’s Encrypt container for challenges from the CA.
  • Certificates folder: written to by the Let’s Encrypt container, this is where the TLS certificates are maintained.

The configuration generator container

This container inspects the other running containers and based on their metadata (like VIRTUAL_HOST environment variable) and a template file it generates the Nginx configuration file for the main Nginx container. When a new container is spinning up this container detects that, generates the appropriate configuration entries and restarts Nginx.

Uses the jwilder/docker-gen Docker image.

It is defined in docker-compose.yml under the nginx-gen service block:

1
2
3
4
5
6
7
8
9
10
11
12
13
services:
...

nginx-gen:
restart: always
image: jwilder/docker-gen
container_name: nginx-gen
volumes:
- "/var/run/docker.sock:/tmp/docker.sock:ro"
- "./volumes/proxy/templates/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro"
volumes_from:
- nginx
entrypoint: /usr/local/bin/docker-gen -notify-sighup nginx -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf

The container reads the nginx.tmpl template file (source: jwilder/nginx-proxy) via a volume shared with the host.

It also mounts the Docker socket into the container in order to be able to inspect the other containers (the "/var/run/docker.sock:/tmp/docker.sock:ro" line). Security warning: mounting the Docker socket is usually discouraged because the container getting (even read-only) access to it can get root access to the host. In our case, this container is not exposed to the world so if you trust the code running inside it the risks are probably fairly low. But definitely something to take into account. See e.g. The Dangers of Docker.sock for further details.

NOTE: it would be preferrable to have docker-gen only handle containers with exposed ports (via -only-exposed flag in the entrypoint script above) but currently that does not work, see e.g. https://github.com/jwilder/nginx-proxy/issues/438.

The Let’s Encrypt container

This container also inspects the other containers and acquires Let’s Encrypt TLS certificates based on the LETSENCRYPT_HOST and LETSENCRYPT_EMAIL environment variables. At regular intervals it checks and renews certificates as needed.

Uses the jrcs/letsencrypt-nginx-proxy-companion Docker image.

It is defined in docker-compose.yml under the letsencrypt-nginx-proxy-companion service block:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
services:
...

letsencrypt-nginx-proxy-companion:
restart: always
image: jrcs/letsencrypt-nginx-proxy-companion
container_name: letsencrypt-nginx-proxy-companion
volumes_from:
- nginx
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./volumes/proxy/certs:/etc/nginx/certs:rw"
environment:
- NGINX_DOCKER_GEN_CONTAINER=nginx-gen

The container uses a volume shared with the host and the Nginx container to maintain the certificates.

It also mounts the Docker socket in order to inspect the other containers. See the security warning above in the docker-gen section about the risks of that.

The sample website and the sample API

These two very simple samples are running in their own respective containers. They are defined in docker-compose.yml under the sample-api and sample-website service blocks:

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
services:
...

sample-api:
restart: always
image: sample-api
build: ./samples/api
container_name: sample-api
environment:
- VIRTUAL_HOST=sampleapi.example.com
- VIRTUAL_NETWORK=nginx-proxy
- VIRTUAL_PORT=3000
- LETSENCRYPT_HOST=sampleapi.example.com
- [email protected]

sample-website:
restart: always
image: sample-website
build: ./samples/website
container_name: sample-website
volumes:
- "./volumes/nginx-sample-website/conf.d/:/etc/nginx/conf.d"
- "./volumes/config/sample-website/config.js:/usr/share/nginx/html/config.js"
environment:
- VIRTUAL_HOST=samplewebsite.example.com
- VIRTUAL_NETWORK=nginx-proxy
- VIRTUAL_PORT=80
- LETSENCRYPT_HOST=sample.example.com
- [email protected]

The important part here are the environment variables. These are used by the config generator and certificate maintainer containers to set up the system.

The source code for these two images is in the samples subfolder, the images are built from there. In a real-world scenario these images would likely come from a Docker registry.

Conclusion

This can be a fairly simple way to have easy, reproducible deploys for websites with free, auto-renewing TLS certificates.


Installing Arch Linux with LVM

Recently I decided to give Arch Linux a try. This is a quick overview of the steps I followed to get up and running on an old Thinkpad T400.

This is mostly a reference for myself, but hopefully it will be somewhat useful for someone out there as well. It is mainly based on the excellent ArchWiki with additional stuff from here and there.

Getting Arch

This guide assumes you already have a bootable Arch installation media. See here on how to get it.

Installation

Start with booting into the Arch live CD/USB.

Connecting to Wi-Fi

Internet connection will be required during install, so let’s do that as the first step (for wired connection see e.g. here).

wifi-menu

Select wireless network, type in the password and if everything went well you should now be connected to the Internet.

Update the system clock

timedatectl set-ntp true

Set up LVM

To view details about the available storage devices:

lsblk

Determine the drive(s)/partition(s) you want to use. In this example I am creating a single LVM physical volume using a full drive.

First, create a Linux LVM partition spanning the whole disk:

fdisk /dev/sdX
n (to add a new partition)
p (for primary partition)
1 (default partition number)
(accept default start)
(accept default end)
t (to change partition type)
8e (for LVM partition when using MBR)
i (to verify)
w (save and quit)

Create an LVM physical volume on the new partition:

pvcreate /dev/sdX1

Create a volume group:

vgcreate vg1 /dev/sdX1

Create logical volumes for boot, root, swap and home.

lvcreate -L 200M -n boot vg1
lvcreate -L 20G -n root vg1
lvcreate -L 4G -n swap vg1
lvcreate -l 100%FREE -n home vg1

Note: instead of -l 100%FREE you may want to leave some unused space for snapshots.

Format the logical volumes:

mkfs.ext4 /dev/vg1/boot
mkfs.ext4 /dev/vg1/root
mkfs.ext4 /dev/vg1/home
mkswap /dev/vg1/swap
swapon /dev/vg1/swap

Then finally mount them under the live system’s /mnt:

mount /dev/vg1/root /mnt
mkdir /mnt/boot
mount /dev/vg1/boot /mnt/boot
mkdir /mnt/home
mount /dev/vg1/home /mnt/home

Install base packages

pacstrap /mnt base base-devel

Generate fstab

genfstab -U /mnt >> /mnt/etc/fstab

Change root to new system

arch-chroot /mnt /bin/bash

Set up locale

In /etc/locale.gen uncomment en_US.UTF-8 UTF-8 or whatever else locale you need then generate the new locale(s):

locale-gen

Create /etc/locale.conf:

# /etc/locale.conf
LANG=en_US.UTF-8

Setup time

Set time zone:

tzselect
ln -s /usr/share/zoneinfo/Zone/SubZone /etc/localtime

Adjust the time skew, and set the time standard to UTC:

hwclock --systohc --utc

Update initramfs

Add LVM2 hook to /etc/mkinitcpio.conf:

# /etc/mkinitcpio.conf
HOOKS="...lvm2 filesystems..."

Re-generate the initramfs image:

mkinitcpio -p linux

Bootloader

In this example we are using MBR with grub. Install grub:

pacman -S grub

Install the bootloader to the drive Arch was installed to:

grub-install --target=i386-pc /dev/sdX

Generate configuration file:

grub-mkconfig -o /boot/grub/grub.cfg

Configure network

Set hostname:

# /etc/hostname
myhostname

Install packages for wireless support:

pacman -S iw wpa_supplicant dialog

Set root password

passwd  

Reboot to new system

Exit the chroot environment, unmount partitions, remove live media and reboot:

exit
umount -R /mnt
reboot

Setup automatic Wi-Fi connection

wifi-menu -o
netctl start profileName
netctl enable profileName

Install desktop environment

For desktop environment let’s install Cinnamon. First, X:

pacman -S xorg-server xorg-xinit xorg-utils xorg-server-utils mesa xorg-twm xterm

Touchpad support:

pacman -S xf86-input-synaptics

Detemine the video card:

lspci | grep VGA

Search for video driver packages:

pacman –Ss | grep xf86-video

In this machine I have an Intel integrated video card, so installing driver for that:

pacman -S xf86-video-intel

Install Cinnamon:

pacman -S cinnamon nemo-fileroller

Install and start display manager:

pacman –S gdm
systemctl enable gdm
systemctl start gdm

Note: On password entry click the gear icon and select Cinnamon.

And done, a barebones Arch Linux installation with a desktop environment is now complete.

Resources

https://wiki.archlinux.org/index.php/Beginners%27_guide

http://blog.ataboydesign.com/2012/08/29/installing-arch-linux-on-lvm

https://www.linuxserver.io/index.php/2014/01/18/installing-arch-linux-with-root-on-an-lvm

https://wiki.archlinux.org/index.php/LVM

http://www.tecmint.com/install-cinnamon-desktop-in-arch-linux

https://wiki.archlinux.org/index.php/netctl#Automatic_operation


Logging Operation Details in WCF Service

This post describes a custom server-side logging solution for WCF services. The basic requirements:

  • Log call details such as:
    • Operation name
    • Operation parameters
    • Caller’s identity
    • Client machine address
    • etc.
  • Ability to exclude specific parameters (e.g. password) from logging.
  • Turn on/off logging at the operation level.
  • No code changes in service methods.

The solution takes advantage of WCF’s extensibility points by injecting an IParameterInspector into the operation’s execution pipeline. This is achieved via a custom IServiceBehavior, configured in web.config with the help of a BehaviorExtensionElement.

Binaries are available on nuget at https://www.nuget.org/packages/OperationLogger.Wcf and the source code is at https://github.com/gilyes/OperationLogger.Wcf.

Usage

After adding a reference to the OperationLogger.Wcf.dll (e.g. via nuget), logging can be enabled with one simple code change and a bit of configuration.

Wiring up logging function

On service initialization (for example in custom ServiceHost’s OnOpening method) setup the OperationLogBehavior.LogAction static function, this will be called with OperationDetails whenever an operation is about to execute:

OperationLogBehavior.LogAction = operationDetails => { /*TODO: log to db/file/whatever*/ }

Configuration

First enable the use of the service behavior by defining the behavior extension element:

<system.serviceModel>
    <extensions>
        <behaviorExtensions>
            <add name="operationLog" 
                 type="OperationLogger.Wcf.OperationLogBehaviorElement, OperationLogger.Wcf" />
        </behaviorExtensions>
        ...

Then add this behavior to the service’s behavior definition:

<behaviors>
    <serviceBehaviors>
        <behavior name="MyServiceBehavior">
            <operationLog>
                <patterns>
                    <add actionPattern=".*" />
                </patterns>
            </operationLog>
            ...

The above configuration example enables logging for all actions. To restrict logging to specific services/actions, add as many pattern entries as needed with the actionPattern set to a regular expression that needs to be matched for logging to be applied. For example:

<operationLog>
    <patterns>
        <add actionPattern=".*IPatientService/FindPatients" />
        <add actionPattern=".*IPatientService/GetPatient" />
        <add actionPattern=".*IExamService/.*" />
    </patterns>
</operationLog>

Parameter logging can be controlled similarly, using the parameterPattern attribute. By default it is turned on for all parameters, but it can be turned off altogether:

<operationLog>
    <patterns>
        <add actionPattern=".*IAuthenticationService/Authenticate" 
             parameterPattern="^$" />
    </patterns>
</operationLog>

Or it could be disabled for specific parameters:

<operationLog>
    <patterns>
        <add actionPattern=".*IAuthenticationService/Authenticate"
             parameterPattern="^((?!password).)*$" />
    </patterns>
</operationLog>

Error handling

Exceptions during logging are swallowed, but the service can get notified of exceptions by setting up the OperationLogBehavior.OnError static function:

OperationLogBehavior.OnError = (e, message) => { /*handle error*/ };

Implementation

Parameter inspector

ParameterInspector implements IParameterInspector. The BeforeCall method is executed before every operation, so that is where we extract operation details from current OperationContext, ServiceSecurityContext, operation inputs, etc then call the log action.

public object BeforeCall(string operationName, object[] inputs)
{
    if (LogAction == null)
    {
        return null;
    }

    try
    {
        var operationContext = OperationContext.Current;
        var securityContext = ServiceSecurityContext.Current;
        var remoteEndpoint = OperationContext.Current.IncomingMessageProperties[RemoteEndpointMessageProperty.Name] 
                                as RemoteEndpointMessageProperty;

        var operationData = 
            new OperationDetails
            {
                OperationName = operationName,
                Action = operationContext.IncomingMessageHeaders.Action,
                ServiceUri = operationContext.Channel.LocalAddress.Uri,
                ClientAddress = remoteEndpoint != null ? remoteEndpoint.Address : "",
                UserName = securityContext != null ? securityContext.PrimaryIdentity.Name : "",
                Identity = securityContext != null ? securityContext.PrimaryIdentity : null,
                IsAnonymous = securityContext == null || securityContext.IsAnonymous
            };

        if (inputs != null)
        {
            // get parameter names
            var parameterInfos = _operationDescription.SyncMethod.GetParameters();

            // add enabled parameters
            int i = 0;
            foreach (var parameterInfo in parameterInfos.Where(x => IsParameterLoggingEnabled(x.Name)))
            {
                operationData.Parameters.Add(parameterInfo.Name, i < inputs.Length ? inputs[i] : null);
                i++;
            }
        }

        LogAction(operationData);
    }
    catch (Exception e)
    {
        // logging failed, try notifying the service
        var onError = OnError;
        if (onError != null)
        {
            onError(e, string.Format("Operation logging failed for '{0}'", operationName));
        }
    }

    return null;
}

Service behavior

OperationLogBehavior implements IServiceBehavior and adds a parameter inspector for all operations that are enabled in the configuration.

public class OperationLogBehavior : IServiceBehavior
{
    public OperationLogBehavior()
    {
        IsEnabledForOperation = operation => true;
        IsParameterLoggingEnabled = (operation, parameterName) => true;
    }

    public static Action<OperationDetails> LogAction { get; set; }
    public static Action<Exception, string> OnError { get; set; }
    public Func<DispatchOperation, bool> IsEnabledForOperation { get; set; }
    public Func<DispatchOperation, string, bool> IsParameterLoggingEnabled { get; set; }

    void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription,
                                               ServiceHostBase serviceHostBase,
                                               Collection<ServiceEndpoint> endpoints,
                                               BindingParameterCollection bindingParameters)
    {
    }

    void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription,
                                                ServiceHostBase serviceHostBase)
    {
        // add parameter inspector for all enabled operations in the service
        foreach (var channelDispatcher in serviceHostBase.ChannelDispatchers.OfType<ChannelDispatcher>())
        {
            foreach (var endpointDispatcher in channelDispatcher.Endpoints)
            {
                var endpoint = serviceHostBase.Description.Endpoints.Find(endpointDispatcher.EndpointAddress.Uri);
                foreach (var dispatchOperation in endpointDispatcher.DispatchRuntime.Operations.Where(IsEnabledForOperation))
                {
                    var operationDescription = endpoint != null
                                                   ? GetOperationDescription(endpoint, dispatchOperation)
                                                   : null;
                    AddParameterInspector(dispatchOperation, operationDescription);
                }
            }
        }
    }

    void IServiceBehavior.Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {
    }

    private static OperationDescription GetOperationDescription(ServiceEndpoint endpoint,
                                                                DispatchOperation dispatchOperation)
    {
        return endpoint.Contract.Operations.Find(dispatchOperation.Name);
    }

    private void AddParameterInspector(DispatchOperation dispatchOperation,
                                       OperationDescription operationDescription)
    {
        var parameterInspector = new ParameterInspector(operationDescription)
                                 {
                                     LogAction = LogAction,
                                     OnError = OnError,
                                     IsParameterLoggingEnabled = parameterName => IsParameterLoggingEnabled(dispatchOperation, parameterName)
                                 };
        dispatchOperation.ParameterInspectors.Add(parameterInspector);
    }
}

Behavior extension element

OperationLogBehaviorElement implements BehaviorExtensionElement and it allows OperationLogBehavior to be configured and associated with services in web.config.

public class OperationLogBehaviorElement : BehaviorExtensionElement
{
    private const string PatternsPropertyName = "patterns";

    public override Type BehaviorType
    {
        get { return typeof (OperationLogBehavior); }
    }

    // Define the <patterns> section to be used in the configuration.
    [ConfigurationProperty(PatternsPropertyName)]
    [ConfigurationCollection(typeof (OperationLogPatternCollection),
        AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")]
    public OperationLogPatternCollection Patterns
    {
        get { return (OperationLogPatternCollection) base[PatternsPropertyName]; }
        set { base[PatternsPropertyName] = value; }
    }

    protected override object CreateBehavior()
    {
        return new OperationLogBehavior
        {
            IsEnabledForOperation = IsEnabledForOperation,
            IsParameterLoggingEnabled = IsParameterLoggingEnabled
        };
    }
    ...
}

OperationLogPatternCollection defines the pattern collection in the configuration by implementing a ConfigurationElementCollection:

public class OperationLogPatternCollection : ConfigurationElementCollection
{
    public override ConfigurationElementCollectionType CollectionType
    {
        get { return ConfigurationElementCollectionType.AddRemoveClearMap; }
    }

    public OperationLogPatternElement this[int index]
    {
        get { return (OperationLogPatternElement) BaseGet(index); }
        set
        {
            if (BaseGet(index) != null)
            {
                BaseRemoveAt(index);
            }
            BaseAdd(index, value);
        }
    }

    public new OperationLogPatternElement this[string Name]
    {
        get { return (OperationLogPatternElement) BaseGet(Name); }
    }

    protected override ConfigurationElement CreateNewElement()
    {
        return new OperationLogPatternElement();
    }

    protected override object GetElementKey(ConfigurationElement element)
    {
        return ((OperationLogPatternElement) element).ActionPattern;
    }

    public int IndexOf(OperationLogPatternElement patternElement)
    {
        return BaseIndexOf(patternElement);
    }

    public void Add(OperationLogPatternElement patternElement)
    {
        BaseAdd(patternElement);
    }

    protected override void BaseAdd(ConfigurationElement element)
    {
        BaseAdd(element, false);
    }

    public void Remove(OperationLogPatternElement patternElement)
    {
        if (BaseIndexOf(patternElement) >= 0)
        {
            BaseRemove(patternElement.ActionPattern);
        }
    }

    public void RemoveAt(int index)
    {
        BaseRemoveAt(index);
    }

    public void Remove(string name)
    {
        BaseRemove(name);
    }

    public void Clear()
    {
        BaseClear();
    }
}

And finally, OperationLogPatternElement is a ConfigurationElement and it defines the actionPattern and parameterPattern item attributes:

public class OperationLogPatternElement : ConfigurationElement
{
    private const string ActionPropertyName = "actionPattern";
    private const string ParameterPatternPropertyName = "parameterPattern";

    [ConfigurationProperty(ActionPropertyName, DefaultValue = null, IsRequired = true, IsKey = false)]
    public string ActionPattern
    {
        get { return (string) this[ActionPropertyName]; }
        set { this[ActionPropertyName] = value; }
    }

    [ConfigurationProperty(ParameterPatternPropertyName, DefaultValue = ".*", IsRequired = false, IsKey = false)]
    public string ParameterPattern
    {
        get { return (string) this[ParameterPatternPropertyName]; }
        set { this[ParameterPatternPropertyName] = value; }
    }

Raspberry Pi Shutdown/Reset/Start Button

Shutting down a Raspberry Pi by cutting the power while it is still running is not recommended and it can lead to data corruption. The Raspberry Pi does not have a built-in shutdown/reset button, but thankfully it is fairly simple to wire one up.

Wiring up the pushbutton

Note: info below applies to B+ and probably rev 2 A/B boards.

Take a pushbutton and simply wire it up as seen in the diagram below, one leg going via the red wire to pin 5 (GPIO3) and the other side going to ground at pin 6 (GND). A pull-up resistor is also needed on the wire connected to pin 5 (see e.g. here for reason) but the same can be achieved by just enabling the built-in pull-up resistor in code (see below).

Wiring the pushbutton

Pins other than number 5 could also be used, however a side benefit of using it is that when the Pi is powered off, shorting pin 5 with ground causes the Raspberry Pi to power on. So the same button can be used for shutdown/reboot and also for power-on.

Code

The code to control the button is fairly simple. First set up the GPIO pin:

GPIO.setmode(GPIO.BOARD)
GPIO.setup(5, GPIO.IN, pull_up_down=GPIO.PUD_UP)

Then, subscribe to button up/down events:

GPIO.add_event_detect(5, GPIO.BOTH, callback=buttonStateChanged)

Finally, in the event handler act on the button press:

def buttonStateChanged(pin):
    global buttonPressedTime

    if not (GPIO.input(pin)):
        # button is down
        buttonPressedTime = datetime.now()
    else:
        # button is up
        if buttonPressedTime is not None:
            if (datetime.now() - buttonPressedTime).total_seconds() >= shutdownMinSeconds:
                # button pressed for more than specified time, shutdown
                call(['shutdown', '-h', 'now'], shell=False)
            else:
                # button pressed for a shorter time, reboot
                call(['shutdown', '-r', 'now'], shell=False)

Using this code, if the button is pressed for more than a few seconds then the Raspberry Pi shuts down, otherwise it reboots.

The full script is available at https://github.com/gilyes/pi-shutdown

Run the script

sudo python pishutdown.py

Autostart the script

If you’re using systemd then create a file called pishutdown.service in /etc/systemd/system/ (replace path\_to\_pishutdown with appropriate path):

[Service]
ExecStart=/usr/bin/python /path_to_pishutdown/pishutdown.py
WorkingDirectory=/path_to_pishutdown/
Restart=always
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=pishutdown
User=root
Group=root

[Install]
WantedBy=multi-user.target

Enable service:

sudo systemctl enable pishutdown.service

Run service (will be automatically started on next reboot):

sudo systemctl start pishutdown.service

Building a Touch Squeezebox Using Raspberry Pi

This post gives a brief overview of the process of building a touchscreen Squeezebox replacement. There may be a future post on building an enclosure for the player.

The system will run on a Raspberry Pi, using SqueezePlug, a customized version of Raspbian specifically tailored for Squeezebox player/server installations. For the player it will use Squeezelite (included with SqueezePlug) and for the UI JiveLite. Depending on the touchscreen the kernel needs to be rebuilt to include touch drivers (there is a link at the bottom of this post where this is described in detail).

Setup SqueezePlug

Download latest version from squeezeplug.eu.

Extract the .img file from the downloaded .zip file then using an imager such as Win32DiskImager write it to your SD card.

Boot up the Raspberry Pi and configure the system (setup networking, configure squeezelite, etc, might expand on this later…).

Install JiveLite

Install required libraries:

sudo apt-get install git libsdl1.2-dev libsdl-ttf2.0-dev libsdl-image1.2-dev libsdl-gfx1.2-dev libexpat1-dev

Get and build luajit:

git clone http://luajit.org/git/luajit-2.0.git
cd luajit-2.0
make
sudo make install
sudo ldconfig
cd..

Get and build JiveLite:

git clone https://code.google.com/p/jivelite/
cd jivelite
make PREFIX=/usr

To run JiveLite:

/path_to_jivelite_source/bin/jivelite

Setup auto-login

In /etc/inittab comment out the following line:

1:2345:respawn:/sbin/getty 115200 tty1

and add in the following line instead (replace your_username with appropriate user name, for SqueezePlug this is root by default):

1:2345:respawn:/bin/login -f your_username tty1 </dev/tty1 >/dev/tty1 2>&1

Auto-start X and JiveLite

In /etc/rc.local, above the exit 0 line, add the following:

su -s /bin/bash -c startx your_user&

If not running as root then edit /etc/X11/Xwrapper.config and change allowed_users=console to allowed_users=anybody to allow non-interactive processes to launch an X server (more info here)

Edit ~/.xinitrc and add the following (replace path_to_jivelite_bin with appropriate path):

/path_to_jivelite_bin/jivelite

Alternatively, if for some reason you want to boot into the desktop and then launch JiveLite then you may do this:

In /etc/rc.local, above the exit 0 line, add the following:

su -l root -c startx

Then create a jivelite.destop file in ~/.config/autostart/ (see e.g. here for details).

Hide mouse cursor

JiveLite seems to hide the mouse cursor automatically depending on how it is launched, but if it is started via the first method above then it does not hide it.

So, if needed, you may add the -nocursor option in /etc/X11/xinit/xserverrc (this will fully disable mouse cursor), or alternatively, sudo apt-get install unclutter and put the following in .xinitrc (above the jivelite command): unclutter -idle 1 -grab -noevents -display :0& (this will hide mouse cursor if idle for 1 second, lower numbers seem not to work).

Calibrating touchscreen

Andrew Fraser has written a great guide for eGalax devices. It involves rebuilding the kernel to include the touch drivers. Worked great for my Sainsmart 7” 800x480 panel.