After a long stint of upgrading domain controllers and fixing print servers, I've finally had the chance to do something remotely interesting. I discovered a server running WAMP for a Piwigo install, which I quickly decided was a great chance to try and "containerize" an existing service. While I didn't quite manage running Piwigo in a Windows Nano container in production*, I did manage to get it up and running on a Nano server in Azure.

*MySql ran fine using volume mapping for the database, however some part of the web stack (Apache/PHP or Piwigo itself) didn't like volume mapping for the actual images. I'll do a separate post about this later

The bits#

Start with some Nano quirks

PHP on windows uses the Visual Studio C++ Redistributable packages. Despite the fact that Nano server doesn't support them, at the time of writing this, the installer is still only available as an MSI. So in order to get the Dll's onto my Nano server, I first installed the MSI on another blank 2016 full server, then grabbed the DLL's with some powershell.

$outDir = "c:\vs2015redis"
$files = "concrt140.dll", "msvcp140.dll", "vcamp140.dll", "vccorlib140.dll", "vcomp140.dll", "vcruntime140.dll", "mfc140.dll", "mfc140chs.dll", "mfc140cht.dll", "mfc140deu.dll", "mfc140enu.dll", "mfc140esn.dll", "mfc140fra.dll", "mfc140ita.dll", "mfc140jpn.dll", "mfc140kor.dll", "mfc140rus.dll", "mfc140u.dll", "mfcm140.dll", "mfcm140u.dll"

foreach ($f in $files)
{
  Copy-Item -Path $env:windir\system32\$f -Destination $outDir
}

Get a Nano Server

If you have access to Azure, and haven't tried Nano server yet, I highly recommend Thomas Maurer's blog post to get you started.

Alternatively if you are using Hyper-V, you can use the Nano Server Image Builder to generate a VHD/X with a GUI, or use the PowerShell commands directly. More guidance can be found on technet. If you don't already have a Nano server to hand, go set one up and come back once you can successfully connect to it over PowerShell remoting.

Copy the Dlls

Next, you need to copy the VS2015 C++ DLLs extracted earlier, to C:\Windows\System32 folder. You can either c$ share onto the nano server if it's been enabled, or just copy the files over PSRemoting. For all of these examples, I will demonstrate with PowerShell remoting.

$session = new-pssession -computername NanoServerName -Credential (get-credential)
Copy-Item -Path c:\vs2015redis -Destination C:\Windows\System32\ -ToSession $session -Include *.dll

Get Apache

Download Apache and copy it into the nano server, then extract the zip and remove it.

Invoke-WebRequest -Method Get -Uri https://www.apachelounge.com/download/VC14/binaries/httpd-2.4.25-win64-VC14.zip -OutFile c:\apache.zip ; 
Copy-Item -Path c:\apache.zip -Destination C:\ -ToSession $session;
Invoke-Command -Session $session -Command {Expand-Archive -Path c:\apache.zip -DestinationPath c:\} ; 
Invoke-Command -Session $session -Command {Remove-Item c:\apache.zip -Force}

Then you need to replace the httpd.conf file with one that has appropriate settings. below is my example file. All comments and unnecessary lines have been removed for brevity.

ServerRoot "C:/Apache24"
Listen 80
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule actions_module modules/mod_actions.so
LoadModule alias_module modules/mod_alias.so
LoadModule allowmethods_module modules/mod_allowmethods.so
LoadModule asis_module modules/mod_asis.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule autoindex_module modules/mod_autoindex.so
LoadModule cgi_module modules/mod_cgi.so
LoadModule dir_module modules/mod_dir.so
LoadModule env_module modules/mod_env.so
LoadModule include_module modules/mod_include.so
LoadModule isapi_module modules/mod_isapi.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule mime_module modules/mod_mime.so
LoadModule negotiation_module modules/mod_negotiation.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule php7_module "C:/php/php7apache2_4.dll"
<IfModule unixd_module>
User daemon
Group daemon
</IfModule>
ServerAdmin admin@example.com
<Directory />
    AllowOverride none
    Require all denied
</Directory>
DocumentRoot "C:/Apache24/htdocs"
<Directory "C:/Apache24/htdocs">
    Options Indexes FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>
<IfModule dir_module>
    DirectoryIndex index.php index.html
</IfModule>
<Files ".ht*">
    Require all denied
</Files>
ErrorLog "logs/error.log"
LogLevel debug
<IfModule log_config_module>
    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
    LogFormat "%h %l %u %t \"%r\" %>s %b" common
    <IfModule logio_module>
      LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
    </IfModule>
    CustomLog "logs/access.log" common
</IfModule>
<IfModule alias_module>
    ScriptAlias /cgi-bin/ "C:/Apache24/cgi-bin/"
</IfModule>
<IfModule cgid_module>
</IfModule>
<Directory "C:/Apache24/cgi-bin">
    AllowOverride None
    Options None
    Require all granted
</Directory>
<IfModule mime_module>
    TypesConfig conf/mime.types
    AddType application/x-compress .Z
    AddType application/x-gzip .gz .tgz
