diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..1cb6d806ea86b5721390e04908cde3c30bf02410
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.idea
+*.iml
+config/*.conf
+config/*.html
+config/*.users
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c7bb40d88a15c96f9869f69f2d5ce9493f86468f
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,49 @@
+image: docker:latest
+
+services:
+  - docker:dind
+
+before_script:
+  - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
+
+#
+# TESTS
+#
+
+lint:dockerfile:
+  stage: test
+  image: hadolint/hadolint:latest-debian
+  before_script:
+    - mkdir ~/.config
+    - cp ./.hadolint.yaml ~/.config/hadolint.yaml
+  script:
+    - hadolint ./context/Dockerfile
+
+lint:shell-script:
+  stage: test
+  image: koalaman/shellcheck-alpine:latest
+  before_script:
+    - shellcheck -V
+  script:
+    - shellcheck ./context/entrypoint
+
+#
+# IMAGE BUILDS/PUSHES
+#
+
+build:master:
+  stage: deploy
+  script:
+    - docker build --pull -t "$CI_REGISTRY_IMAGE:latest" ./context/
+#    - docker push "$CI_REGISTRY_IMAGE:latest"
+  only:
+    - master
+  when: manual
+
+build:dev:
+  stage: deploy
+  script:
+    - docker build --pull -t "$CI_REGISTRY_IMAGE:dev" ./context/
+#    - docker push "$CI_REGISTRY_IMAGE:dev"
+  except:
+    - master
diff --git a/.hadolint.yaml b/.hadolint.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8f7e23e459df52ebbd66d768be22912a79d80eca
--- /dev/null
+++ b/.hadolint.yaml
@@ -0,0 +1,2 @@
+ignored:
+  - DL3008
diff --git a/README.md b/README.md
index 801bae73fd97ba8a894f2492d64a10e2920361dc..44e4d4e0ccdd6565c69704450eb23ee73bda1fe2 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,24 @@
 # nut-upsd
 
+Fully-customizable `nut` instance in a container. Many images exist, but they are
+feature-limited due to how `nut`'s configuration is handled. This image aims to
+provide all `nut` options without making too many compromises.
+
+## Usage
+
+1. Create necessary configurations in `./config`; `.conf`, `.html`, and `.users`
+files are supported. **`./config` includes sample configurations.**
+1. `docker-compose up -d`
+1. If `MODE` is set to `netserver`, `nut` will be available on the container's 
+port `3493`. Confirm using `telnet`:
+    ```bash
+    $ telnet [CONTAINER IP] 3493
+    Trying [CONTAINER IP]...
+    Connected to [CONTAINER IP].
+    Escape character is '^]'.
+    
+    $ LIST UPS
+    BEGIN LIST UPS
+    UPS test "Back-UPS XS 1500 Test"
+    END LIST UPS
+    ```
diff --git a/config/hosts.conf.sample b/config/hosts.conf.sample
new file mode 100644
index 0000000000000000000000000000000000000000..24beb829560e2c82d076284ae9d925fa9e74aef1
--- /dev/null
+++ b/config/hosts.conf.sample
@@ -0,0 +1,29 @@
+# Network UPS Tools: example hosts.conf
+#
+# This file is used to control the CGI programs.  If you have not
+# installed them, you may safely ignore or delete this file.
+#
+# -----------------------------------------------------------------------
+#
+# upsstats will use the list of MONITOR entries when displaying the
+# default template (upsstats.html).  The "FOREACHUPS" directive in the
+# template will use this file to find systems running upsd.
+#
+# upsstats and upsimage also use this file to determine if a host may be
+# monitored.  This keeps evil people from using your system to annoy
+# others with unintended queries.
+#
+# upsset presents a list of systems that may be viewed and controlled
+# using this file.
+#
+# -----------------------------------------------------------------------
+#
+# Usage: list systems running upsd that you want to monitor
+#
+# MONITOR <system> "<host description>"
+#
+# Examples:
+#
+# MONITOR myups@localhost "Local UPS"
+# MONITOR su2200@10.64.1.1 "Finance department"
+# MONITOR matrix@shs-server.example.edu "Sierra High School data room #1"
diff --git a/config/nut.conf.sample b/config/nut.conf.sample
new file mode 100644
index 0000000000000000000000000000000000000000..b0c2685c7d7d1498bf905226292647e7348298b1
--- /dev/null
+++ b/config/nut.conf.sample
@@ -0,0 +1,32 @@
+# Network UPS Tools: example nut.conf
+#
+##############################################################################
+# General section
+##############################################################################
+# The MODE determines which part of the NUT is to be started, and which
+# configuration files must be modified.
+#
+# This file try to standardize the various files being found in the field, like
+# /etc/default/nut on Debian based systems, /etc/sysconfig/ups on RedHat based
+# systems, ... Distribution's init script should source this file to see which
+# component(s) has to be started.
+#
+# The values of MODE can be:
+# - none: NUT is not configured, or use the Integrated Power Management, or use
+#   some external system to startup NUT components. So nothing is to be started.
+# - standalone: This mode address a local only configuration, with 1 UPS
+#   protecting the local system. This implies to start the 3 NUT layers (driver,
+#   upsd and upsmon) and the matching configuration files. This mode can also
+#   address UPS redundancy.
+# - netserver: same as for the standalone configuration, but also need
+#   some more network access controls (firewall, tcp-wrappers) and possibly a
+#   specific LISTEN directive in upsd.conf.
+#   Since this MODE is opened to the network, a special care should be applied
+#   to security concerns.
+# - netclient: this mode only requires upsmon.
+#
+# IMPORTANT NOTE:
+#  This file is intended to be sourced by shell scripts.
+#  You MUST NOT use spaces around the equal sign!
+
+MODE=none
diff --git a/config/ups.conf.sample b/config/ups.conf.sample
new file mode 100644
index 0000000000000000000000000000000000000000..f7de6db5e3158505aaf53c02db68c8b94564cd02
--- /dev/null
+++ b/config/ups.conf.sample
@@ -0,0 +1,141 @@
+# Network UPS Tools: example ups.conf
+#
+# --- SECURITY NOTE ---
+#
+# If you use snmp-ups and set a community string in here, you
+# will have to secure this file to keep other users from obtaining
+# that string.  It needs to be readable by upsdrvctl and any drivers,
+# and by upsd.
+#
+# ---
+#
+# This is where you configure all the UPSes that this system will be
+# monitoring directly.  These are usually attached to serial ports, but
+# USB devices and SNMP devices are also supported.
+#
+# This file is used by upsdrvctl to start and stop your driver(s), and
+# is also used by upsd to determine which drivers to monitor.  The
+# drivers themselves also read this file for configuration directives.
+#
+# The general form is:
+#
+# [upsname]
+#       driver = <drivername>
+#         port = <portname>
+#       < any other directives here >
+#
+# The section header ([upsname]) can be just about anything as long as
+# it is a single word inside brackets.  upsd uses this to uniquely
+# identify a UPS on this system.
+#
+# If you have a UPS called snoopy, your section header would be "[snoopy]".
+# On a system called "doghouse", the line in your upsmon.conf to monitor
+# it would look something like this:
+#
+#   MONITOR snoopy@doghouse 1 upsmonuser mypassword master
+#
+# It might look like this if monitoring in slave mode:
+#
+#   MONITOR snoopy@doghouse 1 upsmonuser mypassword slave
+#
+# Configuration directives
+# ------------------------
+#
+# These directives are used by upsdrvctl only and should be specified outside
+# of a driver definition:
+#
+#    maxretry: Optional.  Specify the number of attempts to start the driver(s),
+#              in case of failure, before giving up. A delay of 'retrydelay' is
+#              inserted between each attempt. Caution should be taken when using
+#              this option, since it can impact the time taken by your system to
+#              start.
+#
+#              The built-in default is 1 attempt.
+#
+#  retrydelay: Optional.  Specify the delay between each restart attempt of the
+#              driver(s), as specified by 'maxretry'. Caution should be taken
+#              when using this option, since it can impact the time taken by your
+#              system to start.
+#
+#              The default is 5 seconds.
+#
+
+# Set maxretry to 3 by default, this should mitigate race with slow devices:
+maxretry = 3
+
+# These directives are common to all drivers that support ups.conf:
+#
+#  driver: REQUIRED.  Specify the program to run to talk to this UPS.
+#          apcsmart, bestups, and sec are some examples.
+#
+#    port: REQUIRED.  The serial port where your UPS is connected.
+#          /dev/ttyS0 is usually the first port on Linux boxes, for example.
+#
+# sdorder: optional.  When you have multiple UPSes on your system, you
+#          usually need to turn them off in a certain order.  upsdrvctl
+#          shuts down all the 0s, then the 1s, 2s, and so on.  To exclude
+#          a UPS from the shutdown sequence, set this to -1.
+#
+#          The default value for this parameter is 0.
+#
+#  nolock: optional, and not recommended for use in this file.
+#
+#          If you put nolock in here, the driver will not lock the
+#          serial port every time it starts.  This may allow other
+#          processes to seize the port if you start more than one by
+#          mistake.
+#
+#          This is only intended to be used on systems where locking
+#          absolutely must be disabled for the software to work.
+#
+# maxstartdelay: optional.  This can be set as a global variable
+#                above your first UPS definition and it can also be
+#                set in a UPS section.  This value controls how long
+#                upsdrvctl will wait for the driver to finish starting.
+#                This keeps your system from getting stuck due to a
+#                broken driver or UPS.
+#
+#                The default is 45 seconds.
+#
+# synchronous: optional.  The driver work by default in asynchronous
+#              mode (i.e *synchronous=no*).  This means that all data
+#              are pushed by the driver on the communication socket to
+#              upsd (Unix socket on Unix, Named pipe on Windows) without
+#              waiting for these data to be actually consumed.  With
+#              some HW, such as ePDUs, that can produce a lot of data,
+#              asynchronous mode may cause some congestion, resulting in
+#              the socket to be full, and the driver to appear as not
+#              connected.  By enabling the 'synchronous' flag
+#              (value = 'yes'), the driver will wait for data to be
+#              consumed by upsd, prior to publishing more.  This can be
+#              enabled either globally or per driver.
+#
+#              The default is 'no' (i.e. asynchronous mode) for backward
+#              compatibility of the driver behavior.
+#
+# Anything else is passed through to the hardware-specific part of
+# the driver.
+#
+# Examples
+# --------
+#
+# A simple example for a UPS called "powerpal" that uses the blazer_ser
+# driver on /dev/ttyS0 is:
+#
+# [powerpal]
+#   driver = blazer_ser
+#   port = /dev/ttyS0
+#   desc = "Web server"
+#
+# If your UPS driver requires additional settings, you can specify them
+# here.  For example, if it supports a setting of "1234" for the
+# variable "cable", it would look like this:
+#
+# [myups]
+#   driver = mydriver
+#   port = /dev/ttyS1
+#   cable = 1234
+#   desc = "Something descriptive"
+#
+# To find out if your driver supports any extra settings, start it with
+# the -h option and/or read the driver's documentation.
diff --git a/config/upsd.conf.sample b/config/upsd.conf.sample
new file mode 100644
index 0000000000000000000000000000000000000000..5cb94062ce5f090f5dd368f5de8e6f84e93b07ed
--- /dev/null
+++ b/config/upsd.conf.sample
@@ -0,0 +1,121 @@
+# Network UPS Tools: example upsd configuration file
+#
+# This file contains access control data, you should keep it secure.
+#
+# It should only be readable by the user that upsd becomes.  See the FAQ.
+#
+# Each entry below provides usage and default value.
+#
+# For more information, refer to upsd.conf manual page.
+
+# =======================================================================
+# MAXAGE <seconds>
+# MAXAGE 15
+#
+# This defaults to 15 seconds.  After a UPS driver has stopped updating
+# the data for this many seconds, upsd marks it stale and stops making
+# that information available to clients.  After all, the only thing worse
+# than no data is bad data.
+#
+# You should only use this if your driver has difficulties keeping
+# the data fresh within the normal 15 second interval.  Watch the syslog
+# for notifications from upsd about staleness.
+
+# =======================================================================
+# TRACKINGDELAY <seconds>
+# TRACKINGDELAY 3600
+#
+# This defaults to 1 hour. When instant commands and variables setting status
+# tracking is enabled, status execution information are kept during this
+# amount of time, and then cleaned up.
+
+# =======================================================================
+# STATEPATH <path>
+# STATEPATH /var/run/nut
+#
+# Tell upsd to look for the driver state sockets in 'path' rather
+# than the default that was compiled into the program.
+
+# =======================================================================
+# LISTEN <IP address or name> [<port>]
+# LISTEN 127.0.0.1 3493
+# LISTEN ::1 3493
+# LISTEN myhostname 83493
+# LISTEN myhostname.mydomain
+#
+# This defaults to the localhost listening addresses and port 3493.
+# In case of IP v4 or v6 disabled kernel, only the available one will be used.
+#
+# You may specify each interface IP address or name that you want upsd to
+# listen on for connections, optionally with a port number.
+#
+# You may need this if you have multiple interfaces on your machine and
+# you don't want upsd to listen to all interfaces (for instance on a
+# firewall, you may not want to listen to the external interface).
+#
+# This will only be read at startup of upsd.  If you make changes here,
+# you'll need to restart upsd, reload will have no effect.
+
+# =======================================================================
+# MAXCONN <connections>
+# MAXCONN 1024
+#
+# This defaults to maximum number allowed on your system.  Each UPS, each
+# LISTEN address and each client count as one connection.  If the server
+# runs out of connections, it will no longer accept new incoming client
+# connections.  Only set this if you know exactly what you're doing.
+
+# =======================================================================
+# CERTFILE <certificate file>
+# CERTFILE /usr/local/ups/etc/upsd.pem
+#
+# When compiled with SSL support with OpenSSL backend,
+# you can enter the certificate file here.
+# The certificates must be in PEM format and must be sorted starting with
+# the subject's certificate (server certificate), followed by intermediate
+# CA certificates (if applicable_ and the highest level (root) CA. It should
+# end with the server key. See 'docs/security.txt' or the Security chapter of
+# NUT user manual for more information on the SSL support in NUT.
+#
+# See 'docs/security.txt' or the Security chapter of NUT user manual
+# for more information on the SSL support in NUT.
+
+# =======================================================================
+# CERTPATH <certificate file or directory>
+# CERTPATH /usr/local/ups/etc/cert/upsd
+#
+# When compiled with SSL support with NSS backend,
+# you can enter the certificate path here.
+# Certificates are stored in a dedicated database (split into 3 files).
+# Specify the path of the database directory.
+#
+# See 'docs/security.txt' or the Security chapter of NUT user manual
+# for more information on the SSL support in NUT.
+
+# =======================================================================
+# CERTIDENT <certificate name> <database password>
+# CERTIDENT "my nut server" "MyPasSw0rD"
+#
+# When compiled with SSL support with NSS backend,
+# you can specify the certificate name to retrieve from database to
+# authenticate itself and the password
+# required to access certificate related private key.
+#
+# See 'docs/security.txt' or the Security chapter of NUT user manual
+# for more information on the SSL support in NUT.
+
+# =======================================================================
+# CERTREQUEST <certificate request level>
+# CERTREQUEST REQUIRE
+#
+# When compiled with SSL support with NSS backend and client certificate
+# validation (disabled by default, see 'docs/security.txt'),
+# you can specify if upsd requests or requires client's' certificates.
+# Possible values are :
+#  - 0 to not request to clients to provide any certificate
+#  - 1 to require to all clients a certificate
+#  - 2 to require to all clients a valid certificate
+#
+# See 'docs/security.txt' or the Security chapter of NUT user manual
+# for more information on the SSL support in NUT.
+
diff --git a/config/upsd.users.sample b/config/upsd.users.sample
new file mode 100644
index 0000000000000000000000000000000000000000..15a0206a32a8fe8a0801a401ac31f2d4919c6deb
--- /dev/null
+++ b/config/upsd.users.sample
@@ -0,0 +1,75 @@
+# Network UPS Tools: Example upsd.users
+#
+# This file sets the permissions for upsd - the UPS network daemon.
+# Users are defined here, are given passwords, and their privileges are
+# controlled here too.  Since this file will contain passwords, keep it
+# secure, with only enough permissions for upsd to read it.
+
+# --------------------------------------------------------------------------
+
+# Each user gets a section.  To start a section, put the username in
+# brackets on a line by itself.  To set something for that user, specify
+# it under that section heading.  The username is case-sensitive, so
+# admin and AdMiN are two different users.
+#
+# Possible settings:
+#
+# password: The user's password.  This is case-sensitive.
+#
+# --------------------------------------------------------------------------
+#
+# actions: Let the user do certain things with upsd.
+#
+# Valid actions are:
+#
+# SET   - change the value of certain variables in the UPS
+# FSD   - set the "forced shutdown" flag in the UPS
+#
+# --------------------------------------------------------------------------
+#
+# instcmds: Let the user initiate specific instant commands.  Use "ALL"
+# to grant all commands automatically.  There are many possible
+# commands, so use 'upscmd -l' to see what your hardware supports.  Here
+# are a few examples:
+#
+# test.panel.start      - Start a front panel test
+# test.battery.start    - Start battery test
+# test.battery.stop     - Stop battery test
+# calibrate.start       - Start calibration
+# calibrate.stop        - Stop calibration
+#
+# --------------------------------------------------------------------------
+#
+# Example:
+#
+#	[admin]
+#		password = mypass
+#		actions = SET
+#		instcmds = ALL
+#
+
+#
+# --- Configuring for a user who can execute tests only
+#
+#	[testuser]
+#		password  = pass
+#		instcmds  = test.battery.start
+#		instcmds  = test.battery.stop
+
+#
+# --- Configuring for upsmon
+#
+# To add a user for your upsmon, use this example:
+#
+#   [upsmon]
+#       password  = pass
+#       upsmon master
+# or
+#       upsmon slave
+#
+# The matching MONITOR line in your upsmon.conf would look like this:
+#
+# MONITOR myups@localhost 1 upsmon pass master	(or slave)
+#
+# See comments in the upsmon.conf(.sample) file for details about this
+# keyword and the difference of NUT slave and master systems.
diff --git a/config/upsset.conf.sample b/config/upsset.conf.sample
new file mode 100644
index 0000000000000000000000000000000000000000..921a20aa528d463fa54fc49468bf194b12fa8a5c
--- /dev/null
+++ b/config/upsset.conf.sample
@@ -0,0 +1,36 @@
+# Network UPS Tools - upsset.conf sample file
+#
+# This file is provided to ensure that you do not expose your upsd server
+# to the world upon installing the CGI programs.  Specifically, it keeps
+# the upsset.cgi program from running until you have assured it that you
+# have secured your web server's CGI directory.
+#
+# By default, your web server will probably let anyone access upsset.cgi
+# once it is installed.  This means that anyone could attempt to crack
+# upsd logins since they would appear to be coming from your web server,
+# rather than the outside world, slipping through any ACL/ACCESS definitions.
+#
+# For this reason, you *MUST* first secure your CGI programs before
+# enabling upsset in this configuration file.  If you can't do this in
+# your web server, then you should *not* run this program.
+#
+# For Apache, the .htaccess file can be used in the directory with the
+# programs.  You'll need something like this:
+#
+#   <Files upsset.cgi>
+#       deny from all
+#       allow from your.network.addresses
+#   </Files>
+#
+# You will probably have to set "AllowOverride Limit" for this directory in
+# your server-level configuration file as well.
+#
+# If this doesn't make sense, then stop reading and leave this program alone.
+#
+# Assuming you have all this done (and it works), then you may uncomment
+# the line below and start using upsset.cgi through your web browser.
+#
+
+###
+### I_HAVE_SECURED_MY_CGI_DIRECTORY
+###
diff --git a/context/Dockerfile b/context/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..221b3cc503e9e0174ecc38239e0dc3d7459e4635
--- /dev/null
+++ b/context/Dockerfile
@@ -0,0 +1,18 @@
+FROM debian:buster-slim
+
+LABEL maintainer="ethitter"
+LABEL version="1.0"
+
+RUN echo "deb http://security.debian.org/ buster/updates main" >> /etc/apt/sources.list
+
+RUN apt-get update \
+    && apt-get -y --no-install-recommends install \
+        nut \
+    && apt-get autoremove -y \
+    && apt-get clean \
+    && rm -rf /var/lib/apt/lists/*
+
+EXPOSE 3493
+
+COPY entrypoint /usr/local/bin/
+ENTRYPOINT ["entrypoint"]
diff --git a/context/entrypoint b/context/entrypoint
new file mode 100755
index 0000000000000000000000000000000000000000..e61934990b663bb1b896af0eaa801f72e1f0eafe
--- /dev/null
+++ b/context/entrypoint
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+CONFIG_SOURCE="/opt/nut-config"
+export CONFIG_SOURCE
+
+CONFIG_DEST="/etc/nut"
+export CONFIG_DEST
+
+echo "Copying configurations from ${CONFIG_SOURCE} to ${CONFIG_DEST}..."
+
+find $CONFIG_SOURCE -name "*.conf" -exec cp '{}' $CONFIG_DEST \;
+find $CONFIG_SOURCE -name "*.html" -exec cp '{}' $CONFIG_DEST \;
+find $CONFIG_SOURCE -name "*.users" -exec cp '{}' $CONFIG_DEST \;
+
+echo "Restarting services..."
+
+upsdrvctl start
+/etc/init.d/nut-server restart
+
+echo "Ready on port 3493"
+tail -F /dev/null
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ec0ea1daf2af1b2872479b345a5111bf23f58643
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,11 @@
+version: "3.7"
+services:
+  nut:
+    image: nut-upsd:latest
+    build:
+      context: ./context
+    ports:
+      - 3493:3493
+    restart: unless-stopped
+    volumes:
+      - ./config/:/opt/nut-config