I recently published a docker image for 1T that allows it to run headlessly on a server, the source for which can be found here.
Though close, it isn't quite ready for release as a self-hostable app/locally running dockerized application. The following are the main issues that would help make it a smoother experience, both to maintain and use.
Configuration
Most programs that are packaged as docker images allow configuration via three sources: Configuration files, environment variables, and command line options. They also typically follow these conventions:
Note: I realize that you probably already know much of this, but for the sake of completeness, potential future documentation, and just to help think it through, I included a large amount of detail.
Extra note: Much of this may well be biased, and based on my personal opinion; but I believe this to be the unwritten standard that most dockerized programs follow - especially in the case of k8s. I am considering writing up a (entirely optional and unofficial, unless it actually gains traction) standard, an RFC of sorts, to help unify configuration across the many different docker apps. I am happy to help further define and create additional documentation surrounding it, should the idea be pursued.
- Configuration is by order of priority, where a configuration file is the lowest, environment variables are in the middle, and command line options (
--opt) are the highest.
- Some programs forego this in favour of the mutually exclusive model - if an option is specified twice, the program will exit with an error. While this is better UX for new users, it doesn't allow for various things such as defaults and overrides - a better solution might be to always throw an informational message if a option is overridden.
- Most configurations follow a tree-like structure, comprising of sections, keys, and values.
- In each of the three formats (file, cli, env), sections, keys, and values are separated by a delimiter.
- YAML uses whitespace/indentation as the key delimiter, with a colon (
:) to delimit a key and a value. Any other configuration format could technically be used, but YAML is typically the easiest to use and the most human readable.
- CLI depends on the implementation, Traefik uses a period (
.) as the key delimiter, and either a whitespace ( ) or equals (=) to delimit keys from a value
- Environment variables typically use an underscore (
_) as the key delimiter, and the key/value delimiter depends on how the variable is set (though typically an equals sign is used). Environment variables are typically all capitalized, but some implementations support camel, snake, pascal, or mixed case.
- It is easy to visualize this with yaml - see this Traefik configuration for reference.
- A section is usually a descriptive name that groups several keys together.
- A key is usually a descriptive name for a configuration option, which can then be mapped to a value
- A value is linked to a key, and can be of several types:
- Boolean
- A simple true or false. Interpretation depends on the programs implementation, but typical values include the boolean strings
true or false, 0 and 1, and in some cases null (key not specified) or specified (i.e. key is present, but the value is null). The last case is easiest to visualize as a command line option - if you specify --debug, the key is specified, but has no value; however the mere presence of --debug implies that it should be true.
- Note: Some programs allow sections to also act as a key - for example, in Traefik, you can enable HTTP/2 support with
http2: true, in which http2 acts as a key - but http2 also has sub options, such as http2.MaxConcurrentStreams: 42. Specifying a sub option implicitly sets http2: true.
- String
- An arbitrary collection of literal characters, i.e.
value123
- Note: This can quickly get confusing when managing multiple different layers (i.e. when using a variable in docker-compose, and passing the value of said variable to a shell, which may have different rules for escaping special characters)
- List
- A list is typically an unordered collection of values corresponding to a single key
- Array
- An array is typically an ordered list, with it's own
key: value mapping. Each section is technically an array (each section has multiple key: value mappings), but this can also be useful when referring to user-defined sections - such as users.<USERNAME>.passwordhash: sha256, where the value <USERNAME> is specified by the user rather than the program.
- Environment variables:
- For a working example, see the Traefik configuration reference
- Typically prefixed with an application name, i.e.
ONETAGGER_ to prevent collisions with other programs
- The prefix is often an abbreviation rather than the full name - i.e.
1T instead of ONETAGGER
- Secret values (such as API tokens) can typically be expanded from a file, by setting the environment variable with an additional
__FILE suffix, e.g. 1T_MY_SECRET__FILE = /path/to/file. At runtime, the file is parsed, such that the contents of the file are used instead.
- The implementation varies from program to program - some are implemented directly in the program itself (the program handles the process of grabbing the variable from the file), whereas some are implemented in an entrypoint script that expands all variables with the
__FILE suffix, setting the corresponding environment variable with the contents of the file.
- Note: I would personally recommend implementing it directly in the program/library (see note below), both to make configuration consistent across platforms (since the entrypoint is typically limited to docker images only), and to make it more secure - if the entrypoint expands environment variables, they are still available via
/proc to anything that can read it, even if they are unset post startup. Handling expansion directly ensures that the values can be censored properly, and read permissions restricted to the user running 1T only.
Extra note: it may be worth writing this as a library in rust rather than building it in to 1T directly - there are some other projects (primarily Kanidm) which I could see benefitting from this. Especially if, as I mentioned earlier, I end up writing a standard that apps could follow - having a library that can simply be included into a project (which would make it that much easier for projects that don't want to implement it themselves)
Extra extra note: using a file is typically used by the image builder to specify relevant default options (since the file takes lowest priority) - for example, running as a server (headlessly), and listening on all addresses instead of localhost. Environment variabls are typically where end user configuration occurs, and command line is typically used as enforced configuration (since they can't overwritten by any of the other configurations.)
In the immortal words of larry the cucumber...
... Oofta.
Along with the above system of configuration, the following are a list of options/keys (in no particular order) that aren't available now (to my knowledge), but would greatly simplify packaging 1T as a docker image.
- Location of all the various files (configuration, logs, and media, which is already implemented)
- Listen address/ports for the websocket/webserver
- Allow listening on HTTPS with a self signed certificate
- Enabling automatic generation of the certificate (or even generating one and using HTTPS by defualt) would be nice, but not necessary - it's a significant upgrade over plain HTTP in terms of security, and would help prevent people deploying this insecurely
- Better explanation of dependencies
- This isn't a configuration option, but would be very appreciated so I can (hopefully) use alpine instead of ubuntu for the image base, which would make it much slimmer and overall faster. The dockerfile I currently use ends up with an image roughly ~800 MB in size, of which the 1T binary only takes 30 (ubuntu pulls in a LOT of dependencies with the current apt command).
- Last but not least, authentication. Even if it's basic, it would be nice to have some form of authentication - ideally direct OpenID connect integration, but since that may be more complicated than it's worth at the moment (for an app intending to be single-user only), HTTP basic auth and HTTP header auth (which could then be plugged into oauth2-proxy for full OpenID integration) would be nice.
These next ones would be nice to have, but aren't absolutely critical
- Logging target - I'm unsure what the usual is on this one, it might help to take a peek at https://linuxserver.io's images and see if they do something special. Basically where logs end up, i.e. syslog instead of a separate file
- Remove leading path (i.e.
/data/music in the docker image that I linked) from the UI
- Other options (as relevant) relating to the webserver
Parting thoughts
I'm going to open an issue with Kanidm (a fellow rustacean project) that references this, to see if they would be interested in any of the configuration bit. In that case, collaboration for the library might be a possible option - but of course, it depends entirely on whether or not either project wants to implement this "standard", let alone write a library to implement it. I fully recognize that this project is developed with your own free time, and that if I really want any of this implemented, I should do it myself - you have no obligation to implement any of it.
As I was writing this I also noticed oauth2-proxy's pinned issue... which matches perfectly with my idea of a standard. I'll link them here as well.
Cheers!
~ TheRealGramdalf
I recently published a docker image for 1T that allows it to run headlessly on a server, the source for which can be found here.
Though close, it isn't quite ready for release as a self-hostable app/locally running dockerized application. The following are the main issues that would help make it a smoother experience, both to maintain and use.
Configuration
Most programs that are packaged as docker images allow configuration via three sources: Configuration files, environment variables, and command line options. They also typically follow these conventions:
Note: I realize that you probably already know much of this, but for the sake of completeness, potential future documentation, and just to help think it through, I included a large amount of detail.
Extra note: Much of this may well be biased, and based on my personal opinion; but I believe this to be the unwritten standard that most dockerized programs follow - especially in the case of k8s. I am considering writing up a (entirely optional and unofficial, unless it actually gains traction) standard, an RFC of sorts, to help unify configuration across the many different docker apps. I am happy to help further define and create additional documentation surrounding it, should the idea be pursued.
--opt) are the highest.:) to delimit a key and a value. Any other configuration format could technically be used, but YAML is typically the easiest to use and the most human readable..) as the key delimiter, and either a whitespace () or equals (=) to delimit keys from a value_) as the key delimiter, and the key/value delimiter depends on how the variable is set (though typically an equals sign is used). Environment variables are typically all capitalized, but some implementations support camel, snake, pascal, or mixed case.trueorfalse,0and1, and in some cases null (key not specified) or specified (i.e. key is present, but the value is null). The last case is easiest to visualize as a command line option - if you specify--debug, the key is specified, but has no value; however the mere presence of--debugimplies that it should be true.http2: true, in whichhttp2acts as a key - buthttp2also has sub options, such ashttp2.MaxConcurrentStreams: 42. Specifying a sub option implicitly setshttp2: true.value123key: valuemapping. Each section is technically an array (each section has multiplekey: valuemappings), but this can also be useful when referring to user-defined sections - such asusers.<USERNAME>.passwordhash: sha256, where the value<USERNAME>is specified by the user rather than the program.ONETAGGER_to prevent collisions with other programs1Tinstead ofONETAGGER__FILEsuffix, e.g.1T_MY_SECRET__FILE = /path/to/file. At runtime, the file is parsed, such that the contents of the file are used instead.__FILEsuffix, setting the corresponding environment variable with the contents of the file./procto anything that can read it, even if they are unset post startup. Handling expansion directly ensures that the values can be censored properly, and read permissions restricted to the user running 1T only.Extra note: it may be worth writing this as a library in rust rather than building it in to 1T directly - there are some other projects (primarily Kanidm) which I could see benefitting from this. Especially if, as I mentioned earlier, I end up writing a standard that apps could follow - having a library that can simply be included into a project (which would make it that much easier for projects that don't want to implement it themselves)
Extra extra note: using a file is typically used by the image builder to specify relevant default options (since the file takes lowest priority) - for example, running as a server (headlessly), and listening on all addresses instead of localhost. Environment variabls are typically where end user configuration occurs, and command line is typically used as enforced configuration (since they can't overwritten by any of the other configurations.)
In the immortal words of larry the cucumber...
... Oofta.
Along with the above system of configuration, the following are a list of options/keys (in no particular order) that aren't available now (to my knowledge), but would greatly simplify packaging 1T as a docker image.
These next ones would be nice to have, but aren't absolutely critical
/data/musicin the docker image that I linked) from the UIParting thoughts
I'm going to open an issue with Kanidm (a fellow rustacean project) that references this, to see if they would be interested in any of the configuration bit. In that case, collaboration for the library might be a possible option - but of course, it depends entirely on whether or not either project wants to implement this "standard", let alone write a library to implement it. I fully recognize that this project is developed with your own free time, and that if I really want any of this implemented, I should do it myself - you have no obligation to implement any of it.
As I was writing this I also noticed oauth2-proxy's pinned issue... which matches perfectly with my idea of a standard. I'll link them here as well.
Cheers!
~ TheRealGramdalf