</IfModule>
<IfModule proxy_html_module>
Include conf/extra/proxy-html.conf
</IfModule>
<IfModule ssl_module>
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
</IfModule>
<IfModule php7_module>
    PHPIniDir "C:/php"
    AddType application/x-httpd-php .php
</IfModule>

Save this as C:\httpd.conf and then copy it over the default one.

Copy-Item -Path c:\httpd.conf -Destination C:\Apache24\conf -ToSession $session -Force;

Get PHP

Then simply repeat the process for PHP. Be ware that windows.php.net moves downloads from release to archive when they are no longer the latest release, so the Uri may need to be changed.

Invoke-WebRequest -Method Get -Uri http://windows.php.net/downloads/releases/php-7.1.1-Win32-VC14-x64.zip -OutFile c:\php.zip ; 
Copy-Item -Path c:\php.zip -Destination C:\ -ToSession $session;
Invoke-Command -Session $session -Command {Expand-Archive -Path c:\php.zip -DestinationPath c:\} ; 
Invoke-Command -Session $session -Command {Remove-Item c:\php.zip -Force}

Here is the example php.ini, All comments and unnecessary lines have been removed for brevity.

[PHP]
engine = On
short_open_tag = Off
asp_tags = Off
precision = 14
output_buffering = 4096
zlib.output_compression = Off
implicit_flush = Off
unserialize_callback_func =
serialize_precision = 17
disable_functions =
disable_classes =
zend.enable_gc = On
expose_php = On
max_execution_time = 30
max_input_time = 60
memory_limit = 256M
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
display_errors = Off
display_startup_errors = Off
log_errors = On
log_errors_max_len = 1024
ignore_repeated_errors = Off
ignore_repeated_source = Off
report_memleaks = On
track_errors = Off
html_errors = On
variables_order = "GPCS"
request_order = "GP"
register_argc_argv = Off
auto_globals_jit = On
post_max_size = 8M
auto_prepend_file =
auto_append_file =
default_mimetype = "text/html"
include_path = ".
doc_root =
user_dir =
enable_dl = Off
file_uploads = On
upload_max_filesize = 2M
max_file_uploads = 20
allow_url_fopen = On
allow_url_include = Off
default_socket_timeout = 60
extension=c:/php/ext/php_gd2.dll
extension=c:/php/ext/php_mbstring.dll
extension=c:/php/ext/php_exif.dll      
extension=c:/php/ext/php_mysqli.dll
[CLI Server]
cli_server.color = On
[Date]
date.timezone = Europe/London
[filter]
[iconv]
[intl]
[sqlite]
[sqlite3]
[Pcre]
[Pdo]
[Pdo_mysql]
pdo_mysql.cache_size = 2000
pdo_mysql.default_socket=
[Phar]
[mail function]
SMTP = localhost
smtp_port = 25
mail.add_x_header = On
[SQL]
sql.safe_mode = Off
[ODBC]
odbc.allow_persistent = On
odbc.check_persistent = On
odbc.max_persistent = -1
odbc.max_links = -1
odbc.defaultlrl = 4096
odbc.defaultbinmode = 1
[Interbase]
ibase.allow_persistent = 1
ibase.max_persistent = -1
ibase.max_links = -1
ibase.timestampformat = "%Y-%m-%d %H:%M:%S"
ibase.dateformat = "%Y-%m-%d"
ibase.timeformat = "%H:%M:%S"
[MySQL]
mysql.allow_local_infile = On
mysql.allow_persistent = On
mysql.cache_size = 2000
mysql.max_persistent = -1
mysql.max_links = -1
mysql.default_port =
mysql.default_socket =
mysql.default_host =
mysql.default_user =
mysql.default_password =
mysql.connect_timeout = 60
mysql.trace_mode = Off
[MySQLi]
mysqli.max_persistent = -1
mysqli.allow_persistent = On
mysqli.max_links = -1
mysqli.cache_size = 2000
mysqli.default_port = 3306
mysqli.default_socket =
mysqli.default_host =
mysqli.default_user =
mysqli.default_pw =
mysqli.reconnect = Off
[mysqlnd]
mysqlnd.collect_statistics = On
mysqlnd.collect_memory_statistics = Off
[OCI8]
[PostgreSQL]
pgsql.allow_persistent = On
pgsql.auto_reset_persistent = Off
pgsql.max_persistent = -1
pgsql.max_links = -1
pgsql.ignore_notice = 0
pgsql.log_notice = 0
[Sybase-CT]
sybct.allow_persistent = On
sybct.max_persistent = -1
sybct.max_links = -1
sybct.min_server_severity = 10
sybct.min_client_severity = 10
[bcmath]
bcmath.scale = 0
[browscap]
[Session]
session.save_handler = files
session.use_strict_mode = 0
session.use_cookies = 1
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly =
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 1440
session.referer_check =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.hash_function = 0
session.hash_bits_per_character = 5
url_rewriter.tags = "a=href,area=href,frame=src,input=src,form=fakeentry"
[MSSQL]
mssql.allow_persistent = On
mssql.max_persistent = -1
mssql.max_links = -1
mssql.min_error_severity = 10
mssql.min_message_severity = 10
mssql.compatibility_mode = Off
mssql.secure_connection = Off
[Assertion]
[COM]
[mbstring]
[gd]
[exif]
[Tidy]
tidy.clean_output = Off
[soap]
soap.wsdl_cache_enabled=1
soap.wsdl_cache_dir="/tmp"
soap.wsdl_cache_ttl=86400
soap.wsdl_cache_limit = 5
[sysvshm]
[ldap]
ldap.max_links = -1
[mcrypt]
[dba]
[opcache]
[curl]

Save this as c:\php.ini and copy it onto the server.

Copy-Item -Path c:\php.ini -Destination C:\php -ToSession $session -Force;

Get MySql

Invoke-WebRequest -Method Get -Uri https://cdn.mysql.com/archives/mysql-5.6/mysql-5.6.29-winx64.zip -Timeoutsec 600 -OutFile c:\mysql.zip ; 
Copy-Item -Path c:\mysql.zip -Destination C:\ -ToSession $session;
Invoke-Command -Session $session -Command {Expand-Archive -Path c:\mysql.zip -DestinationPath c:\} ; 
Invoke-Command -Session $session -Command {Remove-Item c:\mysql.zip -Force};
Invoke-Command -Session $session -Command {Rename-Item -Path C:\mysql-5.6.29-winx64 -NewName MySql -Force -Confirm:$false}

Here is the example my.ini, All comments and unnecessary lines have been removed for brevity.

[client]
port=3306
[mysql]
default-character-set=utf8
[mysqld]
port=3306
datadir=C:/MySQL/data
character-set-server=utf8
default-storage-engine=INNODB
sql-mode="STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
log-output=FILE
general-log=0
general_log_file="container.log"
slow-query-log=1
slow_query_log_file="container-slow.log"
long_query_time=10
log-error="container.err"
server-id=1
max_connections=151
query_cache_size=1M
table_open_cache=2000
tmp_table_size=159M
thread_cache_size=10
myisam_max_sort_file_size=100G
myisam_sort_buffer_size=307M
key_buffer_size=8M
read_buffer_size=64K
read_rnd_buffer_size=256K
sort_buffer_size=256K
innodb_additional_mem_pool_size=22M
innodb_flush_log_at_trx_commit=1
innodb_log_buffer_size=11M
innodb_buffer_pool_size=2G
innodb_log_file_size=48M
innodb_thread_concurrency=8
innodb_autoextend_increment=64
innodb_buffer_pool_instances=8
innodb_concurrency_tickets=5000
innodb_old_blocks_time=1000
innodb_open_files=300
innodb_stats_on_metadata=0
innodb_file_per_table=1
innodb_checksum_algorithm=0
back_log=80
flush_time=0
join_buffer_size=256K
max_allowed_packet=4M
max_connect_errors=100
open_files_limit=4161
query_cache_type=1
sort_buffer_size=256K
table_definition_cache=1400
binlog_row_event_max_size=8K
sync_master_info=10000
sync_relay_log=10000
sync_relay_log_info=10000

Save this as c:\my.ini and copy it onto the server.

Copy-Item -Path c:\my.ini -Destination C:\MySql -ToSession $session -Force;

Get Piwigo

At the time of writing this, there is a bug in the latest version that results in a session_start error that prevents login. The workaround can be found on GitHub. It's probably easiest to simply open the zip file and fix the offending file before transferring it to your Nano Server.

Invoke-WebRequest -Method Get -Uri http://piwigo.org/download/dlcounter.php?code=2.8.5 -OutFile c:\piwigo.zip ; 
Copy-Item -Path c:\piwigo.zip -Destination C:\ -ToSession $session;
Invoke-Command -Session $session -Command {Expand-Archive -Path c:\piwigo.zip -DestinationPath c:\} ; 
Invoke-Command -Session $session -Command {Remove-Item -Path C:\Apache24\htdocs\ -Recurse -Force -Confirm:$false;Rename-Item C:\Apache24\piwigo\ -NewName htdocs;} ; 
Invoke-Command -Session $session -Command {Remove-Item c:\piwigo.zip -Force}

Register and start services

Invoke-Command -Session $session -Command {c:\apache24\bin\httpd.exe -k install} ;

Invoke-Command -Session $session -Command {c:\MySql\bin\mysqld.exe --install} ;

Invoke-Command -Session $session -Command {Get-Service MySql | Restart-Service; Get-Service Apache2.4 | Restart-Service} ;

Open Firewall

Invoke-Command -Session $session -Command {New-NetFirewallRule -Name "Allow_HTTP" -DisplayName "Allow HTTP" -Enabled True -Action Allow -Profile Any -Direction Inbound -LocalPort 80 -Protocol TCP} ;

Success?

Hopefully if I typed everything correctly, you can now browse to the Name/IP of your Nano Server and be presented with the Piwigio setup page.

Further Actions

If you are going to expose the Nano server to the internet, you should defiantly proceed to configure SSL/TLS and properly secure the MySql instance etc. As those steps are not specific to Nano server, they have not been included here.